Wado

WEP: Spec Examples Quote Fixtures

Context

The specification is normative: it says how the language should behave. Nothing holds it to that. Its 353 wado code blocks are never compiled or run, so an example can be wrong from the day it is written, or turn wrong when the language moves, and no check notices.

Measured when this WEP was written, wado check over each block as written, and again wrapped in a function body:

Outcome Blocks Main causes
Compiles 124
Parse error 101 items and statements mixed at the top level; ... placeholders (~60)
Name or type error 128 fragments naming what they never declare; examples meant to be errors

The e2e fixtures, by contrast, are checked on every run, and AGENTS.md already names them as where the language's behaviour is stated. Two blocks out of 353 appear in any fixture, and only once indented.

How other ecosystems hold a specification

The first makes the examples true. The second ties every rule to a test.

Decision

A code block in the specification is a quotation of an e2e fixture.

The rule

  1. Every ```wado block in docs/spec-*.md names a fixture or a source file.
  2. The named file contains the block verbatim: its lines, in order and contiguous, each shifted by one indentation prefix shared by all of them. A blank line matches a blank line.
  3. Any number of blocks may name one file.
  4. A block whose fixture does not expect a compile error contains at least one assert, unless its reference waives the rule. This rule is provisional: the first migrated file decides whether it holds or is absurd, and it is dropped if absurd.

The reference is an HTML comment holding a JSON object, the block directly before the fence in the same container. The formatter puts a blank line between the two, and GitHub does not render the comment, so the reader sees an ordinary code block:

<!-- {"fixture": "cast_fn_type_newtype.wado"} -->

```wado
type Meters = f64;
let double = (|x| x + x) as fn(Meters) -> Meters;
assert double(1.5 as Meters) == 3.0 as Meters;
```

The comment is data, so JSON says what it may hold, and no syntax of its own needs a rule. A comment whose body does not open with { is no reference, which leaves room for a formatter directive. One that does is a reference: it holds exactly one of fixture and source, and may hold "assert": false. A body that fails to parse, holds another key, or gives assert any other value is an error, so a misspelled key is caught rather than read as no reference.

A fixture path is relative to wado-compiler/tests/fixtures/ and is read the way the suite reads it. A file at the top level is a fixture the suite runs, and the block is matched against the code before its __DATA__. A file below it is a helper module, which the suite compiles only through a fixture that imports it, directly, through another helper, or as a dependencies entry. The checker follows those paths from the top level and requires one to reach the helper. The block is matched against the whole file, __DATA__ included, since a helper's data is content that #data reads, not an expectation. The assert rule does not apply, as the fixture importing it states the claims. A two-file example quotes each file.

A source path is relative to the repository root and names a file outside the fixtures: a standard library module, a unit test's input, a package's source. The block is matched against the whole file, and the assert rule does not apply. A source naming a fixture is an error, since it would step around both rules above.

Why a quotation, and not a generated test

A fixture already carries what an example needs: the declarations around it, the expectation in __DATA__, and a run at every optimization level. Quoting it needs no generator, no wrapping rule for fragments, no hidden setup lines and no second expectation syntax. The check is a file lookup and a substring match.

The link is also checked from both sides. Editing the example without the fixture breaks the match, and so does renaming, deleting or rewriting the fixture. A copy with no link drifts silently in either direction.

The reference is the traceability a rule identifier would give: a rule's example names the test that holds it.

What follows from the rule

The checker

The checker is package-gale-highlight-wado/tools/spec_examples.wado, beside the Wado.g4 grammar it lexes with, so it counts an assert keyword only where the lexer finds one, not in a comment or a string. It reads the Markdown with Marl. scripts/check-spec-examples.sh drives it with wado run, as scripts/check-rust-paths.sh drives package-gale/tools/rust_inline_paths.wado.

A block that breaks any rule, naming nothing among them, fails the check.

A #[TODO] rule was dropped: it asked a WEP to name every fixture marked #[TODO] that a block quotes, as a known gap. A marked fixture is not always a gap. The example of a #[TODO] test is itself marked, by design.

Marl's fenced_code_blocks gives the checker what it reads of each fenced block: its info string, its text, its source line, and the text of the comment block directly before it. Marl's document tree stays internal.

Rollout

During the migration a baseline, scripts/spec-examples.json, counted the blocks of each file that had no reference yet, and CI failed when a count grew. It only shrank, as scripts/rust-inline-paths.json does, and was deleted once empty.

Migrating a block is triage. Each one is exactly one of:

  1. correct, and quoted from an existing or new fixture;
  2. a sketch, and turned into ```text;
  3. meant to be rejected, and quoted from a compile_error fixture;
  4. wrong in the specification, and the specification is fixed;
  5. right, but the compiler disagrees: a #[TODO] fixture and a WEP known gap.

The last two are what this WEP is for. The migration will find them.

Roadmap

  1. [x] Extend Marl with a public read of the fenced code blocks: info string, text, source line, and the HTML block directly preceding each. An HTML comment now ends at -->, not at the next blank line, as CommonMark says.
  2. [x] Write the checker, TDD against small Markdown and fixture cases: the reference, the indented substring match, the assert rule, the #[TODO] rule and the baseline.
  3. [x] Add mise run check-spec-examples and its CI job, and record the baseline of all 353 blocks.
  4. [x] Migrate one file first, spec-types.md, and decide the assert rule from it: keep it, or drop it and say why here. Kept. Its 42 blocks became 55 quotations and one ```text, since a rejected example is now a block of its own; 8 quote a compile_error fixture. In most of the rest the assert replaced a comment that stated a value. A block made only of declarations quotes the test that uses them, which closes the first known gap for that block. The assert was artificial in three: a type-level claim of inference, a field visibility, and a #[cm] name. The migration found one block wrong in the specification: a float vector's comparison mask is the integer vector of the same width, not the float one.
  5. [x] Migrate the other twelve spec files, one change per file. The migration found compiler bugs, fixed on the way: a trait turbofish counting parameters the trait does not write, an associated type's bound pinning a type to Self, the locals of a body other than a function's, and a standard library attribute accepted in a program. It found two gaps, each now a WEP known gap: a CM operation bound by its spelling, and an inline with { path }. A block with no fixture to run could still quote a real file, so a reference may name a source.
  6. [x] Delete the baseline, so the rule holds with no exceptions.

Known gaps