Add Rust language port - #17
Closed
AndreasKoestler wants to merge 14 commits into
Closed
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.
Contributor
|
Thanks this is great! Can you please move the unrelated python changes to a separate pr? |
Contributor
|
Superseded by #21 |
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.
Adds Rust as the sixth language port, following the
adding-a-language-portchecklist.What's included
rust/Cargo workspace with five crates:var-core(the pure pipeline, ported 1:1 from Java),var(author facade +Stepsbuilder),var-config,var-runner, andvar-cargotest(cargo-test adapter — one test item per Markdown example, drift gate included).var-doc,registry,plan,trace) byte-for-byte over all 15 bundles, per-bundle*.steps.rsfixtures, plus the 8-case config corpus.examples/rust-cargotest: the six shared specs running viacargo test, output identical to the Python samples (30 examples)..steps.rs+ LSP/VS Code wiring,languages.jsonentry, 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-expressions0.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 = falseand 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.