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.

loading core…

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

ItemWhat it exercises
cleanup attributeA 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-outThe 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 cornerGetting 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 demoNo 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.