How-To: Rust crate imports in Vox scripts
This page is the SSOT for the current import rust:… feature: what it does in the toolchain, what it does not do yet, and how to evolve it with high leverage and low Kolmogorov complexity (small mental model, few rules, familiar Cargo concepts).
In the bell-curve interop model, import rust:... is a Tier 3 escape hatch. See Interop tier policy.
Syntax (what you can write today)
Rust crate imports use the reserved prefix rust: on an import entry. They can be comma-separated with ordinary symbol imports in the same import statement.
// vox:skip
import react.use_state
import rust:serde_json
import rust:serde_json(version: "1") as json
import rust:my_thing(path: "../crates/my_thing"), rust:other(git: "https://example.invalid/repo", rev: "main")
| Piece | Meaning |
|---|---|
rust:<crate_name> | Cargo package name / dependency key (same string you would put in Cargo.toml). |
Optional (<meta…>) | Source/version metadata (see below). |
Optional as <alias> | Local binding name. If omitted, the binding defaults to <crate_name>. |
Metadata keys (inside parentheses)
Keys are identifiers; values may be string literals or simple identifiers.
| Key | Role |
|---|---|
version | Semver requirement string (e.g. "1", "^0.4"). |
path | Local path dependency (string). |
git | Git URL (string). |
rev or branch | Git revision / branch hint (string). |
Compatibility rule: Do not specify both path and git for the same import; the compiler rejects that combination.
Same crate twice: You may bind the same crate under two aliases only if the dependency tuple (version, path, git, rev) is identical. Otherwise you get a lowering diagnostic (conflicting specs).
Architecture (end-to-end)
The feature is implemented inside the existing compiler and codegen crates, not as a sidecar tool.
flowchart LR
A["`.vox` source"] --> B["Lexer / Parser"]
B --> C["AST `ImportPathKind::RustCrate`"]
C --> D["HIR `HirRustImport`"]
D --> E["Type registration"]
D --> F["`Cargo.toml` synthesis"]
F --> G["`cargo build` in cache / generated crate"]
- Parse —
rust:is recognized only when the first segment is the identifierrustfollowed by:; seecrates/vox-compiler/src/parser/descent/decl/head.rs(parse_import_path). - AST —
ImportPathcarriesImportPathKind::RustCrate(RustCrateImport)plus optional alias; seecrates/vox-compiler/src/ast/decl/types.rs. - HIR — Lowering fills
HirModule::rust_imports(HirRustImport: crate name, alias, version/path/git/rev, span); symbol-style imports still populateHirModule::imports; seecrates/vox-compiler/src/hir/lower/mod.rs. - Validation —
crates/vox-compiler/src/hir/validate.rschecks empty names, conflicting path+git, etc. - Type checking —
register_hir_modulebinds the alias to an internalTy::Named("RustCrate::<crate>")and reports alias clashes with other top-level names; conflicting metadata for the same crate name emitsDiagnosticCategory::Lowering; seecrates/vox-compiler/src/typeck/registration.rs. - Code generation — Script mode (
generate_script_with_target) and full-server emit (emit_cargo_toml) append extra[dependencies]lines derived fromrust_imports, with deduplication by crate name (first spec wins in the map). Seecrates/vox-compiler/src/codegen_rust/pipeline.rsandcrates/vox-compiler/src/codegen_rust/emit/mod.rs.
CLI and diagnostics
vox checkruns the same frontend (lex → parse → typecheck → HIR validate). With global--json, type/HIR diagnostics are printed as a JSON array (category,severity,message,line,col,file); seecrates/vox-cli/src/pipeline.rsandcrates/vox-cli/src/commands/check.rs.- Golden coverage for a Lowering rust-import diagnostic lives in
crates/vox-cli/tests/golden/check_rust_import_lowering.json.
Relation to Vox PM (vox.lock)
Project dependencies for Vox packages still flow through Vox.toml / vox.lock / vox sync (see reference/cli.md). import rust:… is compile-time Cargo manifest sugar for generated crates: it does not by itself add rows to vox.lock. Longer term, aligning “script deps” with the PM graph is optional hardening (see below).
Current capabilities vs limitations
What works
- Declaring extra Cargo dependencies for generated script binaries and generated full-stack Rust outputs.
- Deterministic merge/dedup of dependency lines per crate name in codegen.
- Strict error when the same crate name is imported with incompatible version/path/git/rev metadata.
- WASI script guardrail: native-only crates listed under
wasi_unsupported_rust_importsincontracts/rust/ecosystem-support.yamlare rejected as rust imports in WASI mode; examples includetokioandaxum.
What does not work yet (important)
- No automatic Rust
useor Vox-call mapping: Addingimport rust:serde_jsonupdates Cargo.toml only. It does not emit Rust that callsserde_jsonfrom lowered Vox code, and does not import items into the Vox type universe fromrustdocorrustc. - The alias is not a typed API surface: Bindings use the internal marker type
RustCrate::<crate>. Field access on that binding is rejected in the typechecker with a clear error (seecrates/vox-compiler/src/typeck/checker/expr_field.rs). - Default version
*: If you omitversion/path/git, codegen emits a loose crates.io requirement (crate = "*"), which is convenient for experiments but weak for reproducibility. - No linkage to
cargo vendor/ vendoring policy in this path alone; reproducibility remains “whatever Cargo resolves” unless you tighten versions or use path/git explicitly.
Plain language: today’s feature is best thought of as “make this script’s generated crate depend on these Rust packages.” It is not yet “call arbitrary Rust APIs from Vox with one line.”
Support-class annotations and reproducibility warnings
Rust imports now carry a support-class classification for clearer operator expectations:
first_classinternal_runtime_onlyescape_hatch_onlydeferred
Current compiler behavior:
- emits warnings when a crate is classified as
internal_runtime_onlyordeferred - emits warnings when a crate is classified as
escape_hatch_only - emits warnings when a crate has
plannedsemantics in the support registry - emits warnings when no
version/path/gitpin is provided (Cargo fallback*) - emits warnings when import-level pins are provided for full app template-managed crates (those templates may own versions/paths)
- annotates generated
Cargo.tomldependency lines with# vox_rust_import support_class=...
These annotations are guidance, not a typed interop promise.
Canonical support matrix and contract metadata:
For common app capabilities, prefer:
- builtins and
std.*surfaces, - approved wrappers,
- package-managed Vox libraries,
import rust:...only when the earlier tiers do not fit.
Reducing K-complexity and boilerplate (without breaking compatibility)
Keep the mental model small:
- One syntax only — Keep
import rust:…as the single user-facing form; avoid parallel@rust.importor magic decorators unless they lower to the same AST (doc and tooling stay simpler). - Cargo is the execution truth — Users already understand
version/path/git. Prefer mapping from those fields toCargo.tomlover inventing a third version language. - Layer capabilities — Dependency declaration (done) → optional manifest merge from project lock (next) → optional thin escape hatch or shims (later).
High-impact, not over-engineered wins
These are ordered by value / effort:
-
Implicit versions from project context (medium)
IfVox.tomlor a siblingCargo.toml/ lockfile already pinsserde_json, allowimport rust:serde_jsonwithout repeatingversion: "…", by resolving from the project graph when building from a workspace package. Compatibility: When no pin exists, keep today’s behavior (*or diagnostic). K win: One-line imports match user expectation of “like Cargo.” -
vox check/cargo checkparity messaging (low)
When script codegen fails, surface Cargo’s error with a hint { “dependency X declared viaimport rust:Xat line L.” Ties the mental model to the line they wrote. -
Curated
vox-*or shims for 5–10 hot crates (medium)
Instead of fullrustdoctyping, exposestd-style namespaces for e.g. JSON, time, UUID (wrappers invox-runtimeor a smallvox-shimscrate). K win: Users learn one Vox API; compiler stays small. Big win: Works today under the existing builtin pattern. -
Single escape hatch: embedded Rust snippet with explicit unsafe boundary (medium–high)
A block or decl that copies almost verbatim into generatedmain/ module, with scopedusegenerated from adjacentimport rust:…. Compatibility: Opt-in, clearly marked; keeps the main language pure. K win: Power users stop fighting the compiler; everyone else ignores it. -
Defer: full dynamic
rustdoc/ rustc-based typing
High cost, long-term maintenance, and versioning traps. Prefer shims + escape hatch until the language stabilizes.
Wins to defer (usually over-engineered for the current stage)
- Full ABI-stable plugin system for every crate.
- Automatic WASM component bindings for arbitrary crates.
- Replacing Cargo with a custom resolver for script deps.
Those belong behind explicit feature gates and product milestones, not on the default path.
Related docs
- Keyword:
importsyntax - CLI reference: PM vs generated
Cargo.lock - Diagnostic taxonomy
- Vox packaging blueprint (extension boundaries)
Maintenance: When you change parser, HIR, registration, or codegen behavior for rust imports, update this page and the golden JSON under crates/vox-cli/tests/golden/ if diagnostics or spans shift.
After contract/policy edits, run cargo run -p vox-cli --quiet -- ci rust-ecosystem-policy.