Skip to content

WAST conformance is compile-only: 383 assertions in tests/wast are never checked, and the official testsuite is absent (kiln has a gate, synth does not) #928

Description

@avrabe

synth ships 383 spec-style assertions in tests/wast/ and checks none of them. The
only path CI runs against those files asserts that synth compile exits 0 — the
assert_return values are parsed and discarded. Separately, the official
WebAssembly testsuite is absent from this repo, so the compiler has never been
graded against the standard at all.

Sibling comparison is the sharp end: kiln (the interpreter) has a real conformance
gate; synth (the compiler) has none
— and synth is the one whose output runs on
target.

Verified on 97bd6db (v0.55.0).

1. The wired path is compile-only

crates/synth-cli/tests/wast_compile.rs is the only consumer of tests/wast/ that
CI executes. Its own header states the scope:

//! Integration tests: compile every WAST file through synth and verify ELF output.
//! Each WAST file in tests/wast/ is compiled with --all-exports --cortex-m.

and every one of its ~59 cases bottoms out in:

assert!(result.status.success(), "synth compile failed for {} ...")

There is no assert_return / expected-value comparison anywhere in the file
(grep -n 'assert_return\|expected' crates/synth-cli/tests/wast_compile.rs → only an
unrelated relocation-count message).

Meanwhile the data those files carry:

in tests/wast/ (25 files) count
assert_return 381
assert_trap 2
checked by CI 0

So i32_div.wast, i32_rotate.wast, control_factorial.wast etc. are graded on
"did the compiler emit an ELF", not "is the ELF correct". A codegen change that
made i32.rem_s return the wrong value would keep this suite green.
That is the
same shape as #911 (cargo test -- <no match> exits 0) and the v0.54
sret_decide_differential.py gate that printed MISMATCH <-- BUG and exited 0 —
but here it is the primary correctness suite of the backend.

2. The runner that can check values is not wired, and can't run in CI as built

crates/synth-test/ genuinely parses assertions — WastTestRunner::parse_file()
yields WastTestCase { args, expected, expected_trap, test_type }, and
runner.rs:174 really does compare ExecutionResult::Return(actual) against the
expected value. The infrastructure is there. Two things stop it from counting:

  • Zero references in CI. grep -rn "synth-test" .github/ scripts/ → 0 hits
    (only scripts/mythos/rank.md mentions the path). It is in no workflow, no
    bazel target, no gate.
  • Renode-only execution. run_test() opens with
    self.controller.as_mut().context("Renode not connected")? and drives Renode over
    telnet, so it cannot run on a stock CI runner regardless.
  • Its only two #[test]s (test_parse_simple_wast, test_robot_generation) parse a
    3-assertion inline string; neither touches tests/wast/.

Net: the value-checking runner exists, is correct-looking, and has never graded the
381 assertions.

3. The official testsuite is absent

WebAssembly/testsuite is not vendored, submoduled, or fetched anywhere in synth.
The 25 hand-written files are synth's own, so even if they were checked, coverage
would be whatever we happened to write — not the standard.

Suggested fix: adopt kiln's gate, don't build a second one

kiln already solved this in-org and the model is good. Please reuse it rather than
inventing a parallel mechanism:

  • external/testsuite as a pinned submodule (.gitmodules → WebAssembly/testsuite),
    so conformance is reproducible, plus the monthly repin workflow (kiln#360).
  • safety/wast-conformance-baseline.txt — the known-failing list.
  • scripts/wast-conformance-gate.sh + .github/workflows/wast-conformance.yml
    fails the build on any file failing that is not in the baseline, and surfaces
    newly-passing files as a notice so the baseline ratchets tighter.

For synth the only genuinely new part is the execution backend, since grading a
compiler means running its output. synth-test's parser needs no change — it already
produces args + expected. It needs an executor that works on a CI runner:

  • native aarch64 on an arm64 runner is the cheapest honest option — synth's
    aarch64 backend output can be linked and called directly, no emulator;
  • qemu-system-arm for the thumb-2/cortex-m path (already used elsewhere in the repo);
  • keep Renode as the on-target/optional backend it is today.

A frontier note so the gate stays honest: a module synth declines must be reported
as declined, not passed and not failed — otherwise the baseline hides the
frontier. Only accepted-then-wrong should fail.

Why this matters more for synth than for kiln

kiln can be wrong and produce a wrong answer at runtime. synth is wrong in the
artifact that ships
, and its consumers are exactly the targets that can't observe the
error. The compiler having weaker conformance evidence than the interpreter is the
wrong way round.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions