wolf-interp Public
Code
Use Git or checkout with SVN using the web URL.
No matching headings.
Contributing to wolf-interp
Orientation
src/ is the implementation: the frontend, sema-lite, the evaluator with
its region and provenance machines, and the differential tools. tests/ is
the suite that makes the implementation defensible. upstream/ and
vendor/upstream/ are the pinned spec and corpus, consumed as data. docs/
holds the engineering documents and the user manual, indexed by
docs/README.md. The package is wolf-interp; the binary it builds is named
lupin (is12).
The independence doctrine in two sentences: this repository shares no code with the wolf compiler, and the two implementations meet only through the pinned spec, the corpus, and the observation protocol. Disagreement between them is the product.
The gates, all green before any push:
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
cargo run -- corpus
cargo test includes the doc-truth harness (tests/doc_truth.rs). Every
command/output pair in README.md and docs/manual/ runs against the built
binary and is byte-compared, so a doc edit that invents output fails CI.
The independence doctrine
If you are tempted to import it, reimplement it.
wolf-interp shares no code with the wolf compiler. Not the lexer, not the
parser, not the type checker, not the directive parser, not the protocol
types. The two implementations are comparable only through
spec/06-differential-protocol.md, and that comparison is worthless if both
sides run the same code. A divergence is the product. Convenience sharing is
how the product dies.
What that means in practice:
- Never add a dependency on a
wolf_*crate or a git dependency on the wolf-lang repository. CI enforces this againstCargo.lock. - Never read
upstream/crates/orupstream/xtask/. Not for reference, not to check an edge case, not to settle an argument. Readupstream/specand reimplement. If the spec is silent or ambiguous, that ambiguity is the finding:[proto.cmp.triage]makes the spec document the defendant first. - The only permitted coupling is shared data: the pinned
upstream/specandupstream/corpustrees, and the protocol schema they define. - No shared test infrastructure with wolf-lang. The directive parser least of all: its independent reimplementation is the thing that catches a grammar drift.
When your reimplementation disagrees with the compiler, do not open the compiler's source to find out why. Write the disagreement down. That is a sprint deliverable.
The divergence-filing rule (standing)
Any input where this parser and the compiler disagree becomes a wolf-lang spec issue, with both readings attached. The grammar document is amended and both implementations follow the amendment. The two parsers never reconcile by private agreement, and neither one is patched to match the other before the clause is fixed.
Triage order is normative ([proto.cmp.triage]): the spec document is the
defendant first. An ambiguous clause is presumed the root cause until the
clause is shown unambiguous; only then is the implementation that disagrees
with it the defendant. This is the mechanism by which differential testing
hardens the spec. Skip it and a spec bug turns into two implementations
quietly agreeing on something undocumented.
What to attach to the issue:
- the input, reduced;
- both records, verbatim (
conform-run --jsonfrom each side); - the clause each reading claims to follow, by anchor;
- the
classthe comparison assigned (compare::Class).
Four registries keep filing cheap. They live in the code:
parse::CHOICES: every placespec/01underdetermines the parse, and what this implementation chose instead. A divergence that lands on one of these is a spec gap with a candidate amendment already written.parse::CHOICES_RESOLVED: the choices a spec amendment has since closed, each naming the clause that now decides it. Filed gaps get a visible fate; deleting a resolved row would erase the evidence that the pipeline works.diag::UNPINNED_CODES: every diagnostic code this implementation invented. The corpus pins a handful (check: fail(CODE)); the rest are guesses for the s10 catalog to overrule. Disagreement here is expected, and it is the intended input to that catalog.eval::rules::Rule: every dynamic rule, with the clause it enforces. A divergence at therunrung names one of these, and the anchor comes attached (conform-run --trace, andx-trap-clauseon trap records).
The pipeline itself is exercised in tests/divergence.rs against a seeded
counterparty, because a filing rule nobody has run is a filing rule that does
not work.
Verdict classes that are not divergences
src/ledger.rs sorts an observation against a corpus expectation into five
classes. Three of them are neither a match nor a finding:
- dynamic-counterpart: the corpus pins a static code whose dynamic meaning
the spec states, and this machine trapped with exactly that kind.
ledger::dynamic_meaningcarries the six rows[conf.trap.map]states:E1001foruse-after-move,E1002/E1013/E1014forexclusivity,E1004/E1005forregion-fault. Nothing else may be added without a clause to cite. - conservatism: the corpus expects a static rejection this implementation does not perform, and the program ran. Expected by construction (the sema boundary), ledgered, never green-washed.
- out-of-scope: verdict
unsupported, with the reason onx-unsupported.
A mismatch is a finding and is never explained away in code. Do not "fix"
a mismatch by editing the corpus: the corpus is a pinned input, and
[proto.cmp.triage] puts the spec in the dock first.
The sema boundary
The interpreter implements full dynamic semantics and only the static analysis needed to run programs. There is no type checker, no borrow checker, no region checker. Consequences worth stating for anyone adding code:
- A property the static tier owns never becomes a trap. The trap
vocabulary is closed at twelve kinds, and each one is a fault of a defined
execution (
[conf.trap.map]). Wrong arity, a wrong-typed operand, an unknown field: those areunsupported, with a reason naming the code that owns them. Turning one into a trap would put a static rejection in the run rung and corrupt the differential comparison in the worst direction. - The dynamic machine is deliberately more permissive than the compiler's static checkers. The approximation is one-way: the compiler accepting a program obliges this machine not to fault on it, and the converse is never implied.
- A std name with no pinned semantics is declined, not guessed. The
ambient prelude (
eval::builtin) mirrors the compiler's names-only stub. Inventing behavior foracquire()orre"…"would set this implementation's guesses against the compiler's guesses, and the resulting "divergence" would be about nothing.
Commits
Mirroring the compiler track's conventions:
- Commit in chunks. One logical change per commit. A refactor and the feature it enables are two commits. A toolchain bump or a submodule pin bump is always its own commit.
- Terse, imperative subjects, under ~250 characters unless the change
genuinely needs elaboration:
directive: reject unknown trap kinds, notAdded some validation for the trap kinds so that we can.... - Never
git checkoutfiles that were written but not yet committed. Stage or stash first. (This has eaten work before.)
Tests are first class
- Every behavior worth having is worth a test. Every bug fix arrives with the test that would have caught it.
- Unit tests live beside the code in
#[cfg(test)] mod tests. Tests that need the pinned corpus, the built binary, or a snapshot live intests/. - CI counts the same way: a red CI is a stop-the-line event, never a thing to merge around.
The snapshot ritual (insta)
Snapshots live in tests/snapshots/. When a snapshot legitimately changes:
INSTA_UPDATE=always cargo test # rewrite the .snap files
git diff tests/snapshots/ # READ the diff. This is the review
cargo test # verify-clean run against the new snaps
The middle step is the whole point. INSTA_UPDATE=always makes any diff
disappear, including the one that was a real regression, and reading the diff
is what turns that hazard into a workflow. Never commit a .snap.new or
.pending-snap file.
Design snapshot content deliberately. Corpus bytes are canonical output of
wolf fmt (STYLE_VERSION 1), so a snapshot embedding corpus text churns
wholesale when the style version bumps, for reasons having nothing to do with
this repo. Prefer test-owned inputs, as tests/snapshot_exemplar.rs does.
tests/frontend_snapshots.rs splits the difference on purpose. Its fixtures
are test-owned, so they churn only when the lexer changes, and a handful of
corpus-derived snapshots are kept deliberately, because "the tokenizer agrees
with the canonical program" is worth a regression test. When those churn on a
STYLE_VERSION bump, review the diff for span shifts only, then accept.
Spec-derived tests
tests/spec_extract.rs re-reads the pinned spec/01-grammar.md at test time
and diffs it against the transcriptions in lex.rs and parse.rs: the
keyword list and its checksum, the contextual keywords, the §3.2 precedence
table, the counter-examples, the §9 code reservations. Do not "fix" a
failure there by editing the test. A failure means the pinned spec and this
implementation disagree, which is either a pin bump you have not absorbed or a
transcription error. Absorb it or fix the transcription.
Platform lessons (inherited from the compiler track)
These were paid for once already. Do not re-derive them.
- Line endings are protocol surface.
.gitattributespins* text=auto eol=lfand it stays that way: spans are byte-exact, and a Windows CRLF checkout silently shifts every offset in the file. - Never seed an RNG from a raw file path. Normalize separators first. A
Windows
\explored an unvetted seed space compiler-side. More generally: no raw platform path ever reaches a record, a report key, or a hash (seewolf_interp::slash_path). - Sort
read_diroutput before it can influence any generated artifact. Directory order is platform noise; the corpus walk sorts at every level and again at the end. - Pin CI toolchains explicitly.
rust-toolchain.tomlnames the toolchain, and CI installs it with an explicitrustupstep instead of trusting the runner image's default or an implicit proxy install. - The corpus is read-only. wolf-interp never reformats, rewrites, or
"fixes" a file under
upstream/. A corpus file that looks wrong is a finding to report upstream, not a patch to apply locally.
Before you push
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
cargo run -- corpus
All four green, on a clean tree, with the submodule initialized.