Skip to content

A plugin that watches GCC and changes nothing - #63

Merged
tamnd merged 2 commits into
mainfrom
gxplug
Sep 5, 2026
Merged

tamnd merged 2 commits into
mainfrom
gxplug

Conversation

@tamnd

@tamnd tamnd commented Sep 5, 2026

Copy link
Copy Markdown
Owner

gxplug v0.1, the last piece of the M0 toolchain, plus the reader for what it emits.

What it does

The text dumps say what the IR looked like at a few chosen points. They do not say which passes ran, in what order, on which function, how long each took, or what the compiler believed about the IR at that moment. This plugin emits that as one JSON object per line.

Measured on corpora/programs/l1.c at -O2:

494 events, 247 passes ran, 44 left a mark
functions: f
0.0741 seconds inside passes
  0.007735  simdclone
  0.005141  expand
  0.004025  late_combine
  0.002623  cfg
  0.002384  ccp

-fdump-passes lists 395 passes. 247 of them actually run. 44 change something visible from outside. The gap between those three numbers is what T04 is about, and until now there was no way to measure the middle one.

The guarantee

gxplug never changes what GCC compiles. That is a rule a design document cannot enforce, so it is checked:

  • make check compiles the same program twice, with and without the plugin, and compares the assembly byte for byte.
  • The new plugin workflow runs that against four channels on both architectures: Debian sid, Fedora rawhide, Homebrew, and the rel image this project builds itself. They disagree about where the plugin headers live, what the compiler is called, and how a shared object resolves its undefined symbols. Every one of those differences is probed for in gxplug/Makefile rather than assumed, and this is what proves the probes work.
  • Dockerfile.plug now runs make && make check rather than make, so the image a reader pulls ships a plugin that has been shown not to change the code, not one that merely compiled.

That closes the M0 exit item about two distribution channels with four, and the three unchecked boxes on #4.

Two things that cost real time

Written down in the source so they cost nobody else any:

  • A basic_block holds a GIMPLE sequence or an RTL insn chain, in a union. Counting statements after expand does not fail a check. It reads whichever pointer happens to be there and segfaults in *rest_of_compilation.
  • emit-rtl.h declares a function taking an enum memmodel and does not include the header that defines it. This never shows up in tree, because GCC's own sources include memmodel.h first. Out of tree you get an error about an enum without a previous declaration, pointing at a line that has nothing to do with you.

The reader

gxray.plug turns a stream into Event, Run and Stream. Its PROPERTIES table restates bit values from gcc/tree-pass.h, which is exactly the kind of copy that goes stale, so test_plug.py reads that header out of the pinned tree and fails if the two disagree in either direction.

changed is deliberately narrow: statement count, insn count, block count, or property bits moved. A pass that rewrote an expression without moving any of those reports False, and the prose has to say so rather than let a reader conclude that most passes do nothing.

Also here

  • gxplug/README.md, the record format field by field, and the install table for all four channels.
  • just plugin, just plugin-probe, just plugin-check, just plugin-run FILE.
  • A needs_gcc_plugin marker, because a GCC 16 without its plugin development package is the ordinary case and should skip rather than fail.
  • 21 new tests. The suite is 1199 passing.

The text dumps say what the IR looked like at a few chosen points. They do
not say which passes ran, in what order, on which function, how long each
took, or what the compiler believed about the IR at the time. gxplug emits
that as newline delimited JSON.

On l1.c at -O2: 247 passes run and 44 leave a mark you can see from outside.
-fdump-passes lists 395. The gap between those three numbers is what T04 is
about, and until now there was no way to measure the middle one.

The one rule is that the plugin never changes what GCC compiles, and a rule
in a design document is not a guarantee. `make check` compiles the same
program twice, with and without, and compares the assembly byte for byte.
The new plugin workflow runs that against four channels on both
architectures: Debian sid, Fedora rawhide, Homebrew, and the rel image this
project builds itself. They disagree about where the headers live, what the
compiler is called, and how a shared object resolves its undefined symbols,
and every one of those differences is probed for rather than assumed.

Two things cost real time and are written down so they cost nobody else any.
A basic block holds GIMPLE or RTL in a union, so counting statements after
expand does not fail a check, it reads whichever pointer is there and
segfaults in *rest_of_compilation. And emit-rtl.h uses enum memmodel without
including the header that defines it, which never shows up in tree because
GCC's own sources include memmodel.h first.

gxray.plug reads the stream. Its property table restates bit values from
gcc/tree-pass.h, which is exactly the kind of copy that rots, so a test reads
that header out of the pinned tree and fails if the two disagree in either
direction.
Four channels built the plugin and four passed the byte comparison. All
three failures were in the scaffolding around it.

A dataclass looks its own class up in sys.modules while the module body is
still running, so loading gxray.plug by path without registering it first
fails on Python 3.14 with an AttributeError about NoneType. Fedora rawhide
passed only because its python3 is older.

A plugin is C++. The gcc driver knows what to do with a .cc file and goes
all the way to exec'ing a cc1plus that only the g++ package ships, so a
machine with gcc-16 and no g++-16 gets No such file or directory. Both the
Makefile and the test gate now ask the compiler whether it can compile C++
before believing it can build a plugin.

And the build fixture passed check=True with capture_output=True, which
reports an exit status and throws away the compiler error explaining it.
That cost a round trip through CI to learn something make had already said.

Also: test_the_lockfile_is_read_even_when_it_is_empty asserted the committed
lockfile was empty, which stopped being true the moment the matrix first
published. It now writes an empty file in a tmp dir, which is the thing it
was trying to test, and a second test checks that whatever is in the real
lockfile is a digest.
@tamnd

tamnd commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

All seven plugin jobs pass. Something worth writing down came out of them.

channel arch events passes run changed something
Debian sid amd64 496 248 46
Fedora rawhide amd64 496 248 46
our own rel build amd64 496 248 46
Homebrew arm64 494 247 44

Three independently packaged GCC 16.2.0 builds, configured by three different sets of people, produce an identical pass trace on the same input. The architecture is what moves the number, not the packaging. That is a better fact than anything I would have guessed, and T04 gets to use it.

@tamnd
tamnd merged commit 6209449 into main Sep 5, 2026
39 of 40 checks passed
@tamnd
tamnd deleted the gxplug branch September 5, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant