From 408bf12b1a0e12998730e4d2f87a56aa222f720a Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 13 Aug 2026 17:38:56 +0200 Subject: [PATCH 1/2] fix(#952): a declined REQUESTED export now exits non-zero on --all-exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A function synth DECLINES was dropped from the output object with exit 0: a build gating on `$?` (every build) accepted an object silently missing a public entry point. Declining a non-exported internal helper (#235 reachability) stays a routine warning-only skip — only a decline of a function the module actually `(export ...)`s now fails the build. - `compile_all_exports` tracks, per skipped function, whether `func.export_name.is_some()`; if any REQUESTED export was skipped the compile now `anyhow::bail!`s (placed after the existing `compiled_funcs.is_empty()` bail, so a module whose sole export was skipped keeps its current "nothing to emit" message). - New opt-in `--allow-skipped-exports` restores the pre-#952 exit-0 behavior for callers who genuinely want the partial object (an --all-exports corpus sweep, where per-function declines are expected and counted downstream). Inert on the single-function --func-index/--func-name path, which already hard-errors on any decline. - Red-first integration test crates/synth-cli/tests/skipped_export_exit_952.rs: a declined export exits non-zero (RED before the fix), a negative control (skipping only a non-exported helper) stays exit 0 both before and after, and --allow-skipped-exports restores exit 0. Blast radius: six existing tests/oracles compile corpora expecting some functions to decline while asserting overall success, and needed --allow-skipped-exports added — crates/synth-cli/tests/{call_indirect_275_ selfcontained,i64_call_arg_decline_929,rv32_local_promo_flip_472}.rs and scripts/repro/{i64_param_518_riscv_loudskip,call_indirect_275_selfcontained_ differential,wast_conformance_928_differential}.py. The wast conformance sweep (scripts/repro/wast_conformance_928_differential.py) was verified to produce byte-identical CHECKS=240/240 / declined-by-reason output against a separately-built pre-fix baseline binary. Closes #952 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L --- crates/synth-cli/src/main.rs | 85 +++++++- .../tests/call_indirect_275_selfcontained.rs | 7 + .../tests/i64_call_arg_decline_929.rs | 7 + .../tests/rv32_local_promo_flip_472.rs | 8 + .../tests/skipped_export_exit_952.rs | 196 ++++++++++++++++++ ...indirect_275_selfcontained_differential.py | 6 +- scripts/repro/i64_param_518_riscv_loudskip.py | 8 +- .../wast_conformance_928_differential.py | 12 +- 8 files changed, 322 insertions(+), 7 deletions(-) create mode 100644 crates/synth-cli/tests/skipped_export_exit_952.rs diff --git a/crates/synth-cli/src/main.rs b/crates/synth-cli/src/main.rs index f6aae872..12c11551 100644 --- a/crates/synth-cli/src/main.rs +++ b/crates/synth-cli/src/main.rs @@ -543,6 +543,23 @@ enum Commands { /// grows down from the top of SRAM. #[arg(long, value_name = "BYTES")] stack_size: Option, + + /// #952: on the `--all-exports` path, declining a function the module + /// EXPORTS now exits non-zero by default — a build that gates on `$?` + /// (every build) must not ship an object silently missing a public + /// entry point. Declining a non-exported internal helper (pulled in + /// only for reachability, #235) never fails the build either way; + /// that skip is routine and stays a warning. Pass this flag to + /// restore the pre-#952 exit-0 behavior for callers who genuinely + /// want the partial object — e.g. an `--all-exports` corpus sweep + /// over many modules, where per-function declines are expected and + /// counted downstream rather than treated as a build failure. + /// Inert on the single-function `--func-index`/`--func-name` path, + /// which already hard-errors `?`-style on any decline of the one + /// requested function — there is nothing for this flag to loosen + /// there. + #[arg(long)] + allow_skipped_exports: bool, }, /// Disassemble an ARM ELF file (e.g., synth disasm output.elf) @@ -669,6 +686,7 @@ fn main() -> Result<()> { stack_layout, stack_size, proven_safe, + allow_skipped_exports, } => { // #882: track whether `-b/--backend` was given explicitly (the // mismatch diagnostics differ: an explicit backend lists the @@ -756,6 +774,7 @@ fn main() -> Result<()> { volatile_segments, stack_layout, proven_safe, + allow_skipped_exports, )?; // If --link requested, invoke the cross-linker @@ -1611,6 +1630,9 @@ fn compile_command( // VCR-MEM-004 (#901): path to scry's safe-accesses.json. Consumed only on // the --all-exports module path; `None` (the default) is byte-identical. proven_safe: Option, + // #952: opt-in to keep exiting 0 when a REQUESTED export is declined on + // the --all-exports path (default is now a hard, non-zero-exit refusal). + allow_skipped_exports: bool, ) -> Result<()> { // Validate backend exists let registry = build_backend_registry(); @@ -1683,6 +1705,7 @@ fn compile_command( volatile_segments, stack_layout, proven_safe, + allow_skipped_exports, ); } @@ -2801,6 +2824,13 @@ fn compile_all_exports( stack_layout: StackLayout, // VCR-MEM-004 (#901): scry's safe-accesses.json, or None (byte-identical). proven_safe: Option, + // #952: `--allow-skipped-exports` — keep exiting 0 when a REQUESTED + // export is declined (the pre-#952 behavior). Default false: a declined + // export is now a hard refusal, so a build gating on `$?` cannot ship an + // object silently missing a public entry point. Never affects a declined + // non-exported internal helper (#235 reachability) — that stays a + // warning-only skip either way. + allow_skipped_exports: bool, ) -> Result<()> { let path = input.context("--all-exports requires an input file")?; @@ -3535,7 +3565,13 @@ fn compile_all_exports( // Compile each function via the selected backend let mut compiled_funcs = Vec::new(); - let mut skipped_funcs: Vec<(String, String)> = Vec::new(); + // #952: the third field marks whether the skipped function is a REQUESTED + // export (`func.export_name.is_some()`) rather than an internal helper + // pulled in only for reachability (#235). That distinction is what the + // exit-code gate below keys on — a build gating on `$?` must fail when a + // named public entry point silently vanished, but not when an unexported + // implementation detail did. + let mut skipped_funcs: Vec<(String, String, bool)> = Vec::new(); // #778 phase 3: collect per-function WCET intermediates (own-body cycles + // direct call sites, or a decline) and a `func_` → position map, so the // module-level composer can resolve direct calls across the call graph AFTER @@ -3636,7 +3672,11 @@ fn compile_all_exports( code for it rather than a silent miscompile (GI-FPU-001, #369)", backend.name() ); - skipped_funcs.push((name.clone(), format!("unsupported operator: {reason}"))); + skipped_funcs.push(( + name.clone(), + format!("unsupported operator: {reason}"), + func.export_name.is_some(), + )); continue; } // VCR-PERF-002 Phase 2 (#494): fact-spec — behind SYNTH_FACT_SPEC and @@ -3745,7 +3785,7 @@ fn compile_all_exports( backend.name(), e ); - skipped_funcs.push((name.clone(), e.to_string())); + skipped_funcs.push((name.clone(), e.to_string(), func.export_name.is_some())); continue; } }; @@ -3889,7 +3929,7 @@ fn compile_all_exports( all_exports.len(), skipped_funcs .iter() - .map(|(n, _)| n.as_str()) + .map(|(n, _, _)| n.as_str()) .collect::>() .join(", ") ); @@ -3901,6 +3941,43 @@ fn compile_all_exports( ); } + // #952: a declined function the module EXPORTS — as opposed to an + // internal helper pulled in only for #235 reachability — is not a + // routine skip. Before this gate, the compile above still exited 0: a + // build gating on `$?` (every build) accepted the object and shipped it + // with a public entry point silently missing. Refuse loudly instead, + // unless the caller opted in with `--allow-skipped-exports` (the + // `--all-exports` corpus-sweep shape, where per-function declines are + // expected and counted downstream rather than treated as a build + // failure). Placed AFTER the `compiled_funcs.is_empty()` bail above so a + // module whose ONLY export was skipped keeps that existing message + // ("nothing to emit") rather than being relabeled here. + if !allow_skipped_exports { + let skipped_exports: Vec<&str> = skipped_funcs + .iter() + .filter(|(_, _, is_export)| *is_export) + .map(|(n, _, _)| n.as_str()) + .collect(); + if !skipped_exports.is_empty() { + let total_exports = all_exports + .iter() + .filter(|f| f.export_name.is_some()) + .count(); + anyhow::bail!( + "#952: {} of {} requested export(s) were skipped (not in the \ + output object): {}. Exiting non-zero rather than shipping an \ + object that is silently missing a public entry point — a build \ + gating on `$?` would otherwise accept it. Pass \ + --allow-skipped-exports if the partial object is what you \ + want (e.g. an --all-exports sweep over a corpus, where \ + per-function declines are expected and counted downstream).", + skipped_exports.len(), + total_exports, + skipped_exports.join(", ") + ); + } + } + // Check if any function has relocations (import calls) let has_relocations = compiled_funcs.iter().any(|f| !f.relocations.is_empty()); diff --git a/crates/synth-cli/tests/call_indirect_275_selfcontained.rs b/crates/synth-cli/tests/call_indirect_275_selfcontained.rs index 91ef1f98..4efe0b03 100644 --- a/crates/synth-cli/tests/call_indirect_275_selfcontained.rs +++ b/crates/synth-cli/tests/call_indirect_275_selfcontained.rs @@ -100,6 +100,13 @@ fn test_275_selfcontained_a32_still_declines_loudly() { path.to_str().unwrap(), "--target", "cortex-r5", + // #952: `entry` is this fixture's SOLE export, and it is the + // function this test expects to be loud-skipped (the residual + // A32 #275 decline). Since v0.57 a declined REQUESTED export + // exits non-zero by default; this test wants the partial object + // (to inspect its symtab below), which is exactly the opt-in + // `--allow-skipped-exports` case, not a regression. + "--allow-skipped-exports", "-o", elf, ]) diff --git a/crates/synth-cli/tests/i64_call_arg_decline_929.rs b/crates/synth-cli/tests/i64_call_arg_decline_929.rs index 67750a80..d22e591c 100644 --- a/crates/synth-cli/tests/i64_call_arg_decline_929.rs +++ b/crates/synth-cli/tests/i64_call_arg_decline_929.rs @@ -61,6 +61,13 @@ fn compile(tag: &str, wat: &str) -> (bool, String, String) { "cortex-m3", "--all-exports", "--relocatable", + // #952: `f` is the sole export in the two decline fixtures below, + // and it is exactly the function #929 declines — since v0.57 that + // exits non-zero by default. These tests want the decline (and + // its diagnostic) with the compile still reporting success, which + // is the opt-in `--allow-skipped-exports` case. A no-op on + // I32_ONLY, where nothing is skipped. + "--allow-skipped-exports", "-o", obj.to_str().unwrap(), ]) diff --git a/crates/synth-cli/tests/rv32_local_promo_flip_472.rs b/crates/synth-cli/tests/rv32_local_promo_flip_472.rs index dc6e40f4..a476938e 100644 --- a/crates/synth-cli/tests/rv32_local_promo_flip_472.rs +++ b/crates/synth-cli/tests/rv32_local_promo_flip_472.rs @@ -77,6 +77,14 @@ fn compile(rel: &str, out: &str, promo_on: bool) -> Vec { "rv32imac", "--all-exports", "--relocatable", + // #952: `gust_kernel.wasm`'s `gust_poll` export already declines + // on RV32 (unrelated pre-existing gap: `GlobalGet` unsupported in + // the RV32 skeleton) in BOTH arms of this corpus sweep. Since + // v0.57 that exits non-zero by default; this gate compares + // per-function BYTE SIZES across the arms and is unaffected by a + // function absent from both, so the partial object is exactly + // what it wants. + "--allow-skipped-exports", ]) .output() .expect("run synth compile"); diff --git a/crates/synth-cli/tests/skipped_export_exit_952.rs b/crates/synth-cli/tests/skipped_export_exit_952.rs new file mode 100644 index 00000000..b5cf1a99 --- /dev/null +++ b/crates/synth-cli/tests/skipped_export_exit_952.rs @@ -0,0 +1,196 @@ +//! #952 — a declined REQUESTED export must exit non-zero, not 0. +//! +//! Measured on v0.56.0: +//! +//! ```wat +//! (module +//! (func $g (param i32 i64) (result i32) (local.get 0)) +//! (func (export "f") (result i32) (call $g (i32.const 7) (i64.const 9)))) +//! ``` +//! +//! ```text +//! $ synth compile ctrl.wat -b arm -t cortex-m3 --all-exports --relocatable -o ctrl.o +//! warning: skipping function 'f': ... #929: call arg 1 is 64-bit ... Declining +//! rather than emitting a silent miscompile +//! warning: 1 of 2 functions were skipped (not in output): f +//! $ echo $? +//! 0 +//! $ llvm-objdump -t ctrl.o | grep 'F .text' +//! 00000000 l F .text 00000004 func_0 # only the callee — 'f' is gone +//! ``` +//! +//! `f` is the module's sole export. The compile that declined it exited 0, so +//! any build gating on `$?` — which is every build — ships the object missing +//! its one public entry point. +//! +//! # Why this test reads synth's own stdout/stderr, not `synth disasm` +//! +//! `proven_safe_imported_memory_932.rs` already hit this: the first version of +//! that test counted mnemonics in `synth disasm` output and passed on macOS +//! while returning ZERO on the ubuntu runner, because disassembly TEXT is +//! host-dependent (register-name spelling, mnemonic width suffixes, and so +//! on). This test reads the exit code (`ExitStatus`, not text) and synth's own +//! diagnostic text on stderr — both identical on every host — never +//! disassembly. +//! +//! # The asymmetry this test protects +//! +//! Declining a non-exported internal helper (pulled in only for #235 +//! reachability) is routine and must keep exiting 0 — that is the +//! NEGATIVE CONTROL below. Only a decline of a function the module actually +//! `(export ...)`s must flip the exit code. Getting this backwards (failing +//! the build on every skip) would break the `--all-exports` corpus-sweep +//! callers this repo already has (`wast_conformance_928_differential.py` and +//! others) that intentionally compile many modules expecting some functions +//! to decline — hence `--allow-skipped-exports`, tested last. + +use std::path::PathBuf; +use std::process::{Command, Output}; + +fn synth() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_synth")) +} + +fn workdir(tag: &str) -> PathBuf { + let d = std::env::temp_dir().join(format!("synth-952-{tag}")); + std::fs::create_dir_all(&d).expect("temp dir"); + d +} + +fn compile(dir: &std::path::Path, wat: &str, out_name: &str, extra: &[&str]) -> Output { + let src = dir.join("m.wat"); + std::fs::write(&src, wat).expect("write wat"); + let obj = dir.join(out_name); + let mut c = Command::new(synth()); + c.args([ + "compile", + src.to_str().unwrap(), + "-b", + "arm", + "-t", + "cortex-m3", + "--all-exports", + "--relocatable", + "-o", + obj.to_str().unwrap(), + ]); + c.args(extra); + c.output().expect("run synth compile") +} + +fn stderr(o: &Output) -> String { + String::from_utf8_lossy(&o.stderr).into_owned() +} + +/// gale's exact repro (via #929): `f` is the module's SOLE export, and it is +/// the function that gets declined (the callee's i64 param forces the i64 +/// register-pair marshalling #929 refuses on the CALLER, `f`). +const REQUESTED_EXPORT_DECLINED: &str = r#"(module + (func $g (param i32 i64) (result i32) (local.get 0)) + (func (export "f") (result i32) (call $g (i32.const 7) (i64.const 9)))) +"#; + +/// `f` is exported and compiles fine; `$hard` is an internal, NON-exported +/// helper pulled in only because `f` calls it (#235 reachability). `$hard`'s +/// f64 result makes IT decline on a soft-float target — but nothing asked for +/// `$hard` by name, so its absence is routine, not a build failure. +/// +/// Verified empirically before writing this test (against the unmodified +/// v0.56.1 binary) that this fixture actually PRODUCES a skip of a +/// non-exported function: `$hard` unreachable-but-uncalled produces no skip +/// at all (it is simply never compiled), so the helper MUST be called from +/// the export for `reachable_from_exports` to pull it in and then decline it. +const ONLY_HELPER_DECLINED: &str = r#"(module + (func $hard (result f64) (f64.sqrt (f64.const 2.0))) + (func $helper (result i32) (call $hard) (drop) (i32.const 1)) + (func (export "f") (result i32) (call $helper))) +"#; + +/// RED (must pass only after the fix): a compile that declines a REQUESTED +/// export exits non-zero. Before the #952 fix this test fails — the process +/// exits 0 with the export silently absent from the object. +#[test] +fn declined_requested_export_exits_nonzero() { + let dir = workdir("red"); + let out = compile(&dir, REQUESTED_EXPORT_DECLINED, "ctrl.o", &[]); + + // Anchor: the skip must actually have happened, not just an unrelated + // failure. If this assertion stops matching (e.g. #929's message text + // changes upstream), the test below is meaningless and must be revisited + // rather than silently passing on some other error. + let err = stderr(&out); + assert!( + err.contains("skipping function 'f'"), + "fixture must decline 'f' specifically (the anchor this test relies \ + on) — got:\n{err}" + ); + + assert!( + !out.status.success(), + "#952: a compile that declines a REQUESTED export ('f', the module's \ + sole export) must exit non-zero — a build gating on `$?` must not \ + accept an object silently missing its public entry point. \ + stderr:\n{err}" + ); +} + +/// NEGATIVE CONTROL, both before and after the fix: skipping only a +/// non-exported internal helper must still exit 0. This is the asymmetry +/// #952 explicitly preserves — routine helper skips are not build failures. +/// If this test ever starts failing, the fix over-broadened the gate to fail +/// the build on ANY skip, not just a skipped export. +#[test] +fn skipped_nonexported_helper_still_exits_zero() { + let dir = workdir("negctrl"); + let out = compile(&dir, ONLY_HELPER_DECLINED, "ctrl.o", &[]); + let err = stderr(&out); + + // Non-vacuity: a control that never exercises a skip proves nothing (the + // #275/A32 lesson — see call_indirect_275_selfcontained.rs). Anchor on + // BOTH the per-function warning naming the skipped helper AND the + // aggregate count, so a future refactor that stops skipping `$hard` + // (e.g. broadens f64 support) fails this test loudly rather than leaving + // it passing for the wrong reason. + assert!( + err.contains("skipping function") && err.contains("were skipped"), + "fixture must actually skip the non-exported helper for this control \ + to mean anything — got:\n{err}" + ); + assert!( + !err.contains("skipping function 'f'"), + "the EXPORT 'f' must not be the one skipped — this fixture is meant \ + to isolate a helper-only skip. stderr:\n{err}" + ); + + assert!( + out.status.success(), + "#952 negative control: skipping only a non-exported internal helper \ + (never asked for by name) must still exit 0 — only a declined \ + REQUESTED export may fail the build. stderr:\n{err}" + ); +} + +/// `--allow-skipped-exports` restores the pre-#952 exit-0 behavior for +/// callers who genuinely want the partial object (the `--all-exports` +/// corpus-sweep shape). +#[test] +fn allow_skipped_exports_restores_exit_zero() { + let dir = workdir("allow"); + let out = compile( + &dir, + REQUESTED_EXPORT_DECLINED, + "ctrl.o", + &["--allow-skipped-exports"], + ); + let err = stderr(&out); + assert!( + err.contains("skipping function 'f'"), + "the decline must still happen (and still warn) under the opt-out — \ + only the EXIT CODE changes. stderr:\n{err}" + ); + assert!( + out.status.success(), + "--allow-skipped-exports must restore exit 0 on a declined requested \ + export. stderr:\n{err}" + ); +} diff --git a/scripts/repro/call_indirect_275_selfcontained_differential.py b/scripts/repro/call_indirect_275_selfcontained_differential.py index c72e4c2c..4f836614 100755 --- a/scripts/repro/call_indirect_275_selfcontained_differential.py +++ b/scripts/repro/call_indirect_275_selfcontained_differential.py @@ -85,7 +85,11 @@ def check_selfcontained_a32_residual() -> int: its builder emits no funcref table, so a dispatch would be the #717 R11 collision (a silent miscompile).""" out = "/tmp/ci275_selfcontained_r5.elf" - r = run(["--target", "cortex-r5", "-o", out]) + # #952: `entry` is this fixture's sole export and the function expected + # to be loud-skipped here (the residual A32 decline this test verifies). + # Since v0.57 that exits non-zero by default; opt in to the partial + # object so the symtab/stderr checks below can still run. + r = run(["--target", "cortex-r5", "--allow-skipped-exports", "-o", out]) fails = 0 if r.returncode != 0: print(f"self-contained cortex-r5: compile hard-failed (expected skip-and-continue):\n{r.stderr}") diff --git a/scripts/repro/i64_param_518_riscv_loudskip.py b/scripts/repro/i64_param_518_riscv_loudskip.py index f1824d20..7a158303 100644 --- a/scripts/repro/i64_param_518_riscv_loudskip.py +++ b/scripts/repro/i64_param_518_riscv_loudskip.py @@ -51,8 +51,14 @@ def compile_riscv(): + # #952: every I64_PARAM_FNS entry below is a NAMED EXPORT this fixture + # expects to be loud-skipped — since v0.57 a declined requested export + # exits non-zero by default, so this analysis-only oracle (which wants + # the partial object to inspect stderr + the symtab, not a hard failure) + # needs the opt-in. cmd = [SYNTH, "compile", str(WAT), "-o", OUT, "-b", "riscv", - "--target", "riscv32imac", "--all-exports", "--relocatable"] + "--target", "riscv32imac", "--all-exports", "--relocatable", + "--allow-skipped-exports"] r = subprocess.run(cmd, capture_output=True, text=True, env={"PATH": "/usr/bin:/bin"}) if r.returncode != 0: diff --git a/scripts/repro/wast_conformance_928_differential.py b/scripts/repro/wast_conformance_928_differential.py index 578bead3..d4361fa3 100644 --- a/scripts/repro/wast_conformance_928_differential.py +++ b/scripts/repro/wast_conformance_928_differential.py @@ -128,7 +128,17 @@ def compile_module(wat_text: str, tmp: Path) -> tuple[Path, bytes]: obj = tmp / "m.o" r = subprocess.run( [SYNTH, "compile", str(src), "-o", str(obj), "--target", "cortex-m4", - "--all-exports", "--relocatable"], + "--all-exports", "--relocatable", + # #952: this IS the corpus sweep that issue names as the intended + # `--allow-skipped-exports` caller — individual per-function + # declines are expected and already counted below by reason + # (per-assertion, via `symbol-missing`), not a compile failure for + # the whole file. Without this flag, since v0.57 a module where ANY + # exported function declines (not just ALL of them) now exits + # non-zero, which would collapse partial-decline files into + # `compile-declined` and undercount CHECKED assertions this oracle's + # `emulations >= 263` floor depends on. + "--allow-skipped-exports"], capture_output=True, text=True, ) if r.returncode != 0 or not obj.exists(): From 7e77590a6f7224ba8bff32af3abfef2428919786 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 13 Aug 2026 19:16:41 +0200 Subject: [PATCH 2/2] fix(#952): teach the five corpus-sweep oracles to tolerate EXPECTED declines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Making a declined REQUESTED export fatal is the point of this PR, but it also breaks every downstream consumer that had been silently relying on exit 0. CI surfaced them one at a time because the ledger step is `set -e`: the first run died on unreachable_665, the second on i64_globals_643, with 17 of >=32 oracles never reaching the ledger. Rather than iterate one CI round per oracle, ran all 64 oracles from the two failing jobs locally against this branch AND against main. The difference is the blast radius, and it is exactly five: unreachable_665 rv32 skips `guarded` (if/else arity, #343) i64_globals_643 rv32 skips 6 i64-global exports i64_float_conv_869 arm skips the i64<->float family off m7dp trunc_sat_782 arm skips the trunc_sat family off m7dp float_select_return_782 trips its own "compile must succeed" control The main baseline matters: 9 of the 14 failures on this branch also fail on main (local env, not this change). Without that control I would have reported 14 and "fixed" 9 things that were never broken. Each of the five gets `--allow-skipped-exports` — the corpus-sweep shape the flag was added for. That flag SILENCES the gate this PR exists to add, so it is only safe where something else still notices a NEW decline. Here that is the non-vacuity floor, and every one of the five has one with a tight margin: unreachable_665 floor 8 (measured 0 when it broke) i64_globals_643 floor 28 normal 28 — exact float_select_return_782 floor 702 normal 702 — exact i64_float_conv_869 floor 96276 normal 96296 trunc_sat_782 floor 192672 A new decline removes executed emulations and drops the count under the floor, so the oracle still goes red — the flag tolerates the declines we know about without tolerating the ones we don't. A blanket sweep of the flag across all 131 `--all-exports` oracles would NOT have that property, which is why this touches five files and not 131. Verified: all five pass with the flag, and the four with reported counts meet their floors (i64_float_conv_869 at 96296 >= 96276). Refs #952 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L --- scripts/repro/float_select_return_782_differential.py | 2 +- scripts/repro/i64_float_conv_869_differential.py | 4 ++-- scripts/repro/i64_globals_643_differential.py | 2 +- scripts/repro/trunc_sat_782_differential.py | 6 +++--- scripts/repro/unreachable_665_differential.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/repro/float_select_return_782_differential.py b/scripts/repro/float_select_return_782_differential.py index 4b7cecc9..8d04f142 100644 --- a/scripts/repro/float_select_return_782_differential.py +++ b/scripts/repro/float_select_return_782_differential.py @@ -74,7 +74,7 @@ def compile_elf(out, target): r = subprocess.run( [SYNTH, "compile", str(WAT), "-o", out, "-b", "arm", - "--target", target, "--all-exports"], + "--target", target, "--all-exports", "--allow-skipped-exports"], capture_output=True, text=True, env={"PATH": "/usr/bin:/bin"}, ) return r.returncode == 0, (r.stderr + r.stdout) diff --git a/scripts/repro/i64_float_conv_869_differential.py b/scripts/repro/i64_float_conv_869_differential.py index 83eb620d..f9aa5311 100644 --- a/scripts/repro/i64_float_conv_869_differential.py +++ b/scripts/repro/i64_float_conv_869_differential.py @@ -384,7 +384,7 @@ def main(): # two-word build) — undefined encodings on FPv4-SP. Absent symbol = the # honest decline; present = the capability gate regressed. compile_or_die("/tmp/i64_float_conv_869_m4f.o", - ["-b", "arm", "--target", "cortex-m4f", "--all-exports"], + ["-b", "arm", "--target", "cortex-m4f", "--all-exports", "--allow-skipped-exports"], "ARM32 cortex-m4f") _, _, m4f_syms = load("/tmp/i64_float_conv_869_m4f.o") for fn in FAMILY: @@ -396,7 +396,7 @@ def main(): # ==== execute the boundary tables (cortex-m7dp self-contained) ========= compile_or_die("/tmp/i64_float_conv_869_arm.elf", - ["-b", "arm", "--target", "cortex-m7dp", "--all-exports"], + ["-b", "arm", "--target", "cortex-m7dp", "--all-exports", "--allow-skipped-exports"], "ARM32 cortex-m7dp") text, base, syms = load("/tmp/i64_float_conv_869_arm.elf") for fn in FAMILY: diff --git a/scripts/repro/i64_globals_643_differential.py b/scripts/repro/i64_globals_643_differential.py index 9f29a2ba..0562741c 100644 --- a/scripts/repro/i64_globals_643_differential.py +++ b/scripts/repro/i64_globals_643_differential.py @@ -64,7 +64,7 @@ def compile_synth(out, backend_args): env = {"PATH": "/usr/bin:/bin"} - cmd = [SYNTH, "compile", str(WAT), "-o", out, "--all-exports"] + backend_args + cmd = [SYNTH, "compile", str(WAT), "-o", out, "--all-exports", "--allow-skipped-exports"] + backend_args r = subprocess.run(cmd, capture_output=True, text=True, env=env) if r.returncode != 0: sys.exit(f"compile failed ({backend_args}): {r.stderr}") diff --git a/scripts/repro/trunc_sat_782_differential.py b/scripts/repro/trunc_sat_782_differential.py index 62411570..71ffb19f 100644 --- a/scripts/repro/trunc_sat_782_differential.py +++ b/scripts/repro/trunc_sat_782_differential.py @@ -432,7 +432,7 @@ def main(): # ==== ARM32 self-contained (cortex-m7dp): execute ALL EIGHT forms ====== compile_or_die("/tmp/trunc_sat_782_arm.elf", - ["-b", "arm", "--target", "cortex-m7dp", "--all-exports"], + ["-b", "arm", "--target", "cortex-m7dp", "--all-exports", "--allow-skipped-exports"], "ARM32 cortex-m7dp") text, base, syms = load("/tmp/trunc_sat_782_arm.elf") for fn, (aty, rty) in SIGS.items(): @@ -456,7 +456,7 @@ def main(): # trunc_sat_f32_* needs only single-precision VFP; the f64-source forms # ride the existing double-FPU honest-reject. compile_or_die("/tmp/trunc_sat_782_m4f.elf", - ["-b", "arm", "--target", "cortex-m4f", "--all-exports"], + ["-b", "arm", "--target", "cortex-m4f", "--all-exports", "--allow-skipped-exports"], "ARM32 cortex-m4f") m4f_text, m4f_base, m4f_syms = load("/tmp/trunc_sat_782_m4f.elf") for fn, (aty, rty) in I32_FNS.items(): @@ -497,7 +497,7 @@ def main(): # ==== aarch64: all eight, unicorn + native ============================== compile_or_die("/tmp/trunc_sat_782_a64.o", - ["-b", "aarch64", "--all-exports"], "aarch64") + ["-b", "aarch64", "--all-exports", "--allow-skipped-exports"], "aarch64") a64_code, a64_base, a64_syms = load("/tmp/trunc_sat_782_a64.o") host_native = platform.machine() in ("arm64", "aarch64") checked_native = 0 diff --git a/scripts/repro/unreachable_665_differential.py b/scripts/repro/unreachable_665_differential.py index fba9fa3d..d20f3a98 100644 --- a/scripts/repro/unreachable_665_differential.py +++ b/scripts/repro/unreachable_665_differential.py @@ -74,7 +74,7 @@ def compile_elf(out, backend_args): r = subprocess.run( - [SYNTH, "compile", str(WAT), "-o", out, *backend_args, "--all-exports"], + [SYNTH, "compile", str(WAT), "-o", out, *backend_args, "--all-exports", "--allow-skipped-exports"], capture_output=True, text=True, env={"PATH": "/usr/bin:/bin"}, ) if r.returncode != 0: