Skip to content

Andreas koestler rust port - #21

Merged
aslakhellesoy merged 24 commits into
mainfrom
AndreasKoestler-rust-port
Jul 18, 2026
Merged

Andreas koestler rust port#21
aslakhellesoy merged 24 commits into
mainfrom
AndreasKoestler-rust-port

Conversation

@aslakhellesoy

Copy link
Copy Markdown
Contributor

Supersedes #17

AndreasKoestler and others added 24 commits July 13, 2026 08:43
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
@aslakhellesoy
aslakhellesoy merged commit 1ecc2b2 into main Jul 18, 2026
6 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants