Rust and WASM runtime mapping¶
Native Rust CLI¶
The shared Rust runtime can also be built as a native command-line executable. It uses the same evaluator, protocols, persistent data structures, and provider traits as the browser/WASM build; only the host adapters differ.
$ cargo run --manifest-path rust/Cargo.toml --bin hara -- eval '(+ 19 23)'
42
$ cargo run --manifest-path rust/Cargo.toml --bin hara -- --file program.hal
With no eval or --file argument, the executable reads forms from standard input as a small
REPL. Native filesystem and socket capabilities are opt-in:
$ hara --root ./sandbox --native-sockets
The native binary is intended for CLI and server use. The WASM build remains the browser/runtime-embedding target, while both targets share the Rust core so protocol and evaluator behavior can be tested for parity.
This document maps the tested Truffle Hara L0 contract to a Rust implementation. It is a
portability design, not a second language specification: observable behavior comes from
l0-language.md, runtime-libraries.md, and l0-conformance.edn.
Runtime layers¶
Hara reader/forms
|
v
Rust evaluator + lexical environments
|
+--> persistent values (Hara-owned structural sharing)
+--> explicit mutable markers (runtime handles)
+--> protocol registry
+--> iterator/promise machinery
|
v
provider boundary
| | |
WASM browser native/WASI
host adapters capabilities
The evaluator must not call browser, WASI, or operating-system APIs directly. Host-dependent operations use providers and return the same stable Hara errors as the Truffle runtime.
Value and ownership model¶
Persistent values are Hara-owned implementations, split by data structure under
rust/src/lang/data/. The vector uses a 32-way persistent trie, maps use a native HAMT, sorted
maps use a persistent balanced tree, and the list, queue, ordered collections, sets, trie, tuples,
symbols, and keywords retain their Java protocol shapes. Structural sharing uses Rc; no tracing
garbage collector or third-party immutable-collection crate is required.
Value
├── immutable collections -> Hara vector/HAMT/tree/list modules with Rc sharing
├── functions/environments -> Rc-owned frames
├── array/object markers -> Rc<RefCell<...>> or opaque handles
├── promises -> provider-backed settlement state
└── foreign values -> provider-owned opaque handle
Use Weak references or a handle table for closure/provider cycles. Do not use interior
mutability for ordinary persistent values.
Mapping matrix¶
| Hara surface | Rust target | Acceptance source |
|---|---|---|
| Reader, metadata, literals | Reader, Form, source spans |
reader cases in l0-conformance.edn |
if, do, let, loop, recur, fn |
evaluator nodes and lexical frames | compiler/runtime cases |
| Persistent collections | Hara-owned list, vector trie, HAMT map, and set modules | collection and navigation cases |
| Protocols | context-local ProtocolRegistry |
protocol cases |
| Iterators | closeable HaraIterator trait |
iterator cases |
promise/* |
PromiseProvider plus adoption/recovery rules |
generated-library tests |
| Bytes | owned byte buffer with signed protocol view and unsigned library view | byte cases |
array/object |
explicit mutable marker handles | mutable-boundary tests |
file/* |
FileProvider capability |
capability tests |
socket/* |
callback-based SocketProvider |
loopback capability tests |
| JVM/native interop | provider-specific extension only | native-flavor tests |
x:* names |
excluded from Hara L0; future hara.polyglot adapter |
xtalk equivalence metadata |
Provider interfaces¶
Providers own host handles and capability checks. file/resolve normalizes a child path under a
granted root; it never grants read/write authority. Browser providers may use virtual storage and
WebSocket-style connections, while WASI/native providers may use preopened directories and host
networking. Unsupported hosts return unsupported; denied capabilities return denied.
trait FileProvider {
fn resolve(&self, root: &str, path: &str) -> Result<String, FileError>;
fn read(&self, path: &str) -> Promise<Result<Bytes, FileError>>;
fn write(&self, path: &str, bytes: Bytes) -> Promise<Result<(), FileError>>;
}
trait SocketProvider {
fn connect(&self, host: &str, port: u16, options: Value,
callback: Callback) -> Result<(), SocketError>;
fn send(&self, socket: SocketHandle, bytes: Bytes) -> Result<usize, SocketError>;
fn close(&self, socket: SocketHandle) -> Result<(), SocketError>;
}
Sockets remain callback-based. The Rust runtime must not add socket-promise method families.
Conformance sequence¶
- Port the reader and scalar evaluator against the existing conformance IDs.
- Add persistent values and protocol dispatch, then run collection/protocol IDs.
- Add iterators, markers, bytes, and promises using the focused Java tests as cross-runtime cases.
- Add provider adapters only after portable cases pass; capability-specific cases must remain explicit and must not become portable by accident.
Target profiles¶
The exported runtime reports one of three profiles:
wasm -> browser-safe memory files and callback loopback sockets
native -> scoped std::fs files and callback TCP sockets
wasi -> provider registry reserved for preopened files and host networking
The browser playground installs the memory-file and loopback providers explicitly. Native hosts
install NativeFileProvider and NativeSocketProvider through the same registry. WASI remains a
separate host-selection point: the portable core does not assume ambient filesystem or network
authority.
Reproducible release evidence¶
The main workflow verifies all three runtime profiles:
native cargo test --manifest-path rust/Cargo.toml
browser cargo build --target wasm32-unknown-unknown --release --lib
npm run test:hta && npm run test:browser
wasi cargo build --target wasm32-wasip1 --release --lib
parity 1,024 shared cases through Rust WASM and Truffle
CI uploads rust-wasm-artifact-sizes.txt, containing byte counts for the browser runtime, WASI
runtime, and raw HTA extension. It also uploads the Rust-WASM/Truffle timing matrix. These reports
are generated from the same commit as the conformance run rather than copied into documentation.
Iterator boundary¶
The Rust core exposes a host-neutral closeable iterator value:
(iter collection) -> iterator
(iter-has? iterator) -> boolean
(iter-next iterator) -> value
(iter-close iterator) -> nil
iter-next consumes one value and reports iter-next reached the end of the iterator at the boundary, matching the JVM runtime. iter-map and iter-filter preserve callback-based function invocation while remaining independent of filesystem, sockets, JVM classes, or browser APIs. The WASM build uses the same evaluator and iterator representation.
Resource loading boundary¶
The runtime accepts host-supplied source resources through a registry:
register-resource(name, source text)
|
v
load-resource(name) -> evaluate in the current runtime environment
Resource loading is deliberately source-oriented: the host supplies a named Hara resource, while the Rust evaluator owns parsing, evaluation, and namespace state. A missing resource returns the stable module/not-found error. Browser hosts can populate the registry from fetched assets; native and WASI hosts can connect it to their own module providers without changing the evaluator.
Bytes and mutable storage¶
The Rust core now separates transport bytes from language byte buffers. File and socket providers continue to use owned Vec<u8> transport data; (bytes ...) creates a mutable byte buffer with signed storage and checked -128..255 input.
(bytes 1 2 -3) -> (bytes 1 2 -3)
bytes/get -> unsigned 0..255
bytes/u8 -1 -> 255
bytes/s8 255 -> -1
bytes/set -> same mutable buffer
This keeps byte mutation explicit while preserving byte-for-byte provider transport.
Marker values and dot calls¶
Mutable host-like values are explicit Rust-owned markers:
(array ...) -> Rc<RefCell<Vec<Value>>>
(object ...) -> Rc<RefCell<Vec<(String, Value)>>>
|
+--> (. target (get ...))
+--> (. target (set ...))
+--> (. target (clone ...))
The evaluator rejects dot calls on ordinary persistent vectors/maps and does not inspect host members. Array mutation returns the same marker identity; object keys are limited to strings or keywords.
Promise values¶
Promises are first-class Rust values backed by a single-settlement state machine:
(promise) -> pending promise
|\
+--> promise/resolve -> fulfilled
+--> promise/reject -> rejected
+--> promise/adopt -> copies a settled source
promise/state returns :pending, :fulfilled, or :rejected; promise/value returns the fulfilled value or a stable error. Provider operations can return the same value without introducing separate socket-promise APIs.
Portable protocol calls¶
The evaluator exposes protocol dispatch without requiring a host registry for the core collection protocols:
(protocol-call ICount count value)
(protocol-call INth nth value index)
(protocol-call ILookup lookup value key [default])
Byte INth returns signed stored values while bytes/get remains unsigned. Unknown protocol/method pairs return a stable missing-method error; host extensions continue to use the separate ProtocolRegistry.
Module resource loading¶
Registered resources may contain multiple top-level forms. The evaluator processes them in order, allowing a module to define several functions or values before returning its final form:
register_resource("math", "(defn inc ...) (defn dec ...) (inc 4)")
require_resource("math") -> evaluates once
require_resource("math") -> :loaded
Resource loading remains explicit at the host boundary; source is parsed and evaluated by Rust, and no filesystem lookup is implied.
Namespace isolation¶
Runtime namespaces own independent lexical environments:
user -> {answer: 42}
math -> {answer: 7}
use_namespace("math") -> math bindings
use_namespace("user") -> user bindings
create_namespace allocates an empty namespace, use_namespace switches while preserving the previous environment, and eval_in_namespace evaluates source in the selected namespace. This is the foundation for :require, aliases, and generated runtime libraries without implicit source-file lookup.
Core collection helpers¶
The Rust evaluator includes the host-neutral bootstrap operations used by std/lib/foundation: not, numeric comparisons, mod, first, rest, last, and empty?. They operate over persistent collections, byte buffers, marker arrays, and iterators without invoking host APIs.
Iterator aliases and combinators¶
The Rust core exposes the bootstrap iterator aliases and bounded combinators:
map/filter -> iter-map/iter-filter
take/drop -> iter-take/iter-drop
zip -> pair vectors until either source ends
cycle -> replayable closeable iterator
concat -> sequential iterator over sources
All returned values remain closeable iterator handles. iter-next preserves the stable exhaustion error, and callbacks are evaluated through the same captured-function mechanism as ordinary calls.
Namespace aliases¶
Aliases are explicit runtime metadata, separate from source lookup:
alias_namespace("math", "hara.math")
resolve_namespace("math") -> "hara.math"
require_resource_in_namespace("helpers", "math")
This is the Rust/WASM foundation for Hara :require aliases and generated library namespaces.
String and byte library boundary¶
Portable string/byte operations are implemented in the Rust core:
str, str/count, str/trim, str/upper, str/lower
str/encode <-> str/decode (UTF-8)
bytes/copy, bytes/slice
Copies and slices allocate independent byte-buffer storage. Invalid UTF-8 and out-of-range byte indexes return stable evaluator errors.
Complete marker method boundary¶
Array markers now support push-first, push-last, pop-first, pop-last, insert, remove, clone, and slice in addition to indexed access. Object markers support keys, vals, pairs, and assign alongside lookup, mutation, deletion, and cloning. All methods remain available only through restricted dot calls.
Callback socket API¶
The public Runtime exposes socket handles without changing the callback contract:
socket_connect(host, port) -> handle
socket_send(handle, bytes) -> byte count
socket_close(handle) -> nil
Provider callbacks remain the event boundary; these methods do not create socket-promise variants. Unsupported and invalid providers return stable socket/* errors.
Signed bit operations¶
The Rust core implements the signed 32-bit bit library:
bit-and, bit-or, bit-xor, bit-not
bit-shift-left, bit-shift-right (distance 0..31)
Results wrap as signed 32-bit integers, and invalid shift distances return a stable range error. These names are directly compatible with the corresponding xtalk operation family.
L0 numeric and truth predicates¶
The Rust core now provides the small bootstrap predicates inc, dec, zero?, pos?, neg?, even?, odd?, nil?, true?, and false?. It does not provide some or some?; use any? for predicate reduction and the collection protocols for membership.
Collection membership and navigation¶
The core collection surface includes keys and vals for persistent maps, mutable objects, vectors, and lists. Membership is exposed by IFind/find and IFind/has?: maps and objects test keys, vectors and lists test valid numeric indexes, and sets test values. has? remains true for a present key whose value is nil.
Rust protocol and persistent-vector layer¶
The WASM crate exposes a runtime-neutral lang::protocol module containing
the collection protocol traits (IAssoc, IConj, ICons, ICount, IFind,
ILookup, INth, and related persistence, metadata, display, equality, and
hash traits). IFind::has is derived from find, preserving the distinction
between an absent entry and an entry whose value is nil.
lang::data::Vector is a persistent, structurally shared vector with
association, append/pop, metadata, subviews, and mutable conversion support.
The protocol layer is independently testable and does not depend on the
evaluator or browser host APIs.
Application and pair helpers¶
The bootstrap evaluator supports identity, apply, key, val, and reverse. apply accepts a captured Hara function or built-in arithmetic operator and flattens the final collection argument.
Loop and recur¶
The evaluator supports the common single-binding tail-recursive form:
(loop (state initial)
(if condition
(recur next-state)
result))
recur is an internal evaluator value consumed by its enclosing loop; it cannot escape as a user-visible value, and arity mismatches return stable errors.
Variadic functions¶
Function parameter vectors may use a final & rest pair:
(fn [head & tail] ...)
Fixed parameters retain exact arity; extra arguments are bound as a Hara list under rest. This enables bootstrap helpers such as variadic map, concat, and apply without host-specific calling conventions.
Binding vectors¶
let accepts either the compact pair form or a standard binding vector with multiple sequential name/value pairs:
(let [x 19 y 23] (+ x y))
Bindings are evaluated left-to-right, so later initializers may refer to earlier names. The environment is restored after the body completes.
Multi-state loops¶
loop accepts the same binding-vector shape as let, and recur may return one value per state binding:
(loop [x 0 y 1]
(if (< x 4)
(recur (+ x 1) (+ y x))
y))
The evaluator validates recur arity and restores all outer bindings after completion.
Recur safety¶
recur is an internal control-flow value and cannot escape its enclosing loop, top-level evaluation, or function boundary. Escaped recur attempts return recur must be inside loop.
Namespace declarations in resources¶
Registered module sources may begin with (ns namespace ...). The Rust evaluator validates the namespace symbol and ignores declaration clauses at evaluation time; Runtime namespace selection and aliases remain explicit host-neutral APIs.