Anticipated questions about SubsetJuliaVM (sjulia), with answers grounded in the actual implementation. Also: a short Q&A on the Jacobian-conjecture demo (Symbolics.jl).
Q. What is SubsetJuliaVM (sjulia)?
A. A clean-room implementation of a subset of the Julia language, written in Rust — built to run Julia code where the official runtime cannot go.
One pipeline, no JIT: a pure-Rust parser, lowering, and bytecode compiler feed a stack-based bytecode VM (source → CST → Core IR → bytecode). All code generation stays in data (bytecode), so it runs on iOS (W^X), Android, WASM, and desktop.
Ships with a reimplemented Base subset (~46k lines of pure Julia + Rust builtins) and ~34 bundled packages (Plots, OrdinaryDiffEq surface, AbstractAlgebra, …).
Same core, many faces: the sjulia CLI, an iOS/iPadOS App Store app, an Android (Flutter) app, and a browser playground — plus juliars, an experimental Julia→Rust AoT transpiler.
It is a subset: behavior is checked against official Julia 1.12 fixture-by-fixture, but it is not, and does not claim to be, a full Julia implementation.
Q. What is a “stack-based VM”?
A. An interpreter that executes bytecode using an operand stack instead of registers.
Expressions like 1 + 2 * 3 compile (before optimization) to instructions such as PushI64(1), PushI64(2), PushI64(3), MulI64, AddI64 — each operation pops its operands from the stack and pushes the result.
The VM core is a plain Rust loop: fetch code[ip], match on the opcode, execute, advance ip (plus fast paths: pre-decoded straight-line blocks and specialized call sites bypass the generic loop).
The instruction set (enum Instr) has 400+ opcodes: typed arithmetic (AddI64, MulF64), dynamic fallbacks (DynamicAdd), control flow (Jump, JumpIfZero), calls, array ops, etc.
Function calls push a Frame (locals, captured variables, entry stack height) onto a call-frame stack.
No machine code is ever generated at runtime — that is why it satisfies iOS’s W^X policy.
Q. How does println(1 + 2 * 3) actually run? (1/2)
A. Everything up to bytecode happens before execution:
1. Parse — the pure-Rust parser builds a CST; precedence makes * bind tighter than +:
3. Compile — for this literal expression, constant propagation already finishes the math at compile time. The actual emitted bytecode (sjulia --dump-bytecode):
PushI64(7) PrintI64NoNewline PrintNewline
The VM never multiplies — and println of an inferred Int64 compiles to specialized print opcodes, not even a function call.
Q. How does println(1 + 2 * 3) actually run? (2/2)
A. To watch the stack machine work, hide the constants behind an argument. f(x::Int64) = println(1 + x * 3) compiles to (verified with --dump-bytecode):
step
instruction
operand stack after (x = 2)
1
PushI64(1)
[1]
2
LoadSlotI64(0)
[1, 2] — load x from its typed slot
3
PushI64(3)
[1, 2, 3]
4
MulI64
[1, 6] — pops 2 and 3, pushes 6
5
AddI64
[7] — pops 1 and 6, pushes 7
6
PrintI64NoNewline
[] — pops 7, prints 7
7
PrintNewline
[]
The VM loop is: fetch code[ip] → match on the opcode → push/pop the stack (optimized straight-line blocks can bypass this generic loop).
MulI64 is plain Rust: pop two i64s, multiply, push — no type check, no boxing.
No machine code was generated at any point: bytecode is data, interpreted by the same VM on iOS, Android, and WASM.
Q. Stack VM vs. register VM vs. tree-walking interpreter?
A. Three classic interpreter designs:
Design
How it works
Trade-off
Tree-walking
Recursively evaluate the AST
Simplest, slowest
Stack VM (sjulia today)
Linear bytecode + operand stack
Compact bytecode, simple compiler; CPython/older JVMs work this way
Register VM
Bytecode operates on virtual registers
Fewer instructions dispatched; Lua-style
Q. Which design does sjulia use?
A. A stack VM today; a register VM is being evaluated.
sjulia compiles to bytecode first, so the main execution path is not a tree-walker (a small tree-walking evaluator exists only for runtime eval of Expr values).
A register VM prototype exists behind a flag; the stack VM stays the default until side-by-side parity and performance are proven.
Either way it is ordinary ahead-of-time-compiled Rust — no executable memory needed.
Q. What happens between Julia source and execution?
A. Data are nouns, stages are arrows:
Source ─[parse]→ CST ─[lower]→ Core IR ─[compile]→ Bytecode ─[VM interprets]→ result
Parser: pure Rust, producing a tree-sitter-compatible CST, validated against official Julia’s parser. Pure Rust means the same parser runs natively, on iOS, and on wasm32-unknown-unknown.
Lowering: CST → Core IR; expands macros and desugars syntax (some closure-capture analysis finishes later, in the compiler).
Compiler: Core IR → bytecode, with abstract-interpretation type inference, constant propagation, SSA-based optimization, and peephole fusion (e.g. a counted-loop back-edge becomes one fused “add-const-and-jump” superinstruction).
VM: executes the bytecode; the compiled Base library is cached and embedded, so startup skips recompiling the prelude.
Q. What does the software architecture look like?
A. A layered Cargo workspace: one core pipeline, thin per-platform bindings.
flowchart LR
SRC["Julia<br/>source"] --> PARSER
subgraph CORE["Core pipeline crates"]
direction LR
PARSER["parser<br/>→ CST"] --> LOWER["lowering<br/>→ Core IR"] --> COMPILE["compile<br/>inference · opt"] --> BC["bytecode"] --> VM["vm<br/>(stack VM)"]
end
style CORE fill:#2b2b2b,stroke:#777,color:#ddd
parser / lowering / compile / bytecode / vm — each stage is a separate crate with its own tests and a well-defined output (CST / Core IR / bytecode).
Q. Architecture (cont.): how does it reach each platform?
A. Thin per-platform bindings around the same core:
flowchart LR
FE["front end<br/>parser → lowering → Core IR"] --> VM["bytecode compiler<br/>+ stack VM"]
FE --> AOT["aot: juliars<br/>Core IR → Rust source + runtime crate"]
VM --> FFI["ffi<br/>hand-written C ABI"]
VM --> WEB["web<br/>wasm-bindgen"]
FFI --> MOB["iOS (Swift)<br/>Android (Flutter)"]
WEB --> JS["Browser<br/>playground (JS)"]
The bundled pure-Julia Base (~46k lines) and ~34 packages are compiled by the same pipeline.
The C ABI carries a version constant checked from Swift and Dart — mismatches fail fast. (The Web build ships JS glue and WASM as one bundle, so no runtime ABI check is needed.)
Q. What is Core IR?
A. The tree-shaped intermediate representation at the center of the pipeline — “Julia after desugaring.”
CST ─[lowering]→ Core IR ─[bytecode compiler]→ bytecode → stack VM
└────[aot: juliars]─────→ Rust source
Produced by lowering: macros are expanded and syntax desugared (quoted code is kept as data for metaprogramming; some closure-capture analysis finishes later, in the compiler).
What remains is a plain tree: Assign, For/ForEach, While, Literal, Var, BinaryOp, Call, ArrayLiteral, … E.g. println(1 + x * 3) becomes Call("println", [BinaryOp(+, 1, BinaryOp(*, Var(x), 3))]).
Loosely analogous to official Julia’s lowered form (@code_lowered) — but still a tree, not SSA; SSA appears later, inside the compiler’s optimizer.
Expression/statement nodes carry a Span for precise error locations; hot identifier positions are interned; Call carries kwargs and splat masks, so Julia’s calling convention survives lowering.
It is serializable: sjulia --compile f.jl -o f.sjir saves it, --run-ir executes it, and juliars accepts it directly — the shared input of both back ends.
Q. What does “static pipeline” mean here?
A. Code generation happens up front — and anything generated later is still only bytecode.
Parse → lower → macro-expand → infer types → optimize → emit bytecode: done before execution; at run time the VM mostly just interprets data.
Official Julia interleaves these with execution — methods are LLVM-compiled at run time, on first call per type signature. That is the part iOS forbids.
In sjulia the runtime never creates new executable code; even lazy specialization (CallSpecialize) only emits more bytecode — data, not machine code.
Q. What does the static pipeline buy you?
A. Portability and predictability:
The Base library is compiled once at build time and embedded as a cache — apps start without recompiling the prelude.
The same bytecode format and interpreter run on macOS, iOS, Android, and WASM.
Predictable startup and memory — no JIT compilation pauses (first-use bytecode specialization still warms some caches).
And it removes the App Store conflict at the design level: there is no runtime machine-code generation left to forbid.
Q. How do you implement multiple dispatch without a JIT?
A. Method tables + two-level dispatch:
Each function has a MethodTable of signatures (Tuple{argtypes...}, incl. where params).
Compile time: when inference knows the argument types, the call is resolved statically to a direct call — or even a type-specialized one (CallSpecialize compiles a per-type-signature specialized bytecode version of the callee on first use and caches it).
Runtime: Any-typed call sites use dynamic dispatch opcodes with a most-specific-match search, accelerated by dispatch caches (including negative caching) and a first-argument-type index (for primitive first arguments) — the interpreter analogue of inline caches.
Q. Is there a garbage collector?
A. A hybrid scheme, tuned for a single-threaded, no-JIT VM:
Scalars live inline in the Value enum; strings and other shared immutable data use Rust reference counting (Rc — cloning is a refcount bump, not a copy).
Mutable structs live on a VM-managed heap (Vec<StructInstance>), referenced by index.
A stop-the-world mark & compact pass runs only at safe points (e.g. after a top-level evaluation): mark reachable objects from all VM roots (stack, frames, globals, tasks, …), rebuild the heap densely, rewrite indices.
This avoids per-assignment refcount traffic in hot loops, and cross-eval compaction keeps long REPL sessions bounded.
Q. Why is the VM ~5× slower than Julia?
A. That is the price of “no JIT”:
Every operation pays bytecode-dispatch overhead — fetch an instruction, branch on its opcode, execute, repeat.
No native code is specialized for your types at runtime, which is exactly where official Julia’s LLVM JIT gets its speed.
When top speed matters, juliars (AoT → Rust) removes interpretation entirely and matches official Julia.
(The ~5× Julia / ~1.7× Python ratios are from the coprime-π benchmark in the main slides; the gap varies by workload.)
Q. Then why is the VM still faster than Python?
A. sjulia claws a lot back before execution, at compile time:
type inference → typed opcodes and typed local slots (AddI64, LoadSlotF64) instead of boxed generic ops
method dispatch caches and lazy per-signature specialization
straight-line typed block execution that bypasses the generic dispatch loop
Q. Does sjulia have a TTFX / TTFL / TTFP problem?
A. The phenomena all exist — but for these measured workloads they are far lighter than in official Julia, because the first-use work is bytecode compilation, not the second-scale native-compilation latency associated with Julia TTFP workloads.
Official Julia’s TTFX/TTFP can include type inference + LLVM native codegen for method specializations not covered by caches. Julia 1.9+ pkgimages cache native code as well as precompilation results; compilation still occurs for specializations those caches miss.
sjulia has none of that: no LLVM, no native codegen. The Base library is compiled once at build time and embedded as a cache, so startup skips the prelude — measured on this machine: sjulia -e 'println(1)' finishes in ~0.1 s total.
What remains is what we call TTFL (“time to first load”): bundled packages ship as Julia source and are bytecode-compiled on first using. Measured (total process time, baseline 0.05 s): using Plots 0.29 s, OrdinaryDiffEq 0.16 s, AbstractAlgebra 0.59 s, Distributions 0.72 s. Lazy per-signature bytecode specialization adds a minor first-call warmup on top.
End-to-end TTFP is dominated by TTFL: after using Plots, the incremental first plot(...) call measures only ~3 ms (subsequent calls ~1 ms) — plotting just builds a Plot value and emits Plotly JSON, with no native-code generation.
The web playground hides even that: it pre-warms using Plots; plot(sin) once during startup, before the Run button is enabled.
Q. Is there an equivalent of Julia’s pkgimages?
A. Two cache layers exist — complete for Base, shallower than pkgimages for packages:
Base cache — the prelude is compiled at build time and the resulting bytecode is embedded in the binary (fingerprint-invalidated). This is why bare startup is ~0.1 s.
Per-package loader cache — each bundled package’s lowered Module is cached on disk, keyed by source hash + metadata-schema fingerprint + OS/arch; the files are even named in homage to Julia’s .ji (Distributions.<hash>.ji.json, under a temp cache dir; disabled on iOS/WASM).
The difference from pkgimages: this caches only up to lowered IR — bytecode compilation still runs on every using. Measured accordingly: using Distributions cold 0.72 s vs. warm 0.71 s — parse+lower savings only.
So: Base gets the full pkgimage-style treatment (compiled artifact, embedded); packages currently get a partial one. Caching compiled package bytecode is the natural next step down the same road.
Q. Isn’t Python also a stack-based VM?
A. Yes — CPython compiles to bytecode and interprets it on an operand stack, same family as sjulia.
CPython: .py → .pyc bytecode → the eval loop interprets it. Same architecture “family”.
So the difference is not stack VM vs. something else — it is how much work is done before execution starts.
CPython resolves almost everything at run time; sjulia front-loads it at compile time (type inference, specialization, fusion).
That gap is what the benchmark shows: sjulia VM 9.99 s vs. Python 16.76 s on coprime π (\(N=10000\)).
Q. sjulia vs. CPython (1): bytecode
A. Typed instructions vs. generic instructions.
CPython emits generic opcodes like BINARY_OP; every execution inspects the operand types to decide what “+” means (3.11+ adaptive specialization rewrites hot sites — but still at run time).
sjulia runs abstract-interpretation type inference before execution and emits typed opcodes directly: AddI64, MulF64, LoadSlotF64 — no type checks, no boxing in hot loops.
Peephole fusion then merges sequences into superinstructions (compare-and-jump, add-const-and-jump loop back-edges).
Q. What does “boxed” / “boxing” mean?
A. Wrapping a raw machine value in a heap object so it can be handled generically.
A raw (unboxed) Int64 is just 8 bytes — it fits in a CPU register or on the stack.
A boxed value is heap-allocated with a header (type tag, refcount/GC info), and the program passes a pointer to it.
Boxing is what makes “any variable can hold any type” easy — but it costs: heap allocation per value, pointer chasing (cache misses) per use, and GC/refcount pressure per assignment.
In CPython, adding two ints means: follow two pointers, read type tags, then produce a result object — allocated on the heap unless it falls in the small-int cache (−5…256).
Official Julia avoids this via JIT-compiled type-specialized code (boxing only appears at type-unstable / Any boundaries); sjulia does it by inferring types ahead of execution and keeping scalars inline in its Value enum (I64(i64), F64(f64)) and in typed local slots.
Q. sjulia vs. CPython (2): values & memory
A. Unboxed enum variants vs. everything-is-a-PyObject.
CPython: even an integer is a heap-allocated, reference-counted PyObject; 1 + 2 involves pointer dereferences and refcount traffic.
sjulia: Value is a Rust enum — I64(i64), F64(f64) are stored inline, unboxed; type-stable locals live in typed slots.
GC: CPython counts references on every assignment (+ a cycle detector); sjulia uses Rc for immutable values and a mark & compact pass that runs only at safe points — hot loops pay no per-assignment bookkeeping.
Q. sjulia vs. CPython (3): dispatch semantics
A. Multiple dispatch vs. single dispatch.
Python picks a method from the receiver’s type alone (obj.method(...)).
sjulia implements Julia’s multiple dispatch — most-specific match over all argument types — via method tables, dispatch caches (with negative caching), and lazy per-signature specialization (CallSpecialize).
Summary: same VM family, different philosophy — CPython resolves at run time; sjulia resolves as much as possible before running, which is how it recovers speed without a JIT.
Q. Does using Plots download the real Plots.jl?
A. No — nothing is ever downloaded (also an App Store requirement).
All packages are pure-Julia-subset reimplementations bundled into the binary (~34 packages: Plots, OrdinaryDiffEq/SciMLBase, AbstractAlgebra, Distributions, StaticArrays, Symbolics, Optim, QuadGK, …), embedded via include_str! and loaded from virtual paths, since iOS/WASM have no package filesystem.
They cover a practical subset of each package’s API, not the full package.
Base itself is split: performance-critical primitives in Rust builtins, the rest (~46k lines) as pure-Julia base/ + 10 stdlib modules — mirroring official Julia’s layout.
Q. How does plotting actually render?
A. Plots.jl output renders as Plotly JSON on every host.
The bundled Plots subset builds a Plot value; the VM serializes it to an artifact with MIME application/vnd.plotly+json.
The web playground, the slides, and the iOS app all feed that JSON to a bundled plotly.min.js viewer — same interactive plots on every platform, including offline.
Supported: line/scatter/bar/histogram/heatmap/contour/surface/3D paths, plus @animate/@gif (Plotly frames). A second artifact type, JSXGraph JSON, serves interactive geometry.
Q. How close is it to real Julia? How do you verify that?
A. Parity with official Julia 1.12 is enforced mechanically:
~3,250 registered test fixtures across 114 categories, checked against expected outputs (floats within 1e-10); a separate parity sweep runs eligible fixtures under both real julia and sjulia and compares.
Plus differential fuzzing, official-test-suite sweeps, and subtype/dispatch parity tests.
Known gaps: no real threads (single-threaded by design), partial LinearAlgebra/views, some macros are no-ops (@inbounds, @inline), and Dict-heavy hot loops are unoptimized.
Q. Why test compatibility this way — isn’t there a Julia spec?
A. No. Julia has no complete, normative, implementation-independent language specification — so “fully Julia-compatible” can only be defined operationally, not as conformance to a standard.
Python has a mature Language Reference and accepted PEPs, giving alternative implementations (PyPy, MicroPython, …) a much clearer compatibility target — though even there, CPython remains the de-facto reference for the gaps.
Julia has nothing comparable: the docs describe behavior informally, and for corner cases (scope rules, promotion, printing, dispatch ambiguities) the official implementation is the ultimate de-facto reference.
Consequence: with no conformance suite to pass, sjulia defines compatibility operationally — thousands of small differential tests: run a snippet under official julia and sjulia, demand identical output, and pin a concrete version (Julia 1.12) as the target.
Differential fuzzing and official-test-suite sweeps extend the same idea: for undocumented behavior, running the reference implementation is the strongest practical oracle.
Q. Which Julia syntax is supported? (1/2)
A. The everyday core of the language (every item on these two slides was verified by running it on the sjulia binary):
Control flow — if/elseif/else, for (ranges, in over iterables, tuple destructuring), while, break/continue, try/catch/finally, &&/|| short-circuits, ternary ?:, let (even official Julia’s soft-scope rules for top-level loops are reproduced)
Types & dispatch — struct / mutable struct, inner constructors, parametric types (Point{T}, where clauses, Type{T} dispatch), abstract types, Union types, multiple dispatch with most-specific selection
Arrays & broadcast — 1D/2D arrays, typed arrays, comprehensions & generators (with filters), dot broadcasting (x .+ y, f.(xs)), views/SubArray (a representative subset), the full iterate protocol
Collections & strings — Dict, Set, tuples, named tuples, ranges; string operations and PCRE-style regex
Macros & metaprogramming — user-defined macro with hygiene and esc(), quote/$ interpolation, Expr/QuoteNode, eval, macroexpand; built-ins like @test/@testset, @time, @show, @assert, @enum, @kwdef
Tasks & Channels — cooperative, via VM-level continuations
Bundled stdlib subset: Statistics, Random, Dates, Test, Printf, Iterators, LinearAlgebra (partial), and more
Gaps are tracked openly in docs/vm/UNIMPLEMENTED.md — see also the next slide
Q. What does SubsetJuliaVM not provide?
A. Everything that would break the sandboxed, offline, no-downloaded-code model:
Pkg.jl / package management — no Pkg.add, no registry, no downloads. using X only reaches the ~34 packages bundled into the binary (App Store apps may not download code, and the design is offline-first).
Network I/O — no Sockets, no Downloads/HTTP, no download.
Filesystem access — path/file operations (pwd, mkdir, rm, stat, …) are unimplemented; iOS/WASM have no free filesystem, so include resolves through virtual embedded paths instead.
External processes — no run(cmd), environment control, or signals.
C interface — no @ccall/@cfunction/pointer/unsafe_*: arbitrary native code would defeat the whole sandbox/no-JIT story.
Real parallelism — single-threaded by design; Threads API exists as stubs, Tasks/Channels are cooperative continuations, no Distributed.
Native-code reflection — no @code_llvm/@code_native (there is no LLVM); use --dump-bytecode instead.
Q. How does the Rust VM get into an iOS app / the browser?
A. One core, three thin bindings — iOS and Android share a versioned hand-written C ABI; Web has its own wasm-bindgen API:
Android: same FFI crate built with cargo-ndk for each ABI, loaded from a Flutter app through dart:ffi.
Web: a separate wasm-bindgen crate built with wasm-pack (size-optimized profile: opt-level="s", LTO); JS calls run_from_source(code, seed).
The C ABI version constant is checked on the Swift and Dart sides — mismatched binaries fail fast instead of corrupting memory. (Web bundles JS glue and WASM together, so no runtime check is needed.)
Q. How does juliars (AoT) work, and why no libjulia?
A. It transpiles the Core IR to Rust source, then rustc compiles it:
The generated code depends at most on a small Rust runtime crate (dynamic Value, dispatch helpers) — never on Julia’s C runtime, because the whole toolchain is pure Rust.
Fully static programs (--pure-rust) compile to standalone binaries; scalar entry points can be exported as a C ABI.
“Experimental” because the guaranteed subset is deliberately narrow (scalar numerics, loops, 1D arrays, structs; more constructs are partially implemented); the bundled package ecosystem remains the VM’s job.
On the Mandelbrot benchmark it matches official Julia’s speed.
Q. What are “writable + executable pages”?
A. Memory pages that can be both written to and executed — the thing a JIT needs.
The OS manages memory in pages (e.g. 16 KB on iOS), each with permissions: Read / Write / eXecute.
A JIT compiler works by writing machine code into memory (needs W) and then jumping into it (needs X) — so it needs a page that is, at some point, both.
W^X (“write XOR execute”): a page may be writable or executable, never both at once. This blocks a classic attack — inject bytes into writable memory, then run them.
Q. How does iOS enforce W^X, and why is sjulia fine?
A. Runtime code generation is reserved for Apple; interpreting data is not.
Per Apple’s Platform Security guide (cited in the main slides), requesting W+X memory (mmap with MAP_JIT) requires an entitlement that ordinary distributed apps cannot obtain — Apple reserves it for its own apps like Safari’s JS engine (exceptions: approved alternative browser engines, debugger-attached dev builds).
All code in an App Store app must be signed ahead of time; Guidelines §2.5.2 also forbids downloading, installing, or executing code that introduces or changes app functionality.
sjulia never needs X on data: bytecode is just data interpreted by an already-signed Rust binary — only ordinary read-only executable pages are used.
Q. Couldn’t you just run real Julia on iOS some other way?
A. The constraints rule out the usual options:
Official Julia normally relies on the LLVM JIT — writable+executable pages need an entitlement ordinary apps cannot obtain, and §2.5.2 forbids downloading or executing code that changes app functionality.
Julia’s interpreter mode still ships the full JIT-capable runtime, and libjulia is not built for iOS targets.
Server-side execution (Binder/Colab-style) breaks offline and adds latency.
An interpreter compiled ahead of time to ordinary ARM code sidesteps all of this — the same reason Pythonista (Python) and Carnets (Jupyter) can exist on the App Store.
Q. How big is the sjulia codebase?
A. Roughly 570k lines of Rust (src/ code, excluding tests), plus a large pure-Julia layer:
A Cargo workspace of 14 crates — parser, lowering, IR, compiler, bytecode, VM, FFI, web/WASM, AoT runtime, …
The bytecode instruction set alone has 400+ opcodes.
The AoT pipeline (juliars) is ~53k lines — roughly 9–10% of the Rust code.
On top of that, Base is reimplemented as ~46k lines of pure-Julia subset code (75 files mirroring official Julia’s base/, plus 10 stdlib modules), and ~34 bundled packages (Plots, Distributions, AbstractAlgebra, …).
Verified by ~3,250 registered fixtures across 114 categories, with a parity sweep against real julia.
Most of it was written with AI coding agents in the loop — the parity suite is what kept that tractable.
Q. Why Rust (and not C/C++/Swift/Julia itself)?
A.
One codebase targets macOS/Linux, iOS (staticlib), Android (cdylib), and WASM — wasm32-unknown-unknown support with no C toolchain involved was decisive (it is also why the parser was rewritten from tree-sitter’s generated C into pure Rust).
Memory safety without a runtime GC of its own; enum + match fit a bytecode interpreter naturally (tagged Value, exhaustive opcode dispatch).
The project is ratcheting toward a panic-free VM policy — user code should raise Julia exceptions, not Rust panics/aborts, which matters inside an app process.
Development leaned heavily on AI coding agents; a strict compiler + ~3,250 parity fixtures made that loop safe.