Skip to content

Add Rust language port - #17

Closed
AndreasKoestler wants to merge 14 commits into
varar-dev:mainfrom
AndreasKoestler:rust-port
Closed

Add Rust language port#17
AndreasKoestler wants to merge 14 commits into
varar-dev:mainfrom
AndreasKoestler:rust-port

Conversation

@AndreasKoestler

Copy link
Copy Markdown
Contributor

Adds Rust as the sixth language port, following the adding-a-language-port checklist.

What's included

  • rust/ Cargo workspace with five crates: var-core (the pure pipeline, ported 1:1 from Java), var (author facade + Steps builder), var-config, var-runner, and var-cargotest (cargo-test adapter — one test item per Markdown example, drift gate included).
  • Conformance: all four artifacts (var-doc, registry, plan, trace) byte-for-byte over all 15 bundles, per-bundle *.steps.rs fixtures, plus the 8-case config corpus.
  • examples/rust-cargotest: the six shared specs running via cargo test, output identical to the Python samples (30 examples).
  • Tree-sitter dialect for .steps.rs + LSP/VS Code wiring, languages.json entry, Rust tab in every docs code-tab group (extraction-conformance and language-coverage gates green).
  • make rust, .github/workflows/rust.yml, ADRs 0006/0007 + design docs.

Deviations (recorded in ADR 0006): community cucumber-expressions 0.5 for the grammar AST with hand-written regex generation; built-ins limited to {int}/{word}/{string} (others fail loudly); full-replacement state via an injected Registrar, like the JVM ports.

Not included: crates.io publication — the crates are publish = false and the release target is parked; enabling it is a follow-up decision.

Also adds two missing Python var-core test files (test_sentences.py, test_step_role.py) found while verifying test parity across ports.

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.
@aslakhellesoy

Copy link
Copy Markdown
Contributor

Thanks this is great! Can you please move the unrelated python changes to a separate pr?

@aslakhellesoy

Copy link
Copy Markdown
Contributor

Superseded by #21

@AndreasKoestler
AndreasKoestler deleted the rust-port branch July 29, 2026 01:17
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