NomiScript Design Notes
Table of Contents
Goals
- Build a Lisp dialect closer to Scheme/Common Lisp:
- high allocation rate (pairs/lists), closures, symbols, dynamic types
- proper tail calls (language-level), macros (eventually), exceptions/conditions (not sure)
- Primary deployment:
- run as a script language embedded into nomisync
- execution target is WebAssembly running under
wasmtime
- FFI requirement (primary):
- pass lists of strings between Rust (host) and Lisp (guest)
- ideally: Rust uses
Vec<String>, Lisp uses(list-of string)or(vector-of string)
- Use `WasmGC` so the engine provides GC-managed heap objects rather than linear-memory GC.
- Try to maintain compatibility with the nomisync script interface.
Non-goals (initially)
- Full Common Lisp MOP
- Native codegen (keep architecture compatible if possible)
- Perfect cross-language object identity across the boundary (use handles)
Key Constraints & Reality Check
- WasmGC is supported, but interop conventions are still evolving:
- Design the FFI boundary carefully to avoid depending on unstable GC-interop details
- Immediate practical implication:
- Keep the exported ABI conservative and stable:
- lists of strings, records of strings/numbers, byte arrays
- avoid exporting raw GC references across the host boundary in v1
- Keep the exported ABI conservative and stable:
- Support types from the
financecrate across the border
High-level Architecture
Components
- Frontend (new crate)
- reader/parser -> AST
- macro expansion (optional in v1; can start with minimal macros)
- compiler to a compact internal IR/bytecode
- Execution Engine in
scriptingcrate- bytecode VM (recommended for v1)
- supports closures, lexical environments, tail calls
- Runtime Object Model
- implemented using WasmGC types (struct/array)
- major types:
- Pair/Cons cell
- String (backed by wasm string or array of bytes depending on chosen string representation)
- Symbol
- Vector
- Closure
- Numbers (immediate tagged or boxed)
- Integers
- Fractions
- Decimals as syntax sugar, converted to fractions
- Host Integration
- provide imports (host functions) for I/O, logging
- provide API for
financeentities.
Wasm Packaging Choice
- Check the Wasmtime’s component model for typed interop
- If component model + WasmGC typed interop is not stable:
- fallback to a minimal core-wasm ABI using explicit lowering into linear memory
- keep the design such that we can switch to component model canonical ABI later without breaking the Lisp part
Object Representation with WasmGC
Primary design
- Use GC-managed references for heap objects:
(ref null $Pair),(ref null $Vector),(ref null $Closure), etc.
- Define GC struct types:
- Pair: fields = car: ValueRef, cdr: ValueRef
- Closure: code-id + env ref
- Symbol: name string + interned id
- Define a unified
Valuerepresentation:- A variant-like tagged union with a small number of ref types plus immediates
- If won't work, box everything
- Immediates:
- fixnum as i32/i64 with tagging if possible
- native fractions support
- booleans/nil as singletons
- Strings:
- Try wasm
stringref - If won't work just represent as GC
array<u8>+ length.
- Try wasm
Invariants
- Guest never exposes internal GC object references directly to the host boundary.
- Guest can safely move/collect objects without affecting host, since host only sees value-level data (strings, lists of strings, DTOs).
Runtime Codegen (Dual-Mode Compilation)
The compiler defaults to runtime WASM codegen with compile-time constant folding as an optimization. Every expression follows:
- Try to evaluate all operands at compile time (
eval_value) - If all succeed -> constant-fold (emit constant WASM values)
- If any operand is runtime -> emit WASM instructions
Type System
Three strict runtime numeric strata, no implicit promotion across them:
- I32: entity indices, booleans, counts, flags. Used by structural
accessors (
entity-count,entity-type,primary-entity-idx,account-count). - Ratio:
(ref $ratio)WasmGC struct with i64 numerator + i64 denominator. CL-style auto-GCD-reduced. Dimensionless scalars — number literals, scaling factors, percentages, conversion rates, the dimensionless quotient of two same-commodity amounts. - Commodity:
(ref $commodity)WasmGC struct with i64 numerator, i64 denominator, and two i64 halves of a Uuid identifying the originating commodity. Dimensioned monetary or asset amounts — produced byaccount-balanceandconvert-commodity; consumed by the commodity-aware arithmetic helpers.
No floats. No silent cross-strata promotion. The compiler refuses
Commodity + Ratio at codegen time; Commodity + Commodity with
mismatched commodity ids traps at runtime via an unreachable inside
$commodity_add. The bridge between strata is the explicit
(convert-commodity <amount> "<target-uuid>") native, which routes
through the Prices table.
Comparisons are type-aware: Ratio uses cross-multiplication, I32 uses
i32.eq~/~i32.lt_s, Commodity uses id-check + ratio comparison (also
traps on mismatched commodity ids).
Arithmetic dispatch table
| Op | Ratio × Ratio | Commodity × Ratio | Ratio × Commodity | Commodity × Commodity (same) | Commodity × Commodity (diff) |
|---|---|---|---|---|---|
+ |
Ratio | refuse | refuse | Commodity | trap |
- |
Ratio | refuse | refuse | Commodity | trap |
* |
Ratio | Commodity | Commodity | refuse (no Commodity²) | refuse |
/ |
Ratio | Commodity | refuse | Ratio (dimensionless rate) | trap |
| comp | bool | refuse | refuse | bool | trap |
WasmGC Types
Emitted into every module:
$ratio— struct with i64 num + i64 denom, plus helper functions ($gcd,$ratio_new,$ratio_add/sub/mul/div,$ratio_eq/lt/gt/le/ge)$commodity— struct with i64 num + i64 denom + i64 uuidhi + i64 uuidlo. Helpers:$commodity_new,$commodity_add/sub/neg,$commodity_mul_by_ratio,$commodity_div_by_ratio,$commodity_div_to_ratio,$commodity_eq/lt. The binary helpers start with an id-equality check (i64.ne on both halves,i32.or,unreachableon mismatch) so the commodity-mismatch trap is structural at the wasm level.$pair— struct withanyrefcar +(ref null $pair)cdr. Canonical list cell; replaces the retired i32-only$cons. Allocated via$pair_new(anyref, ref null $pair) -> ref $pair. The car carries any GC reference (Ratio / Commodity / String) or an i31-boxed i32 viaref.i31. List monomorphism is enforced by the compiler-sidePairElementtag onWasmType::PairRef, so CAR / CDR / DOLIST know which downcast to emit. See ADR-0012.$i8_array— byte array for strings (GC-managed)
Entity API
Context queries, entity accessors, and output functions compile to WASM
memory reads/writes against the binary format. The OutputSerializer
tracks cursor position, entity count, and string offsets with
anodized design-by-contract specs.
should-apply
User-defined trigger function compiled to a separate WASM export. If
defined via (defun should-apply () ...), the body is compiled to WASM;
otherwise defaults to i32.const 1.
Tail Calls & Scheme Semantics
- Strategy:
- Attempt to reuse
wasmtimetail-call support - If doesn't work, implement tail calls in the VM by reusing the current frame (trampoline / loop-based dispatch)
- Attempt to reuse