Practical orientation for AI assistants working in the Nulang codebase. All paths are relative to the repo root (
~/nulang).
Nulang is a distributed, actor-based programming language written in Rust (edition 2021, Cargo workspace with a primary nulang crate and a nulang-ai support crate). It fuses Erlang-style fault-tolerant actors with a Rust/Pony-inspired type system (Hindley-Milner inference + reference capabilities + row-polymorphic algebraic effects), a register-based bytecode VM, a Cranelift JIT, a WASM backend (MIR→WASM via wasm-encoder) with a Wasmtime host runtime, BEAM/OTP primitives, CRDTs, location-transparent distribution, libsql/Turso persistence, PyO3 Python interop, a C-compatible FFI layer, a v0.9 AI runtime (pure types in crates/nulang-ai/, Runtime glue in src/runtime/), and a format-stability layer (src/format/ — frozen .nbc bytecode artifacts, NUL0 wire protocol versioning, migration registry). Status: Alpha; 1558 tests pass in the core crate (cargo test), 1596 with --features wasm-backend; the nulang-ai crate ships 58 additional tests (cargo test -p nulang-ai). License: Apache-2.0.
The compiler pipeline has two backends, selectable via --backend:
Bytecode backend (default):
source &str
-> Lexer::lex() -> Vec<Token> src/lexer.rs
-> Parser::parse_module() -> AstModule src/parser.rs
-> TypeChecker::check_module() -> Type src/typechecker.rs (HM Algorithm W)
-> EffectChecker::check_module() -> () src/effect_checker.rs
-> CapabilityAnalyzer::infer_cap() -> Capability src/effect_checker.rs
-> HIR lowering (hir_lower::lower_module) -> HIR Module src/hir_lower.rs
-> MIR lowering (mir_lower::lower_module) -> MIR Module src/mir_lower.rs
-> MIR codegen (mir_codegen::compile_mir) -> CodeModule src/mir_codegen.rs
-> VM::load_module() + VM::run() -> Value src/vm.rs (register VM + JIT tiering)
Native/AOT backend (--backend native):
source &str
-> ... (same frontend pipeline through MIR)
-> aot::codegen::compile_module() -> native object code src/aot/codegen.rs (MIR → Cranelift CLIF → native)
Uses Cranelift AOT compilation (src/aot/) with compile-time type metadata (src/type_metadata.rs) for unboxed native operations. Selectable via --backend native.
WASM backend (--backend wasm|wasm-run|wasm-aot, requires --features wasm-backend):
MIR Module
-> WasmBackend::compile() -> Vec<u8> (.wasm) src/mir_wasm.rs
-> WasmRuntime::new() + run() -> () src/wasm_runtime.rs (Wasmtime host)
-> (optional) aot_compile() -> .cwasm src/wasm_runtime.rs (wasmtime compile)
--check stops after capability analysis (no compile/run). The runtime (src/runtime/) is a multi-threaded work-stealing executor: the Runtime is a shard (actor subset by actor_id % shard_count), each shard runs on exactly one worker thread via run_scheduler(), and a Chase-Lev work-stealing scheduler (src/runtime/scheduler.rs) distributes work across worker_count threads per shard. Cross-shard messaging uses mpsc::SyncSender channels (CrossShardMsg in src/runtime/mod.rs); only value-type payloads cross shard boundaries, keeping ORCA reference counting local to each shard. Shard 0 alone binds the network transport. Runtime::new_sharded(num_shards) creates the full shard set; each shard's run_scheduler() runs in its own std::thread (spawned in main.rs via std::thread::scope). The runtime reaches the VM only through two object-safe callback traits (ActorVmCallbacks, DistributedVmCallbacks) to keep the dependency cycle-free. There is no async/await in the runtime or VM — concurrency is crossbeam deques/queues + std::sync atomics/RwLock/mpsc + raw unsafe pointers for ORCA GC. The only async surfaces are main.rs (#[tokio::main]), the LSP server (tower-lsp over tokio stdin/stdout), and the src/ai/ LLM client (async_trait, exposed to sync callers via complete_sync).
- Value (
src/vm.rs; tag constants canonical insrc/value_layout.rs): i64-taggedu64(raw), 48-bit payload, 16-bit type tag. Tags:TAG_NIL 0x7FF8,TAG_UNIT 0x7FF9,TAG_BOOL 0x7FFA,TAG_INT 0x7FFB,TAG_PTR 0x7FFC,TAG_ACTOR 0x7FFD,TAG_STRING 0x7FFE,TAG_CLOSURE 0x7FF7. - Instruction (
src/bytecode.rs): 32-bit fixed-width{opcode:u8, op1:u8, op2:u8, op3:u8}; helpersnew0/1/2/3,imm16(),simm16(),offset16(). 135 opcodes across 18 range groups (Special, Stack & Locals, Int/Float Arithmetic, Comparison & Logic, Control Flow, Closures, Memory & Objects, Actor & Concurrency, Effects, Python Interop, Direct Effect Dispatch, Actor ReceiveWait, FFI, Async Effect Dispatch, Distribution, String & IO, Debug & Meta, Register Spill). - Frames: 256 registers each; flat
Vec<Frame>withcaller_idxlinks; closures carryclosure_env.
Algebraic effects are runtime-resolved via handler_stack: Vec<HandlerFrame>: Handle pushes, Perform deep-clones a Continuation into the matching handler and jumps to its offset, Resume restores the continuation (overwrites frames/pc), Unwind pops. The static EffectRow is checked pre-compile; runtime only sees the four opcodes. Capabilities (iso/trn/ref/val/box/tag/lineariso) are compile-time only and erased at runtime — there are no capability opcodes; mir::RValue::CapabilityCheck compiles to Const1 (true) in src/mir_codegen.rs. LinearIso is enforced statically: CapabilityAnalyzer tracks consumption per binding (at-most-once use along every path; sends and closure captures consume; conservative branch merge).
VM holds jit_session: Option<JitSession>. Before each instruction (vm.rs:1170-1216) the VM snapshots frame regs into [u64;256] and calls jit::tiered_execute_step_typed. Cold code interprets; when a PC's hot counter hits HOT_THRESHOLD=1000, find_compilable_region (max 500 instrs, stops at unsupported op / Ret) is compiled. SIMD path: simd_analyzer detects element-wise binop/unary/cmp loops → simd_compiler emits prefix-scalar + SIMD-body (I64x2/F64x2/I32x4/F32x4) + epilogue CLIF; falls back to compile_region_typed (typed when metadata is provable, else scalar compiler::compile_region). Compiled fn ABI: extern "C" fn(*mut u64 regs, *const u64 constants). Runtime helpers are #[no_mangle] extern "C" in src/jit/runtime.rs (NaN-tag-aware; div-by-zero → nil). typed_compiler strips NaN-tag guards using TypeMetadata recovered at tier-up time by typed_compiler::infer_reg_types (a conservative forward must-analysis over the enclosing function's bytecode — MIR pins each typed local to a fixed register, so types are recoverable from the instruction stream; unmodeled opcodes clobber all registers, effect opcodes yield empty metadata). The live tiering entry point is jit::tiered_execute_step_typed (vm.rs step()): hot regions compile through the guard-stripped path when register types are provable, falling back to scalar compile_region on absent/empty metadata or compile error. Typed IDiv/IMod/FCmpEq always emit runtime-helper calls (never raw sdiv/srem/fcmp) to match interpreter semantics exactly (div-by-zero → nil, epsilon float equality).
- Spawn:
Runtime::spawn_actor→fresh_actor_id()(globalAtomicU64) →Actor::new(64KBActorHeap+OrcaGc) → enqueue in scheduler globalInjector. - Schedule:
run_schedulercallsdrain_cross_shard_messages()before each iteration, thenscheduler.dequeue()(Chase-Lev: local LIFO pop, then inter-worker FIFO steal, then global injector by priority — High, Normal, Low, FIFO within each level).step_actorsetscurrent_actor, receives from mailbox, resolvesbehavior_id→BehaviorEntry.handler_fn(fn pointer) or bytecode handler (raw*mut Runtime), journals+checkpoints if persistent, incrementsreduction_count(monotonic lifetime metric; a separateturn_reductionstracks the per-turn budget); requeues while the mailbox is non-empty — when the per-turn budget (max_reductions=1000messages) is exhausted it resetsturn_reductionsand requeues at the back of the queue (yield); the turn budget also resets when the actor goes Waiting. - Send:
send_message_by_id→Message→mailbox.push(alwaysOk, never drops) → ORCAsend_ref_tobumpsforeign_count→ enqueue target. - GC:
process_gc_opsdrainsOrcaCoordinator→ per-actorOrcaGcapplies deltas;CycleDetector::incremental_detect(epoch-gated) builds foreign-ref graph, suspects by weight, DFS, trial-decrement, reclaims. - Fault:
exit_actor→handle_actor_exit→ unregister +leave_all, DOWN to monitors, propagate to links (abnormal kills non-trapping; trapping gets System msg),Supervisor.handle_exit→SupervisorAction(Restarted/Shutdown/Ignore/Escalate) with cascading shutdown.
Custom TCP wire protocol (src/runtime/network.rs): length-prefixed frames, magic NUL0, 8-byte node-id handshake, Packet enum (ActorMessage/Heartbeat/Ack/SpawnRequest/SpawnResponse/CrdtSync/CrdtDeltaSync/Gossip) with hand-rolled big-endian serde. TCP links are fully duplex — reader threads run on both accepted inbound and dialled outbound connections (a joiner that only dials out still receives heartbeat replies). AddressResolver + ActorAddress::{Local,Remote} + LRU RemoteActorCache (10k) provide location transparency. Gossip membership in cluster.rs (ClusterState::tick → ClusterAction): tick heartbeats Joining + Healthy members; SendGossip is wired over the wire as Packet::Gossip (type 6, Vec: node id, address, status, incarnation per member), sent by Runtime::process_network and merged on receipt via ClusterState::merge_membership (higher incarnation wins; equal-incarnation gossip refreshes last_heartbeat as a liveness hint) — transitive propagation works, so a chain of pairwise seeds converges without a full mesh (heartbeat-based discovery — process_network_packets learns unknown heartbeat senders via NetworkTransport::connection_addr — remains the path by which a seed first learns about a joiner). Remote spawn: Packet::SpawnRequest is answered in process_network_packets — the receiver spawns the named behavior only if it was registered via Runtime::register_spawnable_behavior (unknown names get SpawnResponse{success:false}); the requester picks up the real actor id with Runtime::take_spawn_response(request_id) (the placeholder address from spawn_on_node carries the request id, not an actor id). Heartbeat packets carry the sender's node id. Actor-message payloads cross the wire losslessly: ints/floats/bools/unit serialize directly, and string values travel by content — Packet::ActorMessage carries a string_table: Vec<String> that the sender fills from its module constant pool (resolve_wire_strings in distributed.rs) and the receiver interns into the target actor's module pool on the scheduler thread (intern_wire_strings), so the same content gets one pool id per node. Heap pointers, closures, actor refs, and nil are rejected at send time (packet_payload_wire_safe in network.rs), as are spawn-request strings (remotely spawned native handlers have no module pool to intern into). CRDTs: 8 types (GCounter, PNCounter, GSet, ORSet, AWORSet in crdt.rs; LWWRegister, MVRegister, RGA in crdt_reg.rs) behind the Crdt trait, owned by CrdtManager. Delta-state replication: each CRDT exposes delta_since(base) (minimal state that merges identically for any replica ≥ base; None = unchanged); CrdtManager::generate_delta_sync_ops ships first-seen entries full and changed entries as deltas over Packet::CrdtDeltaSync (type 7, Vec<CrdtDeltaOp>), applied via apply_delta_op (merge-only; deltas for unknown ids are ignored). sync_crdts_delta (distributed.rs) broadcasts deltas to healthy members; Runtime::sync_crdts ships deltas on most rounds and full state on round 1 and every CRDT_FULL_SYNC_INTERVAL (16) rounds thereafter — the sync base advances at delta generation, so these periodic full syncs are the repair mechanism for lost deltas. Persistence: PersistenceStore trait with MemoryStore, JsonFileStore, SqliteStore (rusqlite, two tables).
src/— language frontend + backend:lexer.rs,parser.rs,ast.rs,typechecker.rs,types.rs,effect_checker.rs(effects + capabilities),bytecode.rs,value_layout.rs(canonical i64-tagged constants),hir.rs/hir_lower.rs,mir.rs/mir_lower.rs/mir_codegen.rs,vm.rs,repl.rs,main.rs,lib.rs,format/(frozen artifact formats + migration), plusintegration_tests/&stress_tests.rs(test-only). The legacycompiler.rswas removed — the pipeline is MIR-exclusive.src/mir_wasm.rs— WASM backend: MIR→WASM compiler viawasm-encoder(behindwasm-backendfeature). Emits.wasmmodules with string interning, i64-tagged values, and effect dispatch. Includes SIMD lowering framework for array vectorization.src/wasm_runtime.rs— Wasmtime host runtime: loads and executes.wasmmodules (behindwasm-backendfeature). Configured with guard pages (4GiB reserv, 128MiB guard), Cranelift speed opts (inlining), and SIMD. Provides AOT compilation viawasmtime compile.src/runtime/— actor runtime:mod.rs(Runtimegod-object),actor.rs,scheduler.rs,mailbox.rs,heap.rs(bump allocator with grow-on-demand chained 64KB blocks — exhaustion chains a fresh block instead of failing, objects never move; size-class free lists + large-object space for allocations over the 256-byteHugethreshold, exact-size free-list reuse, all blocks released onreset()/Drop),gc.rs,orca_cycle.rs,supervisor.rs,registry.rs,process_groups.rs,timer.rs,cluster.rs,network.rs,distributed.rs,crdt.rs/crdt_reg.rs/crdt_manager.rs,persistence.rs,tests.rs.src/jit/— Cranelift JIT:mod.rs(JitSession,tiered_execute_step, hot counters),compiler.rs(scalar CLIF),typed_compiler.rs,simd_analyzer.rs/simd_compiler.rs,runtime.rs(extern-C helpers),tests.rs.src/lsp/—tower-lsplanguage server (singlemod.rs).src/python/— PyO3 interop:bridge.rs(GIL +PythonRegistry),marshal.rs(Value↔Py).crates/nulang-ai/— extracted v0.9 AI runtime crate (workspace member,nulang_ailibrary name) with zero dependency on the corenulangcrate. Optional; pulled in by theai-runtimecargo feature. Modules:client.rs(asyncLlmClienttrait + synccomplete_syncbridge),request.rs/response.rs(provider-agnostic wire types),providers/(ollama.rs,openai.rs),mock.rs, memory (memory.rsepisodic,semantic_memory.rs,procedural_memory.rs),pipeline.rs+debate.rs+supervisor.rs(orchestration primitives, each with its own*Runtimetrait),registry.rs(AiRuntimeRegistryfor pipelines/debates +SupervisorTeamRegistry, both generic over the runtime traits so the coreRuntimenever appears in the crate),usage.rs(TokenBudget,estimated_cost,UsageSummary). Core imports everything throughuse nulang_ai::…;directly — there is nosrc/ai/façade module.src/runtime/ai_impls.rs— core-sideimpl PipelineRuntime for Runtime,impl DebateRuntime for Runtime,impl SupervisorRuntime for Runtime. Kept in core by the orphan rule; the ~100-line file is the entire trait glue betweennulang-aiand the actorRuntime.src/runtime/agent.rs,src/runtime/llm.rs— remaining AI-runtime integration that legitimately needsRuntimeinternals: the agent LLM completion pipeline (build_agent_llm_request,finish_agent_llm,complete_agent_llm) reads actor durable state and allocates strings into actor heaps;LlmStateowns the persistentnulang-llmworker thread + completion channels polled by the scheduler.src/aot/— AOT native compiler:mod.rs(module orchestrator, runtime helpers),codegen.rs(MIR→CLIF per-function). Compiles to native code via Cranelift using compile-time type metadata for unboxed operations.src/wasm_component_runtime.rs— WASM Component host runtime: loads.wasmcomponent binaries and executes actor lifecycle (init,handle_message,checkpoint) via Wasmtime.ComponentPoolrecycles instances for low-latency dispatch.src/wasm_types.rs— Borsh-based wire format for WASM Component boundary serialization. Defines the serializable subset of Nulang values shared between component compiler and host runtime.src/type_metadata.rs— Compile-time type knowledge shared between JIT (typed_compiler.rs) and AOT (aot/) backends. Maps registers/locals to statically-known types for unboxed code generation.src/runtime/heap_serialize.rs— Portable heap serialization for durable continuations ("scale to zero" hibernation). Serializes heap objects and VM state into a portable binary format (magicNLCS, big-endian).src/runtime/distributed_context.rs— Extracted distributed-subsystem state (NetworkTransport,ClusterState,AddressResolver) factored out of the Runtime god-object.src/ffi/— C-compatible FFI layer:mod.rs(module root + Rust registration API),native.rs(dynamic library registry),marshal.rs(Value↔C ABI),c_api.rs(stable C embedder API).src/package/—nulapackage manager (MVP):manifest.rs(Nulang.tomlvia thetomlcrate),lockfile.rs(Nulang.lockwithreg+support),resolver.rs(local-path + git + registry deps, simple semver checks, topo order),commands.rs(nulang nula new|build|test|run|publish, dispatched frommain.rs; build/test/run shell out to the currentnulangexecutable).src/registry/—nulapackage registry:server.rs(HTTP server vianulang registry servewith Bearer auth),client.rs(HTTP client viaureqused by resolver andnula publish).src/docgen.rs— documentation generator: scans.nulafiles for//////!comments, extractsfn/actor/type/workflowdeclarations, and emitsdocs/api.md.src/stdlib.rs— standard-library inventory: documents built-in effects/functions (IO.print,IO.read,Timer.sleep,Signal.wait,LLM.ask,Provider.ask,Workflow.query,Actor.*) with signatures and descriptions.src/format/— format-stability layer:constants.rs(LANGUAGE_VERSION),nbc.rs(frozen.nbcbytecode artifact format:CodeModule::to_nbc/from_nbc, BLAKE3source_hash),migrate.rs(versioned migration registry, the sole legal home for format upgrades)..cargo/—config.toml(bfd linker + PyO3 abi3 env),audit.toml(one ignored advisory).build.rs— Fedora libpython symlink workaround for PyO3 linking..agents/— orchestration scratch/handoff artifacts from a prior multi-agent analysis run; not language source.RFC/— Nulang RFC proposals (format stability, frozen core, deprecation cycles, roadmap items).GOVERNANCE.md— stability tiers (Frozen / Stable / Experimental) and RFC process.CHANGELOG.md— changelog organized by stability tier, tracking the language version (not the crate version).SPEC2.md— language specification: syntax, semantics, type system, runtime, standard library, and format stability contract.
cargo build # dev build (opt-level 0, debug)
cargo build --release # release (opt-level 3, LTO, codegen-units 1)
cargo build --features wasm-backend # dev build with WASM backend + Wasmtime
cargo test # run all 1558 tests (test profile: no LTO, 16 codegen-units for speed)
cargo test --features wasm-backend # run all 1596 tests including WASM backend
cargo test -p nulang-ai # run the AI crate's 58 tests
cargo test --release # run tests under the release profile
cargo run -- --repl # interactive REPL (prompt `nulang>`)
cargo run -- --eval 'perform IO.print("Hello")' # evaluate a string
cargo run -- --check myprogram.nula # type+effect+cap check only (no run)
cargo run -- myprogram.nula # compile and run a file
cargo run -- --backend wasm myprogram.nula # compile to out.wasm
cargo run -- --backend wasm-run myprogram.nula # compile and run via Wasmtime
cargo run -- --backend wasm-aot myprogram.nula # compile to .wasm + .cwasm (AOT)
cargo run -- --lsp # start the LSP server on stdin/stdout
cargo run -- nula new my-app # package manager: scaffold a package
cargo run -- nula build my-app # resolve deps + type-check
cargo run -- nula build-wasm my-app # resolve deps + build .wasm + .cwasm
cargo run -- nula test|run my-app # package manager: test/run
cargo run -- --doc # generate docs/api.md from .nula doc comments
cargo run -- -v myprogram.nula # verbose: print AST/bytecode/inferred type
python3 verify_implementation.py # gate: cargo test + forbidden-pattern scans + integration checks
python3 verify_report.py # gate: validates codebase_analysis_report.md structureRuntime requirements: Rust stable 1.93+ (cranelift 0.132 requires 1.93), Linux or macOS. .cargo/config.toml forces the GNU bfd linker on x86_64-unknown-linux-gnu (for Cranelift/PyO3 native-symbol linking) and sets PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1. build.rs creates a libpythonX.Y.so symlink in OUT_DIR for Fedora-style systems missing the unversioned symlink (auto-detects Python; default 3.14). mimalloc is the #[global_allocator] (src/main.rs).
- Naming:
snake_casefunctions/methods/modules/files;PascalCasetypes/structs/enums and enum variants;SCREAMING_SNAKE_CASEconsts (HOT_THRESHOLD,TAG_INT,PAYLOAD_MASK).nulang_prefix onextern "C"JIT runtime helpers.__mainis the synthetic function wrapping a top-level expression (parser + HIR lowering). - Error model: one project-wide
NuErrorenum (src/types.rs:463) aliasedNuResult<T> = Result<T, NuError>. Compile-time variants (LexError/ParseError/TypeError/EffectError/CapError/LinearTypeError) carry{ msg: String, span: Span }; runtime variants (RuntimeError/VMError/PythonError) carryString.Displayformats spanned errors as<Kind> at <line>:<col>: <msg>. First error aborts; no error collection/recovery.?propagates.EffectChecker/CapabilityAnalyzeraccumulatediagnostics: Vec<String>instead of failing fast. Runtime subsystems use per-domain enums (RegisterError,PgError) implstd::error::Error; persistence/network useio::Result/Option; JIT usesCompileError. Noanyhow/thiserror. - Async: only
main.rs(#[tokio::main]),src/lsp/, and thenulang-aiLLM client (async_trait) are async. VM, runtime, JIT, Python, REPL are all synchronous. Actor concurrency is cooperative reduction-yielding, not async tasks. - Non-blocking LLM calls:
perform LLM.ask(...)in scheduler-driven actor bytecode behaviors is non-blocking. TheLlmAskopcode callsActorVmCallbacks::llm_ask(default delegates to blockingcomplete_llm);BytecodeRuntimeCallbacks::llm_askbuilds theLlmRequeston the scheduler thread, spawns anulang-llmworker thread (own current-thread tokio runtime +block_on(client.complete(...)), result sent overRuntime.llm_tx), and returnsPending— the VM then decrements the PC and raises the"LlmAsk:suspend"sentinel error, captured ontoactor.suspended_executioninrun_bytecode_at_offsetexactly like"SignalWait:suspend"and"ReceiveWait:suspend"(helperis_suspend_error).run_schedulerpumps completions (poll_llm_completions→store_llm_completion→resume_suspended_llm_step, which re-installs per-actor callbacks beforevm.resume()and re-captures on chained suspends) and keeps running whilellm_inflight_count > 0(10msrecv_timeoutwait when the queue is drained). A workflow step resumed from a signal wait (resume_suspended_workflow_step) goes through the same machinery — it re-installs the actor's callbacks and enablesllm_suspend_enabledaroundvm.resume(), so aperform LLM.askafter the wait suspends non-blockingly and re-captures on either sentinel viais_suspend_error, withsuspension_markerrecording whether the actor awaits a signal or an LLM completion. Suspension is gated onRuntime.llm_suspend_enabled:step_actorenables it around the bytecode invocation;ask_actor_syncforces it off, so nested synchronous paths (pipelines, supervisors, debates,Ask, top-level VM) keep blocking behavior. Request build (build_agent_llm_request/build_actor_llm_request) happens pre-suspend; tool-call post-processing (finish_tool_calls) and agent memory/usage write-back (finish_agent_llm) happen on the scheduler thread at resume. - Unsafe / FFI: raw
*mutpointers with hand-writtenunsafe Send/SyncandSAFETYdoc justifications (ORCA headers, foreign-ref ops,BytecodeRuntimeCallbacks). JIT function pointers obtained viaunsafe transmuteof*const u8; bytecode must not mutate during JIT execution. Python: GIL acquired viaPython::attach;PythonObjectIdis a non-owningCopyhandle (real refcount inPythonRegistry);get_objectacquires GIL before the registryMutexto avoid lock-order deadlock. - Dependency injection / decoupling: the VM talks to the runtime through two object-safe callback traits (
ActorVmCallbacks,DistributedVmCallbacks) — defaultStandaloneVmCallbacksowns a privateActorHeap.RuntimeVmCallbacks(Rc<RefCell<Runtime>>) andBytecodeRuntimeCallbacks(raw*mut Runtime) bridge the other direction. - State: actor state fields live in two parallel maps —
state_data: HashMap<String, Value>andstate_models: HashMap<String, StateModel>(src/runtime/actor.rs:26-27); each field'sStateModelisLocal/Durable/EventSourced/Crdt. Actor identity is a bareu64(noPidwrapper), fromfresh_actor_id(). - Spans: threaded into nearly every
Expr/Declvariant and every compile-time error. - Type system: HM Algorithm W (
Substitution = Vec<(TypeVar,Type)>,mgu+ occurs check,generalize/instantiateoverType::Scheme); Pony-inspiredCapabilitylattice (is_subtype_ofviajoin,is_sendable) withLinearIso/Linearconsumption tracking inCapabilityAnalyzer(src/effect_checker.rs): at-most-once (no double-use) is enforced for every binding; exactly-once (must-use) is enforced forlet-bound linear values (2026-08-02, with a transparent-rebind exemption for barelet a = xaliases) and for values already bound in the initial context, e.g. function/lambda/behavior parameters whose capability is seeded into theCapContext(2026-08-07, extended to the FFI and LSP capability-analysis paths); Koka-inspired row-polymorphicEffectRow(Closed/OpenwithRegion). Records are row-polymorphic too: an open record carries a row tail encoded as the reserved".."pseudo-field holding a rowType::Var(RECORD_ROW_TAIL_FIELD,src/types.rs; literals and annotations are always closed). Field access on an unknown-shape receiver accumulates the demanded field into the row (infer_field_accessinsrc/typechecker.rs), sofn(r) r.x + r.ygeneralizes over any record withxandy; closed records unify exactly (unify_closed_records— field-count mismatch is a type error, sofn(r: {x: Int})rejects a 2-field literal).
src/main.rs— CLI entry; hand-rolled arg parser;run_source/check_sourcepipeline;#[tokio::main].src/lib.rs— crate root; declares all public modules.src/hir.rs,src/mir.rs— High-level and Mid-level IR type definitions.src/hir_lower.rs,src/mir_lower.rs,src/mir_codegen.rs— AST → HIR → MIR → bytecode pipeline.src/vm.rs— i64-taggedValue,Frame,VM,step/run, effect handlers, JIT hook, callback traits.src/mir_wasm.rs— WASM backend: MIR→.wasmcompiler (behindwasm-backendfeature). SIMD lowering framework.src/wasm_runtime.rs— Wasmtime host runtime with guard pages, inlining, SIMD, AOT compilation.src/runtime/mod.rs—Runtimegod-object; actors, scheduler, GC, supervision, distribution, persistence.src/lsp/mod.rs— Full-featured LSP server (12 features: hover, goto def, references, rename, signature help, inlay hints, completion, diagnostics, etc.).
- Runtime: Rust stable, edition 2021. Linux/macOS (Windows planned).
- Linker: GNU
bfdforced on x86_64 Linux via.cargo/config.toml(notlld) for Cranelift/PyO3 compatibility. - Python: PyO3 0.29 abi3 limited-API;
build.rssymlinkslibpythonX.Y.sofor Fedora. - Allocator: mimalloc (
#[global_allocator]inmain.rs). - Cargo features:
default = ["python", "sqlite", "lsp", "ai-runtime"](PyO3 interop, libsql/Turso persistence, tower-lsp server, AI runtime).wasm-backendis optional (off by default) — enableswasm-encoderWASM compiler,wasmtimehost runtime, and--backend wasm|wasm-run|wasm-aotCLI modes. All features are optional;--no-default-features --features <subset>builds a leaner binary. - No external test/criterion/proptest crates — standard
#[test]only.
- Framework: standard Rust
#[test]+#[cfg(test)]. No proptest/quickcheck/criterion. No#[ignore]/#[should_panic]/async tests. - Organization: two styles — (a) inline
mod testsat file foot (lexer.rs,parser.rs,typechecker.rs,effect_checker.rs,value_layout.rs,vm.rs, mostruntime/*.rs,jit/*,python/*,ffi/*,lsp/mod.rs, plus every source file incrates/nulang-ai/src/); (b) dedicated test files (src/integration_tests/mod.rs,src/stress_tests.rs,src/runtime/tests.rs,src/jit/tests.rs). - Naming:
test_<subject>(unit/integration),stress_<scenario>(chaos). - Counts: 1596 total in the core crate with
wasm-backend, 1558 without; 58 more innulang-ai. The suite coverssrc/integration_tests/mod.rs(end-to-end pipeline viarun_source/assert_int/run_source_with_runtimeplus MIR-pipeline variants, WASM backend e2e tests, selective-receive and receive-after,Actor.*builtin-effect and actor-priority, non-blocking LLM suspend/resume, typed-JIT tiering, float-threading regression tests, behavior-internal send/spawn, workflow query, and Otp supervisor effects),stress_tests.rs,runtime/tests.rs,jit/tests.rs,src/mir_wasm.rs,src/wasm_runtime.rs,src/aot/codegen.rs,src/package/,src/docgen.rs,src/stdlib.rs,src/lsp/mod.rs, plus inlinemod testsacrosslexer.rs,parser.rs,typechecker.rs,effect_checker.rs,value_layout.rs,vm.rs,runtime/*.rs,jit/*.rs,python/*,ffi/*, and everycrates/nulang-ai/src/*.rs. Per-file breakdowns are too volatile to maintain manually — runcargo test 2>&1 | grep "test result:"for the current split. - Run:
cargo test(test profile: LTO off, 16 codegen-units for fast parallel builds).cargo test --releasefor optimized runs. - Gate scripts:
verify_implementation.py(forbidden-pattern scans for known anti-patterns — Box'd frames, string leaks,crdt_regtemp Vec, timer BinaryHeap rebuild, check-then-unwrap — + asserts JIT integration, escape-analysis deadness, scheduler-stats and cycle-detector wiring, then runscargo testandcargo check --testsagainst a zero-warning baseline) andverify_report.py(validatescodebase_analysis_report.md: required sections, ≥5 code snippets, referencedsrc/*.rspaths exist). Each exits 0 only on full pass. - Audit:
.cargo/audit.tomlignoresRUSTSEC-2026-0186(memmap2 unsound; vulnerable APIs unused; upgrade blocked on cranelift-jit).
- The
escape_analysis.rsmodule was removed; its former tests and references have been cleaned up. - NaN-tag constants now have a single source of truth:
src/value_layout.rs(TAG_MASK,PAYLOAD_MASK,SIGN_BIT, allTAG_*, plussext48/tag_int/tag_bool).src/vm.rs,src/jit/{runtime,compiler,typed_compiler,simd_compiler}.rs, andsrc/python/marshal.rsall import from it — do not reintroduce local copies. The one exception isTAG_PYTHON(0x7FF6), defined insrc/python/bridge.rsand imported bymarshal.rs; it was chosen not to collide withTAG_CLOSURE(0x7FF7) orTAG_STRING(0x7FFE). - Remote actor messages carry the behavior name on the wire (
Packet::ActorMessage.behavior_name); the receiving node resolves it viaRuntime::behavior_id_foragainst the target actor's behavior table on delivery (process_network_packetsinsrc/runtime/distributed.rs), falling back to behavior id 0 for unknown names — mirroring localsend_message'sunwrap_or(0). String payloads cross the wire by content, never by pool id:Packet::ActorMessage.string_tablecarries the UTF-8 text, populated on send bydistributed::resolve_wire_strings(from the current actor's module pool) and interned on delivery bydistributed::intern_wire_stringsinto the target actor's module pool viaVM::add_runtime_string— always on the scheduler thread, never in a network reader thread. Heap pointers, closures, actor refs, and nil stay rejected at send time (packet_payload_wire_safe), as are strings inSpawnRequestinitial state (remotely-spawned actors have native handlers and no module pool). - Cluster membership entries carry their gossip version in the
_incarnationmetadata key onNodeInfo.metadata(absent = baseline 1 ingossip_payload, 0 in merge-compare).merge_membershiponly applies status/address changes on a strictly higher incarnation; equal incarnation only refresheslast_heartbeat.join_clusterseeds start at incarnation 1 so gossip can't clobber the authoritative seed address;handle_heartbeatbumps the entry incarnation on status promotions so they propagate. LamportTime/LamportClockare defined incrdt.rsand imported bycrdt_reg.rs— single definition, no duplication.- LSP: 12 features (per the capability table at the top of
src/lsp/mod.rs) — diagnostics (full frontend), hover, goto definition, references, document symbols, rename (with prepareRename), signature help, formatting, semantic tokens, code actions, inlay hints (typechecker-backed for well-formed programs, regex fallback), completion. Zero compiler warnings (enforced byverify_implementation.py). - The
receiveexpression (receive { | Behavior(params) => expr }) is wired end-to-end with selective-receive dispatch. MIR lowering (lower_receiveinsrc/mir_lower.rs) resolves arm behavior names to behavior-table indices (same suffix-match rule assend) and emitsmir::RValue::ReceiveMatch(bytecodeOpCode::ReceiveMatch0x8F): a spec constant"max_params:id1,id2,..."carries the candidate behavior ids, the VM callsActorVmCallbacks::try_receive_match(&ids)(mailbox scan inMailbox::receive_match, FIFO order, non-matching messages requeued), writes the matched arm index to dst plus payload values into the following registers (missing → nil, extras ignored), and a MIR compare chain dispatches to the arm body with params bound. No-match falls through to the legacy pop-anyReceive(nil when the mailbox is empty or outside an actor context) — non-blocking, no suspension. The timed formreceive { arms } after ms => bodyinstead emitsOpCode::ReceiveWait(0xA0, same spec-constant/register contract asReceiveMatch, timeout staged in r0): on no match with a positive timeout inside an actor the VM decrements the PC and raises the"ReceiveWait:suspend"sentinel (captured ontoactor.suspended_executionlike the other suspend sentinels); the runtime arms a one-shot timer (Actor.receive_wait: Option<ReceiveWaitState>, armed once per wait), resumes the suspended behavior when a matching message arrives or the timer fires (timeout → the re-executedReceiveWaitwrites the arm-count sentinel to dst and the compare chain routes to the after body), and resolves non-positive timeouts/outside-actor contexts synchronously with the same sentinel (no legacy pop-any fallthrough in the timed form). - Behavior-internal
send/spawnand the deferred receive-wait wake:sendandspawnwork from inside scheduler-driven bytecode behaviors —BytecodeRuntimeCallbacks::send_messagecallsRuntime::send_message_by_id, andBytecodeRuntimeCallbacks::spawn_actorsharesRuntime::spawn_from_modulewithRuntimeVmCallbacks::spawn_actor. The one piece ofsend_message_by_idthat is unsafe mid-behavior is the receive-wait wake hook:resume_suspended_receive_waitrunsvm.resume()on the sharedrt.vm, which would nest VM execution inside the sender's still-running behavior and clobber the shared frames. The runtime therefore tracksvm_execution_depth(vm_exec_begin/vm_exec_endwrap every shared-VM call:run_bytecode_at_offset,resume_suspended_receive_wait,resume_suspended_llm_step,resume_suspended_workflow_step). While the depth is > 0 the wake hook dedup-pushes the target ontoRuntime.pending_receive_wakesinstead of resuming;vm_exec_enddrains the backlog when the outermost call returns, looping until empty because a resumed behavior can itself send and re-queue a wake (draining_receive_wakeskeeps the drain iterative rather than recursive, and setscurrent_actorto the resumed actor around each resume soMessage.senderis attributed correctly).vm_exec_endmust run only AFTER any suspend-state capture (vm.take_suspended_state) on every path — a drained resume callsvm.restore_suspended_state, which would overwrite the frames an un-captured suspend still needs. Nestedaskfrom behaviors remains unsupported (theBytecodeRuntimeCallbacks::ask_actortrait default returns nil). - BEAM fault-tolerance language surface:
perform Actor.link/unlink/monitor/demonitor/trap_exit/exit/register/unregister/whereis/set_prioritydispatch throughActorVmCallbacks::perform_builtin_effect(no user handler installed) intoRuntime::perform_actor_builtin— both runtime callback impls (RuntimeVmCallbacks,BytecodeRuntimeCallbacks) reach it; the standalone VM nil-no-ops everyActor.*effect (matching the outside-an-actor contract).spawn link Actor {..}/spawn monitor Actor {..}are parser desugars tospawn+Actor.link/Actor.monitoron the spawner. Actor scheduling priority:Actor.priority: ActorPriority {High, Normal=default, Low}(set viaActor.set_priority(0|1|2)); the scheduler's global injector is split into three per-level queues and every enqueue path (Runtime::enqueue_actor) reads the actor's current priority — strict per-level preference (all High before any Normal before any Low, FIFO within a level, Erlang-like; lower levels can starve under sustained High load), reduction-budget yield fairness unchanged. Priority affects scheduling only —Mailbox::receive_matchstays FIFO and ignoresMessage::priority. - The compiler pipeline is MIR-exclusive (AST → HIR → MIR → bytecode). The legacy AST compiler (
src/compiler.rs) has been removed. - Reclamation protocol (do not break): intra-actor memory is reclaimed by three cooperating pieces. (1) VM write barriers —
ArrStore/RecS/FieldSretain a stored heap pointer and release the overwritten slot's old value (src/vm.rs); (2)OrcaGc::free_objectreleases slot references when a container is freed (src/runtime/gc.rs) — so every container slot MUST hold a counted reference, which is why theFieldSbarrier is mandatory; (3)plan_dropsinsrc/mir_codegen.rsemitsOpCode::Dropat conservative liveness points, and theDrophandler clears the register to nil so duplicate drops are no-ops — removing the nil-clearing reintroduces double-decrement.OrcaHeadercounts (ref_count/foreign_count/sticky) andGcStatsare plain integers by the per-shard thread-confinement invariant (all heap/GC access runs on the owning shard's scheduler thread; network/LLM/Python threads never touch heaps) — do not reintroduce cross-thread heap access without restoring atomics. Message.payloadisArc<Vec<Value>>: payload values are shared viaArcto avoid cloning on everyreceive_matchscan. The VM never mutates incoming payloads, soArcis safe. Construct withArc::new(vec![...])— a barevec![...]is a type error.- DST determinism:
run_scheduler_deterministic_with_rng(with the seed-takingrun_scheduler_deterministicwrapper) drives actor selection viapick_ready_actor_deterministicover the sorted ready set, drains cross-shard messages each iteration, advances a virtual clock to timer deadlines, and pumps GC on the production cadence (process_deferred_alleveryGC_PUMP_INTERVALsteps,process_gc_ops+ deferred retry at quiescence — the DST path applies foreign-ref decrements exactly like the productionrun_scheduler's drained-queue drain).ClusterState::set_rngseeds gossip/repair picks (DeterministicRngimplementsrand_core::RngCore).Runtime::enable_distribution_with_transportlets tests run realRuntimes over the in-memoryDeterministicNetworkTransport;src/runtime/cluster_dst.rs(DeterministicCluster, test-gated) pumps N such nodes with per-node virtual clocks advanced in lockstep and one master RNG permuting node order per round. Fault injection on theNetworkTransporttrait (default no-ops, real in the DST transport):set_partition(firewall-style drop),set_reorder/flush_held(bounded adjacent reorder — consecutive packets to a peer arrive swapped; nothing lost or duplicated, only delayed one slot, deterministic per-pair state, no RNG). Production paths never install a clock … Mailbox.skip_bufferisVecDeque, notRefCell<VecDeque>: all methods that access the skip-buffer take&mut selfbecause they run exclusively on the owning shard's scheduler thread.Mailboxcarriesunsafe impl Sync— theSegQueuefields are lock-free and the mutableskip_bufferis thread-confined.TypeMetadata.regsis[KnownType; 256], notHashMap<usize, KnownType>: a flat array replaces the previousHashMapfor deterministic O(1) access with no hashing overhead.KnownType::Unknown(the default) represents untyped registers.TypeMetadata::is_empty()returns true when every register isUnknown.infer_reg_typesinsrc/jit/typed_compiler.rsand callers insrc/jit/mod.rs/src/jit/tests.rsuse.is_empty()instead of.reg_types.is_empty().- WASM backend (
src/mir_wasm.rs): thewasm-backendfeature enableswasm-encoder(WASM binary encoding) andwasmtime(host runtime). The WASM module uses i64-tagged values (not NaN-boxed) to avoid WASM NaN canonicalization. Function indices in the import section are offset by one — the memory import (index 0) is NOT a function import, soFUNC_IMPORT_COUNT(5) differs from the total import count (6). When adding new imports, remember to update both the import-order-preservingrebuild_imports()and the function-index constants (IMPORT_*). SIMD lowering inmir_wasm.rsuses raw byte emission viaFunction::raw()becausewasm-encoder0.220 lacks SIMD instruction variants in itsInstructionenum. Theunreachableafter each blockendis required for WASM validation: every MIR block terminates with a divergent instruction (return/br), but the validator needs the block fallthrough path marked unreachable to match the function's return type. - Wasmtime runtime (
src/wasm_runtime.rs):wasmtime = "46"usesdefault-features = falsewithcranelift,runtime,std,anyhowfeatures. TheErrortype iswasmtime::Error(NOTanyhow::Error— they are distinct in wasmtime 46, thoughwasmtime::Error::msg()provides string construction). Host import callbacks useCaller<'_, HostState>viaLinker::func_wrap. Thenulang_initexport has type() -> i64(returns the tagged program result), NOT() -> (). AOT compilation (wasmtime compile) requires thewasmtimeCLI tool installed on the build host.