Skip to content

test(#275): execution gate for the self-contained reachable call graph#784

Merged
avrabe merged 3 commits into
mainfrom
feat/46-275-reachable-callgraph
Jul 16, 2026
Merged

test(#275): execution gate for the self-contained reachable call graph#784
avrabe merged 3 commits into
mainfrom
feat/46-275-reachable-callgraph

Conversation

@avrabe

@avrabe avrabe commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Finding first

The direct reachable call graph on the self-contained --cortex-m image already works — it shipped in #235 (v0.11.28). compile_all_exports walks reachable_from_exports (transitive closure over static call), and the self-contained cortex-m builder patches every internal BL to the emitted callee's address after layout. What was missing was a mechanical EXECUTION gate proving the standalone image actually runs — presence in nm doesn't catch a mis-patched or dropped BL.

This PR does not implement reachability (already done) and does not close #275. The issue's actual long pole — call_indirect table dispatch on the self-contained path — remains silicon-gated and out of scope (a unicorn value-differential can't verify the blx Thumb-bit property). It stays loud-declining.

What I measured

Case Today (main)
export → non-exported helper (1 level) ✅ helper emitted, BL patched, executes
export → mid → leaf (transitive) ✅ full closure emitted, all BLs patched
unreachable dead helper ✅ correctly ABSENT (closure, not emit-all)
mixed: direct helper + call_indirect ✅ per-function — direct helpers compile, only the call_indirect export loud-declines (#275 residual). The falcon shape.

So there is no direct-call bug to fix; the deliverable is the gate.

The gate (reachable_callgraph_275_selfcontained_differential.py)

Runs the DEFAULT self-contained image (its own Reset_Handler startup) on unicorn and diffs entry(x) vs wasmtime for 7 vectors. entry (the only export) calls two non-exported helpers, one of which calls a third — the result 8*x+8 depends on the whole transitive call closure being compiled in and every BL resolved.

Non-vacuity (proven, not asserted): a non-default cargo feature exports_only_275_probe exposes an EXPORTS_ONLY_275=1 hatch that reverts to the pre-#235 exports-only drop. With helpers dropped, entry's BL becomes an external relocation and the builder degrades to a link-me ET_REL object with no Reset_Handler — not self-contained. The gate detects this and goes RED. The default/release binary physically cannot carry the hatch (#[cfg(feature = ...)]-compiled out; the default binary ignores the env var). CI wires both directions: GREEN on the normal binary, RED on a separately-built probe binary.

Safety / anchors

  • Default codegen path untouched → frozen anchors byte-identical (verified: const_cse_reduction_242, heterogeneous_table_676 pass; existing #275 loud-decline oracle and #758 self-contained oracle pass).
  • clippy -D warnings clean with and without the feature.

Refs #275.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 3 commits July 16, 2026 18:45
…h + non-vacuity hatch

The DIRECT reachable call graph on the self-contained --cortex-m image was
already implemented (#235, v0.11.28): compile_all_exports walks
reachable_from_exports (transitive closure over static `call`) and the
cortex-m builder patches every internal BL to the emitted callee's address.
What was missing was a mechanical EXECUTION gate proving the standalone image
runs correctly — presence in `nm` doesn't catch a mis-patched or dropped BL.

Adds an artifact-derived execution differential
(reachable_callgraph_275_selfcontained_differential.py) that runs the DEFAULT
self-contained image (its own Reset_Handler startup) on unicorn and compares
`entry(x)` to wasmtime for 7 vectors. `entry` (the only export) calls two
non-exported helpers, one of which calls a third — result 8*x+8 depends on the
whole transitive closure being compiled in and every BL resolved. An
unreachable `dead` helper is confirmed ABSENT (a closure, not "emit all").

Non-vacuity: a TEST-ONLY `EXPORTS_ONLY_275=1` env hatch in compile_all_exports
reverts to the pre-#235 exports-only behavior (drop non-exported callees). With
helpers dropped, `entry`'s BL becomes an EXTERNAL relocation and the builder
degrades to a link-me ET_REL object with no Reset_Handler — not self-contained.
The gate detects this (ET_REL, not ET_EXEC) and goes RED, proving it catches
the historical #275 drop; GREEN with the shipped reachability walk.

Default path unchanged (hatch is an env-gated no-op) → frozen anchors
byte-identical. call_indirect residual unchanged: a mixed module (direct helper
+ call_indirect) still per-function loud-declines the call_indirect export while
the reachable direct-call helpers compile — the falcon shape, silicon-gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… RED non-vacuity)

Adds both directions to the trap-semantics-oracle job (already has
wasmtime/unicorn/pyelftools): the GREEN gate runs the default self-contained
image and diffs entry(x) vs wasmtime; the RED step asserts EXPORTS_ONLY_275=1
(pre-#235 exports-only drop) degrades to a non-self-contained ET_REL and the
oracle fails — `! ...` because RED is the expected, gated outcome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… binary

Cold-review fix: the previous EXPORTS_ONLY_275 env hatch was a permanent
"regress to the fixed #275 defect" switch in the shipped compiler — a footgun
with no precedent (SYNTH_SPILL_ON_EXHAUST/SYNTH_SHIFT_MASK_ELIDE gate real
alternative behaviors, not reproductions of a fixed bug).

Now gated behind a non-default cargo feature `exports_only_275_probe`. The
default/release build physically cannot carry the hatch (the block is
`#[cfg(feature = ...)]`-compiled out; the default binary ignores EXPORTS_ONLY_275
entirely). CI builds a SEPARATE probe binary (distinct --target-dir so it never
shadows the default) only for the RED non-vacuity step; every GREEN step uses
the normal binary.

Verified: default binary + EXPORTS_ONLY_275=1 -> GREEN (hatch inert);
probe binary + EXPORTS_ONLY_275=1 -> RED (ET_REL, not self-contained).
clippy clean with and without the feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 23601bf into main Jul 16, 2026
40 checks passed
@avrabe
avrabe deleted the feat/46-275-reachable-callgraph branch July 16, 2026 17:33
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.

arm backend compiles only exports, not the reachable call graph - blocks self-contained firmware

1 participant