Andreas koestler rust port - #21
Merged
Merged
Conversation
Cargo workspace (edition 2024, forbid(unsafe_code)) with the `var-core` crate: the full pure pipeline (parse -> match -> plan -> execute), diffs, drift/hash, canonical JSON, and the conformance projections — ported 1:1 from the Java var-core, 209 tests green. The var-doc conformance golden gate runs byte-for-byte over all 15 bundles. Not released (publish = false); the facade/runner/adapter crates and the registry/plan/trace golden gates remain future work.
Records the decisions and roadmap for completing the Rust port beyond the already-green var-core + var-doc gate: - ADR 0006: Rust is a full pipeline port (no runtime interop); settles the injected-Registrar + full-replacement author-API forks; records the cucumber-expressions 0.5 and regex-no-lookahead deviations. - ADR 0007: the cargo adapter uses libtest-mimic with thread-local re-derive, the way around var-core being Rc-based (not Send). - Design spec: the var facade (hosting the deferred registry/plan/trace gates), var-config, var-runner, and var-cargotest crates. - Task plan: phased, TDD, conformance-gated; P0 (docs) done here.
Adds the `var` author-facade crate (a thin, curated re-export of the
var-core authoring surface — the injected-Registrar model of ADR 0006) and,
hosted in its test suite, the three conformance golden gates that var-core
deferred: registry.json, plan.json, and trace.json, byte-for-byte over all
15 bundles. Mirrors Java's `var` module ConformanceTest — each bundle's Rust
step fixture (conformance/bundles/<n>/<stem>.steps.rs, siblings of the
existing *.steps.ts, pulled in by #[path]) registers the same expressions and
deterministic handlers as the other ports, dispatched by an explicit match.
With this the Rust port gates all four conformance artifacts (var-doc was
already gated in var-core). Full-replacement state and the closed Value model
carry through the fixtures (e.g. bundle 15 encodes money as a Value::Float
rendered by a format function; bundle 11's {string} sensor takes the trailing
table as a second slot).
The strict, fail-loud `var.config.json` reader (`{ docs: { include, exclude },
steps, snippets, scannerPlugins }`, all optional, default empty). Missing file
→ empty config; malformed JSON / wrong types / unknown keys → error with the
file path. Reproduces the shared config corpus (conformance/config/cases, 8
cases) byte-for-byte through var-core's canonical JSON. The reader itself is
pure (serde_json only); var-core is a test-only dependency.
The imperative shell shared by test-runner adapters: spec discovery with the shared glob→regex semantics (`/**/`, `**/`, `**`, `*`, `?`) + recursive walk; plan_spec/run_example over a caller-supplied Registry and context factory; render_failure (reusing the core diff payloads); and a filesystem FileBaselineStore implementing var-core's BaselineStore for var.lock.json drift reconciliation. No pipeline logic — it delegates to var-core. Steps are compiled in by the caller, so there is no dynamic load_steps.
The cargo-test adapter: one libtest-mimic trial per Markdown example, so `cargo test` reports/filters/lists each like a native #[test]. Because var-core is Rc-based (not Send), each trial captures only owned Send data — the spec path/source plus fn pointers to the step registry and context factory — and re-derives its one example thread-locally. Drift is reconciled on the main thread at enumeration (rewrites var.lock.json on a clean run; a drifted paragraph is a failing trial; VAR_UPDATE=1 accepts). `run()` is the `harness = false` entry point; `run_one()` is unit-tested.
Refactor examples/rust-cargotest off its in-crate mini-runner and onto the shipped crates: `tests/specs.rs` is now a `harness = false` main calling var_cargotest::run, so `cargo test` reports one libtest item per Markdown example (spec.md::name — filterable, listable) instead of one test per spec. Discovery/planning/running/rendering/drift come from var-config/var-runner/ var-core; the sample carries no runner of its own. The context factory is now a plain fn (adapter-thread-boundary friendly), and a `unit` test target keeps the discovery + deliberate-mismatch checks the libtest harness can't express. Commits the var.lock.json drift baseline, like the other samples.
…e pin - Makefile: `make rust` now also builds/tests examples/rust-cargotest. - .github/workflows/rust.yml: CI for the core + the sample (mirrors ruby.yml). - examples/README.md: adds the rust-cargotest row. - release/targets/70-var-examples.sh: Cargo.lock exclude + a (currently inert) crates.io pin block for the sample.
Ports sentences.test.ts (sentence splitting: abbreviations, numeric literals, quoted-string and backtick no-split zones, blank-line and single -newline boundaries, plus an astral UTF-16 offset case) and step-role.test.ts (the neighbour-based role inference truth table). Both modules were previously exercised only transitively; this closes the gap versus the Rust/Java suites. python var-core: 207 -> 222 tests.
Adds the ergonomic `var::Steps` builder (from_registry/stimulus/sensor/param/
param_with_format/into_registry) over var-core's registry, so step files read
as `s.stimulus("expr", …)` / `s.sensor("expr", …)` — the call name IS the kind,
matching every other port and what the LSP/tree-sitter dialect extracts (the
scanner derives kind from the method name). Migrates all 15 conformance
fixtures and the sample's 6 step files off raw add_step onto the builder
(behaviour unchanged — the builder folds through the same pure add_step); the
sample's steps now dogfood the `var` facade.
Adds the Rust authoring dialect so the LSP/editor tooling understands `.steps.rs` files, and threads Rust through every cross-language axis the language-coverage drift gate enforces: - var-language: rust.ts (LanguageSpec — step-def/param queries verified against tree-sitter-rust 0.24.0, raw-string regexp handling), wired into the scanner (LanguageId, SPECS, .rs extension) and both grammar loaders + the VS Code esbuild bundler + knip; a tree-sitter-scanner-rust test. - languages.json: a `rust` entry (ext .rs, hasCli false); SiteLang union += rust. - website: a Rust code tab in all 18 `<Tabs syncKey="lang">` groups. Proven: extraction-conformance (15/15 bundles yield the same (kind, expression)/ (name, regexp) as TypeScript), language-coverage (dialect + loaders/bundler + doc tabs), and the website build all pass.
Mirrors the other registry targets (dependency-ordered cargo publish of var-core → var-config → var → var-runner → var-cargotest), auto-discovered by release.sh. Parked (DISABLED=1) until the port is ready to ship: the crates are publish = false and their names are unclaimed. The header documents the go-live checklist (claim names, flip publish, add `rust` to lint-commits/cliff consumer scopes, un-inert the Cargo pin block in 70-var-examples.sh).
…y sort
Three behavioural fixes from the rust-core review:
- expression: a custom parameter type's parse now receives its regexp's own
capture groups (non-participating → ""), falling back to the whole match for
group-free patterns — Java CaptureGroupTransformer / Python parse(*groups)
parity. Previously always the whole match, silently wrong for a pattern like
(\d+)-(\d+). Located positionally: every generated construct is
non-capturing, so groups between __pN and __p{N+1} belong to parameter N.
- handler: Handler::sync_var / async_var — the any-arity escape hatch (Java's
reflective invocation, Python's *args). A three-slot step (two params +
trailing table/doc string) and async handlers WITH parameters were previously
unconstructible; the fixed sync0-2/async0 conveniences remain.
- canonical_json: object keys re-sorted by UTF-16 code units (what the goldens'
JS sort()/Java TreeMap produce), not BTreeMap's code-point order — they
diverge for keys mixing astral chars with U+E000..U+FFFF. Also documents the
write_number saturation/formatting caveats.
The expression header now also records the full built-in deviation (only
{int}/{word}/{string}; float/double/byte/short/long/biginteger/bigdecimal and
the anonymous {} are omitted, failing loudly) with the ADR 0006 pointer.
…in 1.97 Review follow-ups, no behaviour change: - execute/lib: the Once-guarded process panic hook (the only way to silence stderr for panics catch_unwind deliberately catches — the AssertionError parity channel) is now a DECLARED exception to the no-globals rule, documented at the site and in lib.rs alongside the clippy allows. - scanner: header now records that the plugins parameter is intentionally out of scope, following Scanner.java (a config's scannerPlugins has no core hook yet). - rust-toolchain.toml: pin channel 1.97 (tracks patch releases), like java/.tool-versions pins JDK 21, instead of floating on stable.
rust/ was pinned via rust-toolchain.toml but the example project was not, so `make rust` built it under each checkout's rustup default. edition 2024 needs >= 1.85, so a stale default failed. Pin it to 1.97 to match rust/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
The DeepThought/Library/RomanNumerals editors hard-code one File tab per language and had no .rs entry, so selecting Rust in the header dropdown fell back to another language instead of showing the .rs sources. Add the Rust steps (and logic where the other languages have it) to each editor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
<Editor> now renders its <File> children once, maps each tab's extension to a SiteLang, and throws at build time if any language in languages.json has no code tab. Adding a port and forgetting its example files is now a red build with a message naming the missing language, instead of a silent runtime fallback. Build the website in the PR `test` job (Makefile + CI) so the guard gates merges, not just the main-only deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
Adding a port must also add a <File> tab per language to the interactive editors under website/src/components/examples/*.astro — a distinct surface from the docs <Tabs> and the one most often missed. Record that Editor.astro now fails the build (caught in the PR gate) when a port's examples are absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
…LE const sensor/stimulus were passed a per-file `FILE` constant and a hand-written line number on every call — bookkeeping the TS/Python ports get for free from the imported module. Make both methods #[track_caller] and read the call site via Location::caller(); the captured path's stem (e.g. numerals.steps) is what the registry/conformance artifacts already record, so goldens still match. Step files now read `s.sensor(expr, handler)` with no const and no dummy line. The example adapter keys per-file state by filename suffix instead of the constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
Step files passed `Handler::sync1(|state, a| …)`, naming the arity and wrapping
the closure in a constructor. Add an `IntoHandler<Args>` trait (the axum/bevy
pattern) with impls for 0–2 captures, so `sensor`/`stimulus` take `impl
IntoHandler<A>` and authors write `s.sensor("…", |state, a| …)` — the arity, and
thus each Value parameter, is inferred from the closure. An already-built
Handler (Handler::noop, and the async/variadic escape hatches) passes through
unchanged. var-core's explicit Handler::sync{0,1,2}/async/var constructors are
untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
…thor-API forks Add two facade decisions to the add-a-port skill: capture a step's source file/line from the call site (Rust #[track_caller]) rather than hand-passing it, and infer handler arity from a bare closure (IntoHandler) rather than naming it. Both are choices a new port should make deliberately, so they live beside the existing registration-mechanism and state-evolution forks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
…port Rust .rs tabs fell through to Markdown highlighting — the only language without a CodeMirror mode. Add @codemirror/lang-rust and move the per-language map into cm-languages.ts as a Record<SiteLang, () => Extension> (a missing port is a type error there). tests/cm-languages.test.ts asserts every SiteLang has a working highlighter, so forgetting one for a future port is a red test in the pnpm check gate — the website isn't type-checked in CI, so that test is the enforcement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
Record the new website surface a port must wire (CM_LANGUAGE in cm-languages.ts) and its enforcing test (tests/cm-languages.test.ts), beside the docs-tabs and <Editor>-examples bullets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Er6JR4ChdCVTETUG3ygkzh
Closed
aslakhellesoy
pushed a commit
that referenced
this pull request
Jul 18, 2026
…he commit lint These landed on main via merged PRs (#1, #21, #22) with non-conventional subjects ("Words", "Fix grammatical error in oaths.md"). Pushed history can't be reworded, so add them to EXEMPT alongside the existing entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RVGgxd1Ez2pgJsLzhyjin8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #17