8000 [RFC] Add compile-time checking of mp_printf format strings by jepler · Pull Request #17556 · micropython/micropython · GitHub
[go: up one dir, main page]

Skip to content

[RFC] Add compile-time checking of mp_printf format strings #17556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

jepler
Copy link
Contributor
@jepler jepler commented Jun 24, 2025

Summary

It's always nice when errors can be detected at compile time. In traditional C programs, gcc can check that the printf argument types match the printf format string. This has not been possible up to now with mp_printf, because it has both extensions to standard printf (e.g., the %q format type) and is missing things in standard printf (e.g., %zd is not supported).

To that end, I have developed a GCC plugin that does this checking at compile time. I've also made the necessary changes for the unix coverage build to complete with the plugin enabled, and enabled it during the coverage build process.

Creating in draft mode to get feedback and also because this is cumulative with some other outstanding PRs that are needed to get the CI board to green.

Testing

I built the unix port coverage variant & ran the tests locally. The plugin itself should cause no code changes. There is a small code growth reported, so one of the added casts must not actually be a no-op. I have not determined which one.

Trade-offs and Alternatives

As a gcc plugin this can only support gcc-based toolchains. clang and proprietary compilers would not work. This does not seem important, as this feature only produces diagnostics.

The plugin is GPL licensed. I started with a GPL-licensed plugin template, and plugins need to be GPL-or-compatible in license in order to be loaded in gcc anyway. The plugin code IMO does not affect the license situation of the output object code, as you'd get the exact same code with or without the plugin.

Missing support for:

  • Knowing whether %ll is runtime-supported
  • handling enum argmuents properly (just one enum was %d printed in the coverage test and I added a cast instead of fixing the checker)
  • Whether to add & use defines similar to standard PRId32 for printing mp_{u,}int_t values: some ports need %d and others %ld, and maybe some even need %lld (I think maybe 32-bit nanbox builds would require this, for example). e.g., #define PRIdPY "lld" next to typedef long long mp_int_t. This would replace (int) casts which was the easiest way to get local builds to finish.

CI may need a new package installed -- debian needed gcc-12-plugin-dev and there's no gcc-plugin-dev (w/o version number) package to install the 'usual' one). I tried to code this, we'll see if it works.

Whether to enable it on more ports. This could catch problems in port-specific files, or for different fundamental object sizes.

Adding support for cmake-based builds and any other oddball build configurations

@jepler
Copy link
Contributor Author
jepler commented Jun 24, 2025

Example diagnostic:

coverage.c: In function ‘extra_coverage’:
coverage.c:206:9: error: argument 3: expected ‘long int’ or ‘long unsigned int’, not ‘int’ [-
Werror=format=]
  206 |         mp_printf(&mp_plat_print, "%ld\n", 123); // long
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors

@jepler jepler force-pushed the compile-time-format-checker branch 2 times, most recently from df0062d to c39e2bd Compare June 24, 2025 08:49
Copy link
codecov bot commented Jun 24, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.23%. Comparing base (431b791) to head (3bb0775).

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #17556      +/-   ##
==========================================
- Coverage   98.23%   98.23%   -0.01%     
==========================================
  Files         171      171              
  Lines       22140    22139       -1     
==========================================
- Hits        21749    21748       -1     
  Misses        391      391              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
github-actions bot commented Jun 24, 2025

Code size report:

   bare-arm:   -32 -0.056% 
minimal x86:  -355 -0.189% 
   unix x64:  -312 -0.036% standard
      stm32:   -40 -0.010% PYBV10
     mimxrt:   -56 -0.015% TEENSY40
        rp2:   -72 -0.008% RPI_PICO_W
       samd:   -40 -0.015% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:   -76 -0.017% VIRT_RV32

@jepler jepler force-pushed the compile-time-format-checker branch 3 times, most recently from 0153502 to 536f6fb Compare June 24, 2025 16:48
@jepler
Copy link
Contributor Author
jepler commented Jun 24, 2025

the plugin can also run during the gcc windows builds. I tested it locally using the cross-building steps but I assume it'd work on windows too. I won't complicate this PR by adding it, but I'd plan to add it in a subsequent PR. Some of the format string "findings" came out of me doing that locally.

@jepler
Copy link
Contributor Author
jepler commented Jun 25, 2025

@dpgeorge Please let me know if you think this is worth pursuing.

@jepler
Copy link
Contributor Author
jepler commented Jun 25, 2025

I did some checks in a sibling branch, and on macos, gcc is clang (!) which has a different incompatible plugin api; gcc-14 is also installed via brew in the runner, but it is missing some indirect dependency required for plugin building (/opt/homebrew/Cellar/gcc@14/14.3.0/bin/../lib/gcc/14/gcc/aarch64-apple-darwin23/14/plugin/include/system.h:700:10: fatal error: 'gmp.h' file not found). this may be solvable with brew, but as this would not cover any additional source files or integer size models it's probably not worth the time to figure out how to fix it.

@jepler
Copy link
Contributor Author
jepler commented Jun 25, 2025

Plugin support on Windows/MinGW has a number of limitations and additional requirements so adding support for that would better be postponed to a subsequent PR.

@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Jul 4, 2025
@dpgeorge
Copy link
Member
dpgeorge commented Jul 4, 2025

Please let me know if you think this is worth pursuing.

I'm mildly in favour of this.

The three main things to consider would be:

  1. The code being GPL licensed: no GPL code in this repo can be part of compiled firmware/executables. But the code added here is only part of the toolchain, which is OK. After all, gcc itself is GPL, and having this new plugin under GPL is the same as that, it has the same scope.
  2. Increased complexity of the build process: before building any object files the plugin needs to be built. I guess this is fine, although it does add yet more rules to Make (and eventually CMake).
  3. Being enabled by default: this will probably annoy some developers who would either need to install gcc-plugin-dev or disable it each time with DISABLE_PLUGIN=1. Luckily Arch Linux includes gcc-plugin-dev by default (for both gcc and arm-none-eabi-gcc) so that lowers the bar somewhat.

I did test out this PR locally with the unix coverage build and it works well.

It looks like this did find same cases of mp_printf that need to be fixed, so I guess that's a big reason to have it.

@jepler
Copy link
Contributor Author
jepler commented Jul 4, 2025

Thanks for the feedback.

I did consider trying to automatically enable the plugin if possible, and disable it if not; but this looked fragile.

One option would be to switch it to requring the plugin to be enabled (and enabling it during as many CI jobs as possible). A developer who runs into a diagnostic during CI would then have the option to install the plugin and use ENABLE locally, or whether to make a stab at fixing it and submit another job to CI.

Would it make more sense to work on fixing the diagnosed format problems in a separate PR (some of them do seem to be "real bugs" that will bite at runtime) and then bring the format checker in later? Or are you content to let the fixes and the checker land at the same time, which would make the fixes later?

Final question: C99 "solves" some format string problems by using macros like PRId64 that expand to a correct format string depending on the underlying types (e.g., it might expand to "lld" on an LLp64 sytem or "ld" on an LP64 system). What do you think of introducing such macros in micropython for mp_int_t/mp_uint_t? It looks like there's also a problem integer-printing pointer-width types which leads to the current Windows build failures (and which I'll correct)..

(Some issues are getting fixed in #17538 because the problems DID turn up during CI; this branch will need to be rebased when that one goes in)

@dpgeorge
Copy link
Member
dpgeorge commented Jul 4, 2025

One option would be to switch it to requring the plugin to be enabled

Yes, I also thought about that.

At the very least, I think the option should be in the positive tense, eg GCC_ENABLE_MP_PRINTF_PLUGIN, and that could either be disabled by default, or enabled by default on selected builds (eg just unix coverage).

Would it make more sense to work on fixing the diagnosed format problems in a separate PR

Yes, please. That PR would be a much easier thing to review and merge.

Final question: C99 "solves" some format string problems by using macros like PRId64
...
What do you think of introducing such macros in micropython for mp_int_t/mp_uint_t?

Yes, I think that's a good idea.

I have many times considered using the PRIxxx macros for all printf strings. But it's a fair bit of work to do that. But a good idea to start with them for mp_int_t/mp_uint_t.

@jepler
Copy link
Contributor Author
jepler commented Jul 4, 2025

My thoughts on sequencing the work:

  1. Land Coverage test sys.settrace & improve coverage #17538 because it has some initial format-string fixes
  2. Cherry pick just the fixes from this PR into a new branch and get it to pass CI.
  3. Introduce PRI-macros if it helps. Internally I'll use the format checker during this step.
  4. Finally, once that new format string fix PR is in, rebase & return to this PR.

@jepler jepler force-pushed the compile-time-format-checker branch 3 times, most recently from 990966c to f0a7d80 Compare July 5, 2025 15:32
jepler added 9 commits July 5, 2025 16:34
The name field of type objects is of type uint16_t for efficiency,
but when the type is passed to mp_printf it must be cast explicitly
to type qstr.

These locations were found using an experimental gcc plugin
for mp_printf error checking, cross-building for x64 windows
on Linux.

Signed-off-by: Jeff Epler <jepler@gmail.com>
Signed-off-by: Jeff Epler <jepler@gmail.com>
Signed-off-by: Jeff Epler <jepler@gmail.com>
The type of the argument must match the format string. Add
casts to ensure that they do.

It's possible that casting from `size_t` to `unsigned` loses
the correct values by masking off upper bits, but it seems likely
that the quantities involved in practice are small enough that
the %u formatter (32 bits on most platforms, 16 on pic16bit) will
in fact hold the correct value.

The alternative, casting to a wider type, adds code size.

These locations were found using an experimental gcc plugin
for mp_printf error checking, cross-building for x64 windows
on Linux.

In one case there was already a cast, but it was written
in
8000
correctly and did not have the intended effect.

Signed-off-by: Jeff Epler <jepler@gmail.com>
These locations were found using an experimental gcc plugin
for mp_printf error checking.

Signed-off-by: Jeff Epler <jepler@gmail.com>
During the coverage test, all the values encountered are within the
range of %d.

These locations were found using an experimental gcc plugin
for mp_printf error checking.

Signed-off-by: Jeff Epler <jepler@gmail.com>
On the nanbox build, `o->obj` is a 64-bit type but `%p` formats
a 32-bit type, leading to undefined behavior.

Print the cell's ID as an integer instead.

It can't be printed in hex because the '%llx' format specifier is not
supported in mp_printf.

This location was found using an experimental gcc plugin for mp_printf
error checking.

Signed-off-by: Jeff Epler <jepler@gmail.com>
we still want this not to crash a runtime but the
new static checker wouldn't like it.

Signed-off-by: Jeff Epler <jepler@gmail.com>
Signed-off-by: Jeff Epler <jepler@gmail.com>
jepler added 2 commits July 5, 2025 16:34
This adds support for %llx (needed by XINT_FMT for printing cell
objects) and incidentally support for capitalized output of %P.

Signed-off-by: Jeff Epler <jepler@gmail.com>
Signed-off-by: Jeff Epler <jepler@gmail.com>
@jepler
Copy link
Contributor Author
jepler commented Jul 5, 2025

huh. two wrongs make a "fascinating". I didn't expect to see code size savings. Is it coming from 3f9b9c4, which I had actually intended to rebase out? Let's find out over at #17618

@jepler jepler force-pushed the compile-time-format-checker branch from f0a7d80 to 3bb0775 Compare July 5, 2025 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
py-core Relates to py/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0