Scope-Guard Ripple Tank
Algorithms & Numeric
A fixed-point 2-D ripple tank — but the compiler stress is
__attribute__((cleanup(fn))), C's "poor man's destructor". A guarded local
runs its cleanup at every exit of its scope: fall-through, early return,
loop break, nested-block close — and multiple guards run in reverse
declaration order. The compiler must fan those synthesized calls out to each control-flow
edge. Compiler stress-test #90.
Self-running — ripples spread and interfere; HUD shows the cleanup count.
Click the screen to focus, then watch. Tab away and it pauses.
What it is
Pebbles drop into a fixed-point wave-equation tank and the ripples spread, reflect, and
interfere. That's the visible part; the part that stresses the compiler runs once at
startup — a routine full of cleanup-guarded locals across branches, loops,
and nested blocks, whose scope-exit calls are folded into a CRC and checked against the host.
// __attribute__((cleanup)): the cleanup fn runs at EVERY exit of the variable's scope.
uint16_t process(uint16_t a, uint16_t b) {
uint16_t outer __attribute__((cleanup(guard))) = a; // runs last, on any exit
if ((a & 3) == 0) {
uint16_t g __attribute__((cleanup(guard))) = a ^ 0x1234;
return a + 1; // exit: g fires, then outer (compiler synthesizes both)
}
for (i = 0; i < 3; i++) {
uint16_t lg __attribute__((cleanup(guard))) = a + i;
if (((a+i) & 7) == 5) break; // early break: lg fires
}
{ uint16_t p __attribute__((cleanup(guard))) = b, q __attribute__((cleanup(guard))) = b^a; }
return a ^ b; // q, then p fired at the block close; outer fires here
} Cleanup handlers are how C code does RAII-style resource release (closing files, freeing locks) without a destructor. Correctly duplicating them onto every exit edge — and only those edges, in the right order — is a real obligation of the front-end + backend, and this demo checks it end to end on the 65816.
Compiler stress-test
| Item | What it exercises |
|---|---|
| cleanup attribute | A variable marked __attribute__((cleanup(fn))) has fn(&var) called automatically when the variable goes out of scope — GCC/Clang's "poor man's destructor". Clang synthesizes those calls (CGDecl.cpp:2254) at compile time. |
| scope-exit fan-out | The cleanup must run on EVERY way control leaves the scope: falling off the end, an early return, a break out of a loop, or the close of a nested block. The compiler duplicates (fans out) the cleanup call onto each of those control-flow edges — and multiple guards in one scope run in reverse declaration order. |
| the codegen corner | Getting this right means the backend emits the correct set of cleanup calls at the correct edges, in the correct order, with no double-runs and no missed exits. The gate's CRC folds the exact sequence of cleanup calls, so any missed/extra/reordered call diverges it. |
| first cleanup-attr demo | No other demo in the battery uses the cleanup attribute. The ripple tank is just the visual witness that the program runs; the compiler stress is the synthesized scope-exit call sequence, checked at startup. |
Written in C with the llvm-mos 65816 toolchain and verified against
bsnes-jg and MAME. Hit Verify fidelity to reproduce the build gate's WRAM assert
(cleanup CRC 0x05A3 — a fold of the exact scope-exit call sequence) live in this tab.
No far pointers — host == default == +mos-a16 == +mos-xy16,
-verify clean.