Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions crates/synth-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,23 @@ enum Commands {
/// grows down from the top of SRAM.
#[arg(long, value_name = "BYTES")]
stack_size: Option<u32>,

/// #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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -756,6 +774,7 @@ fn main() -> Result<()> {
volatile_segments,
stack_layout,
proven_safe,
allow_skipped_exports,
)?;

// If --link requested, invoke the cross-linker
Expand Down Expand Up @@ -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<PathBuf>,
// #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();
Expand Down Expand Up @@ -1683,6 +1705,7 @@ fn compile_command(
volatile_segments,
stack_layout,
proven_safe,
allow_skipped_exports,
);
}

Expand Down Expand Up @@ -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<PathBuf>,
// #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")?;

Expand Down Expand Up @@ -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_<idx>` → position map, so the
// module-level composer can resolve direct calls across the call graph AFTER
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
};
Expand Down Expand Up @@ -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::<Vec<_>>()
.join(", ")
);
Expand All @@ -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());

Expand Down
7 changes: 7 additions & 0 deletions crates/synth-cli/tests/call_indirect_275_selfcontained.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
])
Expand Down
7 changes: 7 additions & 0 deletions crates/synth-cli/tests/i64_call_arg_decline_929.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
])
Expand Down
8 changes: 8 additions & 0 deletions crates/synth-cli/tests/rv32_local_promo_flip_472.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ fn compile(rel: &str, out: &str, promo_on: bool) -> Vec<u8> {
"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");
Expand Down
Loading
Loading