fix(drv): the cross-arch gate was green because it could not see the defect - #316
Open
avrabe wants to merge 7 commits into
Open
fix(drv): the cross-arch gate was green because it could not see the defect#316avrabe wants to merge 7 commits into
avrabe wants to merge 7 commits into
Conversation
…defect The gate merged in #310 asserted `undefined_count > 0` over a hand-written list of 8 drivers. Both halves were wrong, and they cancelled into a green light. **It gated 8 of 13.** `DRIVERS=(wdg gpio i2c timer adc dac pwm uart)` while thirteen `*-thin` directories exist — can, hm, mpu, spi and switch were never checked. Third enumerated list this month to silently exclude the newest thing; now discovered, like check-providers.sh and the Lean target list before it. **`> 0` is not the property.** It is satisfied by ANY undefined symbol, including a leaked one. Every RISC-V object carries a dangling `synth_func_N`, so every one of them counted as passing. And `hm-thin` imports nothing, so a CORRECT object has zero undefined symbols — under `> 0` a correct driver fails. The rule is equality: the object's undefined set must equal the names the wasm imports. Same rule check-driver-components.py already uses on its object axis. Correct for a driver importing nothing, for a seam that is not gust:hal (mpu-thin -> mpu-write, switch-thin -> the three ctx symbols), and it rejects a leak. What the correct rule finds, across all 13: ARM 13/13 clean. Undefined set == imports, exactly. No leak, no truncation. That half of the cross-arch claim holds. RISC-V 0/13. Twelve emit an unlinkable object — synth's RV32 selector skips a function it cannot select and emits the call site anyway, leaving `synth_func_N` dangling (definitions are named `func_N`, so it never resolves). mpu-thin emits nothing: GlobalGet(0) is unsupported by the RV32 skeleton, 8 of 20 functions skipped, and synth then exits non-zero (#952). For a skipped INTERNAL function synth exits 0, so the unlinkable object ships with only a warning. That is what let `> 0` read green. Not a regression: synth 0.52.0 produces byte-identical results to 0.58.0 on today's sources (bisected 0.52/0.53/0.54/0.55/0.57/0.58). The RISC-V leg is carried as an explicit ledger — if a driver starts crossing cleanly the gate FAILS until it is removed from the list, so the ledger shrinks to empty instead of outliving the bug. CROSS-ARCH.md's table was stale, not wrong-at-the-time: it measured pre-componentization sources (wdg-thin had 6 functions then, 21 now) and was never re-measured when they changed. Re-measured here, and the section explaining why `> 0` was the wrong rule replaces the one that justified it. Also removes check-driver-components.sh — a second, unreferenced gate for REQ-DRV-COMPONENT-001 that I duplicated in .py without noticing. Two gates for one requirement is itself a drift hazard. Its red-first framing was right and is preserved in the .py. build-cross-arch.sh is kept as a reporting tool (it prints byte sizes) and as the subject of the workflow's pin-guard negative control, marked superseded as a gate, and now discovers drivers rather than enumerating them. Kill-criterion: `check-cross-arch.py --self-test` must reject a leaked-symbol superset and must PASS an empty expectation. Both verified, and both run in CI before the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
Re-tested the RISC-V leg on synth 0.60.0 (the latest release, our pin is 0.58.0): 12 of 13 drivers still exit 0 while emitting a dangling `synth_func_N`, and `ld.lld` refuses to link them — ld.lld: error: undefined symbol: synth_func_18 >>> referenced by out.o:(gust:hal/gpio@0.1.0#set) referenced by two EXPORTED entry points, so no gc-sections pass drops it. Bumping the pin would not clear the ledger. Filed as synth#1102. The premise of synth#1013 — that ARM and RISC-V decline cleanly where aarch64 panicked — does not hold when the declined target is an internal rather than a requested export: the #952 export guard never fires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
…m this PR disproves The header said: 'On the pinned toolchain every driver crosses and the gate exits 0.' That was the `> 0` rule reading a dangling `synth_func_N` as a seam symbol. On the equality rule ARM holds for all 13 and RISC-V holds for none (synth#1102). Also states why the rule is neither '> 0' nor '== 0' but equality, and that the RISC-V ledger fails on a status change in either direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
…at does not link The v0.7.1 plan scopes this item as 'RISC-V from compiled to executed', on the premise that the objects already compile correctly and only lack a silicon run. They do not compile correctly. 0 of 13 drivers cross RISC-V on the corrected gate rule; ld.lld refuses every object (synth#1102, confirmed on 0.60.0). Records the blocker and the split that keeps the item shippable: the target-model half (generated RV32 target, memory.x, constants) is independent of the selector bug and can proceed; only the 'executed' half waits upstream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
…er installed check-cross-arch.py derives each driver's EXPECTED undefined set from the wasm's import section, so it needs wasm-tools. The old shell gate only ran nm, so drv-cross-arch.yml never installed it — this PR's gate would have exited 2 with 'FATAL: wasm-tools not usable' on the first CI run. Pinned to 1.245.1, the same version drv-components.yml uses, so both gates read imports through identical tooling. Also hardens synth_bin(): it shelled out to `varve which synth` unguarded, which raises FileNotFoundError where varve is not installed. CI never hit that path (it sets $SYNTH, checked first), but a local run without either would have crashed instead of falling back to PATH. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ted object Deleting check-driver-components.sh in this PR removed the only gate that globbed dma-own. That was a silent coverage regression, and it is the exact failure this PR is about — a gate whose scope excludes something without saying so. Restoring it surfaced two things that were invisible while nothing looked: 1. dma-own DOES NOT BUILD on rustc 1.97.0. rust-lld rejects the three raw externs (undefined symbol: dma_barrier / dma_irq_poll / dma_program). It has no .cargo/config.toml, unlike the five thin drivers that carry --allow-undefined for exactly this. Fixed by adding one. My earlier 'it builds' reading was a stale artifact from an older toolchain — the build had failed and I had sent stderr to /dev/null. 2. dma-own is NOT componentized. Its three TCB atoms land as raw TERM_SESSION_ID=w0t5p0:7E6A1C62-B482-423B-857E-357D1616A66E SSH_AUTH_SOCK=/var/run/com.apple.launchd.2B1x5eSyt7/Listeners LC_TERMINAL_VERSION=3.6.11 COLORFGBG=0;15 ITERM_PROFILE=Default OSLogRateLimit=64 XPC_FLAGS=0x0 LANG=en_US.UTF-8 PWD=/Volumes/Home/git/pulseengine/gale SHELL=/bin/zsh __CFBundleIdentifier=com.googlecode.iterm2 SECURITYSESSIONID=186a2 TERM_FEATURES=T3LrMSc7UUw9Ts3BFGsSyHNoSxFP TERM_PROGRAM_VERSION=3.6.11 TERM_PROGRAM=iTerm.app PATH=/Users/r/.wasmtime/bin:/Users/r/.opencode/bin:/Users/r/.nvm/versions/node/v20.19.2/bin:/Users/r/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/Users/r/.elan/bin:/Users/r/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/opt/zerobrew/prefix/bin:/Users/r/.zerobrew/bin:/Users/r/.cargo/bin:/Applications/iTerm.app/Contents/Resources/utilities:/Users/r/.lmstudio/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/rust-analyzer-lsp/1.0.0/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/frontend-design/unknown/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/commit-commands/unknown/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/superpowers/6.3.0/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/github/unknown/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/security-guidance/2.0.7/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/playwright/unknown/bin:/Users/r/.claude/plugins/cache/claude-plugins-official/clangd-lsp/1.0.0/bin:/Users/r/.claude/plugins/cache/pulseengine-eu/pulseengine-claude/0.20.0/bin LC_TERMINAL=iTerm2 COLORTERM=truecolor COMMAND_MODE=unix2003 TERM=xterm-256color TERMINFO_DIRS=/Applications/iTerm.app/Contents/Resources/terminfo:/usr/share/terminfo HOME=/Users/r TMPDIR=/var/folders/yp/nb2yryq12f9bnx48xdh11n8w0000gn/T/ USER=r XPC_SERVICE_NAME=0 LOGNAME=r LaunchInstanceID=F7BFEA73-4019-4630-9B3D-8A3BC077C845 __CF_USER_TEXT_ENCODING=0x0:0:0 ITERM_SESSION_ID=w0t5p0:7E6A1C62-B482-423B-857E-357D1616A66E SHLVL=2 OLDPWD=/Volumes/Home/git/pulseengine/gale/benches/gust/drivers ZEROBREW_DIR=/Users/r/.zerobrew ZEROBREW_BIN=/Users/r/.zerobrew/bin ZEROBREW_ROOT=/opt/zerobrew ZEROBREW_PREFIX=/opt/zerobrew/prefix PKG_CONFIG_PATH=/opt/zerobrew/prefix/lib/pkgconfig:/opt/zerobrew/prefix/lib/pkgconfig: HOMEBREW_PREFIX=/opt/homebrew HOMEBREW_CELLAR=/opt/homebrew/Cellar HOMEBREW_REPOSITORY=/opt/homebrew FPATH=/opt/homebrew/share/zsh/site-functions:/usr/local/share/zsh/site-functions:/usr/share/zsh/site-functions:/usr/share/zsh/5.9/functions INFOPATH=/opt/homebrew/share/info: __ETC_PROFILE_NIX_SOURCED=1 NIX_PROFILES=/nix/var/nix/profiles/default /Users/r/.nix-profile XDG_DATA_DIRS=/usr/local/share:/usr/share:/Users/r/.nix-profile/share:/nix/var/nix/profiles/default/share NIX_SSL_CERT_FILE=/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt NVM_DIR=/Users/r/.nvm NVM_CD_FLAGS=-q NVM_BIN=/Users/r/.nvm/versions/node/v20.19.2/bin NVM_INC=/Users/r/.nvm/versions/node/v20.19.2/include/node WASMTIME_HOME=/Users/r/.wasmtime NoDefaultCurrentDirectoryInExePath=1 COREPACK_ENABLE_AUTO_PIN=0 AI_AGENT=claude-code_2-1-245_agent CLAUDE_CODE_ENTRYPOINT=cli CLAUDE_CODE_MESSAGING_SOCKET=/tmp/cc-socks/30731.sock CLAUDE_CODE_MESSAGING_TOKEN=8aaee9c7b3ab23e93412e343f5b28d9d CLAUDE_CODE_BRIDGE_SESSION_ID=session_011QG86sovTbfnPNY9SfhSmo GIT_EDITOR=true CLAUDE_CODE_EXECPATH=/Users/r/.local/share/claude/versions/2.1.245 CLAUDECODE=1 CLAUDE_CODE_SESSION_ID=b9703d91-674b-4409-82ca-8c80975abd51 CLAUDE_CODE_CHILD_SESSION=1 CLAUDE_PID=30731 CLAUDE_EFFORT=high _=/usr/bin/env imports, not a typed WIT interface the way all 13 thin drivers do. That was the deleted shell gate's EXPECTED red — its header said so — but it was wired to no workflow, so the red was never seen. Whether dma-own is in REQ-DRV-COMPONENT-001's scope is a genuine question (the requirement says 'every thin-seam driver'; dma-own is an ownership FSM). That is a scope decision and is NOT made here. Its actual state is asserted instead: the gate fails if its raw env set changes in either direction. Adds a census over every committed *-cm3.o. Nine live outside the gated set — breadth, dma-own, os-node (5), and two providers. The composed/fused ones are not 1:1 lowerings, so gating them by this rule would produce false failures; they are LISTED instead. A committed object that is neither gated nor listed fails the gate, and so does a ledger entry whose object has vanished. Kill-criteria, both directions verified: ledger names a nonexistent object -> exit 5, names it new committed object appears unlisted -> exit 5, names it restored tree -> exit 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
…pecific
synth#1102 is fixed on main as #1104, and the fix corrects this PR's central
inference. I reported the defect as rv32-specific and cited the clean ARM column
as support. That was wrong: Thumb-2 and A32 shipped the identical dangling symbol
at exit 0 on a module that declines on those backends. The guard was missing
everywhere.
Our ARM objects are clean only because the ARM backend DECLINES NOTHING on this
corpus, so the broken path is never entered. 'ARM 13/13, undefined set equals
imports' remains a true measurement of those objects; it is not evidence that the
ARM lowering path is sound, and the gate comment now says so explicitly and warns
against upgrading it into one.
Both sides reached the same wrong conclusion from a probe artefact — upstream
from get_section_by_name('.symtab') returning nothing on synth's ARM builder,
here from a corpus that happens not to exercise the path.
The ledger stays: #1104 is on main, not in a release (0.60.0 is the latest tag),
and our pin is 0.58.0. When we pin a build carrying it, the ledger fails and
forces itself to shrink rather than outliving the bug.
Also searched our corpus for the residual case they asked for (a declined
function referenced only from a funcref table entry) and reported the negative
result: switch-thin and mpu-thin both carry call_indirect and both decline
heavily, but neither has any overlap between declined indices and elem-referenced
ones. That incidentally explains why switch-thin declines 13 functions but shows
only 10 dangling symbols — the other three are dead, not table-referenced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reopened from #313, which GitHub auto-closed when its base branch was deleted on #312's merge. Same branch, now targeting
mainso the gates actually run — the zero-checks state on #313 was an empty gate, not a pass.The gate merged in #310 was green for the wrong reason
It asserted
undefined_count > 0over a hand-written list of 8 drivers. Bothhalves were wrong, and they cancelled into a green light.
It gated 8 of 13.
DRIVERS=(wdg gpio i2c timer adc dac pwm uart)whilethirteen
*-thindirectories exist.can,hm,mpu,spi,switchwerenever checked. This is the third enumerated list this month to silently exclude
the newest thing — after
check-providers.shand the Lean target list. Nowdiscovered, like those.
> 0is not the property. It is satisfied by any undefined symbol,including a leaked one — and
hm-thinimports nothing, so a correct objecthas zero undefined symbols and would fail.
The rule is equality: the object's undefined set must equal the names the wasm
imports. Same rule
check-driver-components.pyalready applies on its objectaxis.
What the correct rule finds
Full table (re-measured, synth 0.58.0)
read32 write32read32 write32read32 write32read32 write32(none)read32 write32mpu-writewrite32read32 write32ctx-resume ctx-save region-swapread32 write32poll read32 write32read32 write32Twelve RISC-V objects cannot be linked: synth's RV32 selector skips a function
it cannot select and emits the call site anyway, leaving a dangling
synth_func_N(definitions are namedfunc_N, so it never resolves). On ARMthe same internals are emitted as local
t func_N, all defined — the defect isspecific to the RISC-V backend.
For a skipped internal function synth exits 0, so the unlinkable object
ships with only a warning. That is exactly what let
> 0read green.Not a regression. Bisected 0.52.0 / 0.53.0 / 0.54.0 / 0.55.0 / 0.57.0 /
0.58.0 — all produce identical results on today's sources. Filing upstream
separately.
The RISC-V leg is a ledger, not a skip
If a driver starts crossing cleanly the gate fails (exit 5) until it is
removed from
RISCV_KNOWN_BAD. The list shrinks to empty instead of outlivingthe bug. A driver that newly breaks also fails.
CROSS-ARCH.md was stale, not wrong-at-the-time
It measured pre-componentization sources —
wdg-thinhad 6 functions then, 21now — and was never re-measured when they changed. Re-measured here. The
section that justified
> 0is replaced by one explaining why it was the wrongrule.
Also: a duplicate gate I created
check-driver-components.shalready existed — a red-first shell gate forREQ-DRV-COMPONENT-001 — when I wrote the
.pyfor the same requirement. Twogates for one requirement is itself a drift hazard, and the
.shwas wired tonothing. Removed; its red-first framing is preserved in the
.py.Kill-criterion
check-cross-arch.py --self-testmust reject a leaked-symbol superset and mustpass an empty expectation:
Both run in CI as a step before the gate.
🤖 Generated with Claude Code
https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo