Repository navigation
v0.4.0
Performance
This release is a ground-up pass over the interpreter's hot paths. The cross-language benchmark
(benches/RESULTS.md) improves from 11.8×–4159× slower than CPython to 1.07×–7.1×: sumsq is at
parity (1.07×) and fib is at parity, while pi/poly/dot sit at 2.1×–2.8× and sieve at
7.1× (the remaining gap is the boxed-Value stack-machine cost measured by perf; closing it
fully needs register-style specialization).
-
Bytecode VM promoted to the default execution path (spec §19.5).
vm := trueis now the
default; the AST interpreter remains the authoritative fallback outside the compiled subset. The
dispatch loop was restructured around a cached top frame with the hot instruction set handled
inline (local load/store, constants, jumps, typed arithmetic, fused loop forms), so numeric loops
no longer pay a per-instruction delegation to the evaluator. -
Number/
Valueshrank from 64 to 32/24 bytes.Number::Integer/Number::Rational(and the
fixed-widthI128/U128) now box their payloads, andValueboxesDict/Set/Result
payloads, halving every clone/push/pop in the interpreter and VM (number semantics, rendering,
keys, and equality are unchanged — regression-tested). -
Fused bytecode instructions (spec §14/§12.2):
SetLocalNc(bind without stack round-trip),
AddImmLocal(x += <small literal>in place),AddToSlot(fusedx = x + expr), and
BranchLocalLt/BranchLocalLe(fusedwhile/forloop tests on two local slots). Awhile
loop iteration compiles to 3–4 instructions instead of 10–18. -
Typed arithmetic fast paths in the VM dispatch (spec §6.1/§6.5):
Small/Smalluses checked
i64 arithmetic,F64/F64plain IEEE+ - * / %(division keeps thefractionpolicy and the
exact-layer zero-divisor diagnostics), mixed exact/F64division promotes directly, and array
indexing/index-assignment with integer indices run inline with the authoritative diagnostics. -
Per-call-site callee cache (spec §19.5).
CallNamesites cache their resolved builtin or
program-function callee, validated against a process-wide function-definition epoch (bumped by
everyEnv::set_func), so a user redefinition (e.g. shadowing a core builtin) always
re-resolves while repeated calls skip the environment walk.to_f64with a numeric argument
converts directly on the cached path. -
Inlined small-integer representation for
Number(spec §6.1). The numeric tower's integer
layer gains an internalSmall(i64)variant that is semantically identical toInteger
(Small(5) == Integer(5), same rendering, conversions, and hash keys) but avoids the per-value
heap allocation ofnum_bigint::BigInt. Integer literals,as_i64, and the add/subtract/
multiply hot paths now run allocation-free with checked i64 arithmetic, falling back to the
exact BigInt path on overflow (results stay exact). -
Shared copy-on-write array representation for
Value::Array(spec §11.3).Value::Array
now holds a shared handle (prima_core::ArrayVal, anArc<RwLock<Vec<Value>>>—Arc/RwLock
keepValue: Send + Syncfor theparfor/@parallelrayon paths) instead of an owned
Vec<Value>, so cloning an array value is O(1) instead of a full element copy. Mutation goes
throughArrayVal::with_mut, which mutates the buffer in place when the handle is uniquely
owned and copy-on-writes when shared, preserving value semantics exactly:let b = a; b[0] = 9
(orb.push(x)) still leavesaunchanged, and storing an array into its own buffer (a[0] = a) stores a snapshot copy rather than creating a reference cycle. Equality, hashing keys,
rendering/printoutput, and error messages are unchanged. -
Quadratic lexing on large inputs (spec §3). The lexer's
cur/peek_chardecoded the
current character by runningstr::from_utf8over the entire remaining input — O(n²) overall
(lexing 400 000 tokens took ~70 s, a hang-DoS on untrusted source). Decoding is now a single O(1)
incremental read: 400 000 tokens lex in ~0.18 s (~400× faster at that size). -
Exact
BigIntarithmetic-sum closed form (spec §10/§19.1). Thefor i in 0..n { acc += i }
closed-form optimization computedn(n-1)/2ini64; it now computes the product inBigInt,
matching the real loop's exact accumulation at every magnitude.
Fixed
-
C-ABI panic across the FFI boundary (spec §18.4). Generated
extern "C"wrappers called
call_file_exportunguarded, so any interpreter panic unwound across the C boundary — aborting
the host process on Rust ≥ 1.81.call_file_exportnow wraps its body incatch_unwind: the
panic payload is logged to stderr, the per-thread module cache is dropped, and the call surfaces
as aRuntimeErrorso each wrapper returns its documented default value. -
Parser/evaluator/checker stack exhaustion on deeply nested source (spec §16.4). Tens of
thousands of nested parentheses or a 100 000-term flat expression (0+0+0+…, which the iterative
Pratt loop wraps into a deep chain without recursing) drove recursive consumers into
stack-overflow SIGSEGV;prima checkon untrusted source was the hard exposure. The parser now
enforces an exact per-node AST depth budget (2 000) plus a balanced recursion guard (512) and
reportsE0010-coded syntax errors (appendix C has no dedicated "too deep" code), running on a
dedicated 32 MB thread; the evaluator and static checker gained matchingDrop-safe depth
guards. Allexamples/still parse unchanged. -
JIT parameter-index truncation and slot-offset overflow (spec §19.2).
Op::Paramcarries a
u8index but the compiler cast parameter positions withidx as u8(≥ 257 parameters silently
read the wrong slot), and the parameter-buffer offset was computed asi32::from(8 * i)with
i: u8— a debug panic at ≥ 32 parameters and a silently wrong slot in release. Indices beyond
the instruction set's range are now rejected (interpreter fallback, never a wrong value), the
offset is computed ini32, andvalidate_bytecodeenforces the arity limit. 32- and
33-parameter functions are regression-tested. -
JIT engine initialization panics.
JITBuilder::newanddeclare_functionusedunwrap();
an environment where cranelift cannot initialize now permanently marks the JIT unavailable and
every compilation degrades to the interpreter fallback. -
Silent lost write in the bytecode VM's array index assignment (spec §19.5). Under the VM,
A[i] = vevaluated the store against a stack copy of the array and never wrote back. The
compiler now lowers index assignment toIndexStoreLocal(slot)/IndexStoreName(name)(in-place
mutation through the slot or the environment chain) and mutatingArraymethods on local slots
run throughMethodLocal, mutating the slot's array directly. -
Multi-parameter functions misbound in the bytecode VM (spec §11/§19.5). Function chunks
bound parameters with per-parameterSetLocalinstructions, which pop the last-pushed
argument and push it back — with two or more parameters every parameter received the wrong
argument and the operand stack leaked the arguments. A singleBindParamsinstruction now
distributes the call arguments to the parameter slots in order (single-parameter kernels had
worked by accident; two-parameter VM/AST parity is regression-tested). -
Float→integer saturation in
Number::as_bigint/as_rational(spec §9.2). Converting a
fractional-free float beyond thei64range (e.g.to_bigint(1e19)) silently saturated to
i64::MAX. Both conversions now apply the samei64round-trip guard used byas_i64and
returnNone; collapse callers report proper errors instead of a silently wrong result. -
i64 overflow in loop index stepping and the
parforiteration count (spec §16.1 R0001).
for … step s,range(start, end, step?), andparforstepped/materialized with unchecked
arithmetic (debug panic, or silent wrap → dropped iterations/infinite loop in release). All
paths now usechecked_add/i128counting and reportRuntimeError::Overflow. -
Hash-consing collision unsoundness (spec §8.1).
ExprPool::interndeduplicated by 64-bit
content hash alone, so a hash collision silently replaced one symbolic expression with another
(DefaultHasheris keyed identically in every process, making collisions constructible).
Interning now keeps per-hash candidate buckets and confirmsExprDataequality before reusing
anExprId, preserving the equal-content ⇒ equal-ExprIdinvariant under collisions. -
parfor+ JIT-fallback data race (spec §17.2). Ajit(...)callable whose compilation
failed runs through an interpreted fallback that dereferences the registering thread's
Rc<RefCell<Env>>; inside aparforrayon task that is a cross-threadRcrace (UB). Worker
threads are now marked for the duration of aparfortask and the JIT fallback refuses to run
on them with a clear error; single-threaded fallback behavior is unchanged. -
No interruption path for long-running evaluations (spec §16). The evaluator now checks a
process-wide cancellation flag at loop back-edges and statement boundaries (RuntimeError "interrupted"), and the C ABI exportsprima_cancel/prima_cancel_resetso an embedded host
can stop a runaway export instead of hanging forever. -
Process-level leaks. The JIT registry (spec §19.2) grew without bound for loops like
while … { f = jit(x^2); f(1.0); }— callables are now evicted (oldest-first) once the registry
reaches its capacity, andValue::JitFunctionlookups prune dead entries. The generated C-ABI
wrappers'CSTR_KEEPbuffer (spec §18.4) accumulated oneCStringper string-returning call;
it now uses a double buffer that is recycled across calls. -
Dict/Set key semantics diverged from membership tests (spec §11.6).
d[1]andd[1.0]
used different keys while1.0 in d(numeric comparison) reported the key present, and
0.0/-0.0were distinct keys. Numeric keys are now canonicalized by value (integral floats
and denominator-1 rationals key as the integers they equal;-0.0keys as0.0), matching
membership tests; NaN keys remain rejected. -
Resource-exhaustion guards (OOM aborts → runtime errors).
Integer^Integerexponentiation
(including constant folding) refuses results beyond a bit-length budget,range/parfor
refuse to materialize beyond an element/iteration limit,String.repeatrefuses results beyond
a byte limit, and f-string{:spec}width/precision are clamped — each reports a runtime error
instead of aborting the process on allocation failure. -
vm_paritytest concurrency race.tests/vm_parity.rswrote kernels to per-pid temp files
that parallel test threads could truncate concurrently; each invocation now uses a uniquely
createdNamedTempFile. -
Tail-recursive
fnbodies skip the bytecode VM (spec §10.2/§19.5). The compiled subset has
no constant-stack recursion, so a 100 000-deep tail-recursive function overflowed the stack
undervm := truewhere the AST trampoline handled it; tail-recursive bodies now stay on the
AST path (results identical,tail_call_optimization_avoids_stack_overflowgreen).