Skip to content

Latest commit

 

History

History
188 lines (160 loc) · 44 KB

File metadata and controls

188 lines (160 loc) · 44 KB

Repository Guidelines

Practical orientation for AI assistants working in the Nulang codebase. All paths are relative to the repo root (~/nulang).

Project Overview

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.

Architecture & Data Flow

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 in src/value_layout.rs): i64-tagged u64 (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}; helpers new0/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> with caller_idx links; closures carry closure_env.

Effects & capabilities at runtime

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).

JIT tiering

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).

Actor runtime lifecycle

  1. Spawn: Runtime::spawn_actorfresh_actor_id() (global AtomicU64) → Actor::new (64KB ActorHeap + OrcaGc) → enqueue in scheduler global Injector.
  2. Schedule: run_scheduler calls drain_cross_shard_messages() before each iteration, then scheduler.dequeue() (Chase-Lev: local LIFO pop, then inter-worker FIFO steal, then global injector by priority — High, Normal, Low, FIFO within each level). step_actor sets current_actor, receives from mailbox, resolves behavior_idBehaviorEntry.handler_fn (fn pointer) or bytecode handler (raw *mut Runtime), journals+checkpoints if persistent, increments reduction_count (monotonic lifetime metric; a separate turn_reductions tracks the per-turn budget); requeues while the mailbox is non-empty — when the per-turn budget (max_reductions=1000 messages) is exhausted it resets turn_reductions and requeues at the back of the queue (yield); the turn budget also resets when the actor goes Waiting.
  3. Send: send_message_by_idMessagemailbox.push (always Ok, never drops) → ORCA send_ref_to bumps foreign_count → enqueue target.
  4. GC: process_gc_ops drains OrcaCoordinator → per-actor OrcaGc applies deltas; CycleDetector::incremental_detect (epoch-gated) builds foreign-ref graph, suspects by weight, DFS, trial-decrement, reclaims.
  5. Fault: exit_actorhandle_actor_exit → unregister + leave_all, DOWN to monitors, propagate to links (abnormal kills non-trapping; trapping gets System msg), Supervisor.handle_exitSupervisorAction (Restarted/Shutdown/Ignore/Escalate) with cascading shutdown.

Distribution

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::tickClusterAction): 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 contentPacket::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).

Key Directories

  • 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), plus integration_tests/ & stress_tests.rs (test-only). The legacy compiler.rs was removed — the pipeline is MIR-exclusive.
  • src/mir_wasm.rs — WASM backend: MIR→WASM compiler via wasm-encoder (behind wasm-backend feature). Emits .wasm modules 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 .wasm modules (behind wasm-backend feature). Configured with guard pages (4GiB reserv, 128MiB guard), Cranelift speed opts (inlining), and SIMD. Provides AOT compilation via wasmtime compile.
  • src/runtime/ — actor runtime: mod.rs (Runtime god-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-byte Huge threshold, exact-size free-list reuse, all blocks released on reset()/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-lsp language server (single mod.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_ai library name) with zero dependency on the core nulang crate. Optional; pulled in by the ai-runtime cargo feature. Modules: client.rs (async LlmClient trait + sync complete_sync bridge), request.rs/response.rs (provider-agnostic wire types), providers/ (ollama.rs, openai.rs), mock.rs, memory (memory.rs episodic, semantic_memory.rs, procedural_memory.rs), pipeline.rs + debate.rs + supervisor.rs (orchestration primitives, each with its own *Runtime trait), registry.rs (AiRuntimeRegistry for pipelines/debates + SupervisorTeamRegistry, both generic over the runtime traits so the core Runtime never appears in the crate), usage.rs (TokenBudget, estimated_cost, UsageSummary). Core imports everything through use nulang_ai::…; directly — there is no src/ai/ façade module.
  • src/runtime/ai_impls.rs — core-side impl 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 between nulang-ai and the actor Runtime.
  • src/runtime/agent.rs, src/runtime/llm.rs — remaining AI-runtime integration that legitimately needs Runtime internals: 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; LlmState owns the persistent nulang-llm worker 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 .wasm component binaries and executes actor lifecycle (init, handle_message, checkpoint) via Wasmtime. ComponentPool recycles 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 (magic NLCS, 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/nula package manager (MVP): manifest.rs (Nulang.toml via the toml crate), lockfile.rs (Nulang.lock with reg+ support), resolver.rs (local-path + git + registry deps, simple semver checks, topo order), commands.rs (nulang nula new|build|test|run|publish, dispatched from main.rs; build/test/run shell out to the current nulang executable).
  • src/registry/nula package registry: server.rs (HTTP server via nulang registry serve with Bearer auth), client.rs (HTTP client via ureq used by resolver and nula publish).
  • src/docgen.rs — documentation generator: scans .nula files for //////! comments, extracts fn/actor/type/workflow declarations, and emits docs/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 .nbc bytecode artifact format: CodeModule::to_nbc/from_nbc, BLAKE3 source_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.

Development Commands

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 structure

Runtime 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).

Code Conventions & Common Patterns

  • Naming: snake_case functions/methods/modules/files; PascalCase types/structs/enums and enum variants; SCREAMING_SNAKE_CASE consts (HOT_THRESHOLD, TAG_INT, PAYLOAD_MASK). nulang_ prefix on extern "C" JIT runtime helpers. __main is the synthetic function wrapping a top-level expression (parser + HIR lowering).
  • Error model: one project-wide NuError enum (src/types.rs:463) aliased NuResult<T> = Result<T, NuError>. Compile-time variants (LexError/ParseError/TypeError/EffectError/CapError/LinearTypeError) carry { msg: String, span: Span }; runtime variants (RuntimeError/VMError/PythonError) carry String. Display formats spanned errors as <Kind> at <line>:<col>: <msg>. First error aborts; no error collection/recovery. ? propagates. EffectChecker/CapabilityAnalyzer accumulate diagnostics: Vec<String> instead of failing fast. Runtime subsystems use per-domain enums (RegisterError, PgError) impl std::error::Error; persistence/network use io::Result/Option; JIT uses CompileError. No anyhow/thiserror.
  • Async: only main.rs (#[tokio::main]), src/lsp/, and the nulang-ai LLM 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. The LlmAsk opcode calls ActorVmCallbacks::llm_ask (default delegates to blocking complete_llm); BytecodeRuntimeCallbacks::llm_ask builds the LlmRequest on the scheduler thread, spawns a nulang-llm worker thread (own current-thread tokio runtime + block_on(client.complete(...)), result sent over Runtime.llm_tx), and returns Pending — the VM then decrements the PC and raises the "LlmAsk:suspend" sentinel error, captured onto actor.suspended_execution in run_bytecode_at_offset exactly like "SignalWait:suspend" and "ReceiveWait:suspend" (helper is_suspend_error). run_scheduler pumps completions (poll_llm_completionsstore_llm_completionresume_suspended_llm_step, which re-installs per-actor callbacks before vm.resume() and re-captures on chained suspends) and keeps running while llm_inflight_count > 0 (10ms recv_timeout wait 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 enables llm_suspend_enabled around vm.resume(), so a perform LLM.ask after the wait suspends non-blockingly and re-captures on either sentinel via is_suspend_error, with suspension_marker recording whether the actor awaits a signal or an LLM completion. Suspension is gated on Runtime.llm_suspend_enabled: step_actor enables it around the bytecode invocation; ask_actor_sync forces 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 *mut pointers with hand-written unsafe Send/Sync and SAFETY doc justifications (ORCA headers, foreign-ref ops, BytecodeRuntimeCallbacks). JIT function pointers obtained via unsafe transmute of *const u8; bytecode must not mutate during JIT execution. Python: GIL acquired via Python::attach; PythonObjectId is a non-owning Copy handle (real refcount in PythonRegistry); get_object acquires GIL before the registry Mutex to avoid lock-order deadlock.
  • Dependency injection / decoupling: the VM talks to the runtime through two object-safe callback traits (ActorVmCallbacks, DistributedVmCallbacks) — default StandaloneVmCallbacks owns a private ActorHeap. RuntimeVmCallbacks (Rc<RefCell<Runtime>>) and BytecodeRuntimeCallbacks (raw *mut Runtime) bridge the other direction.
  • State: actor state fields live in two parallel maps — state_data: HashMap<String, Value> and state_models: HashMap<String, StateModel> (src/runtime/actor.rs:26-27); each field's StateModel is Local/Durable/EventSourced/Crdt. Actor identity is a bare u64 (no Pid wrapper), from fresh_actor_id().
  • Spans: threaded into nearly every Expr/Decl variant and every compile-time error.
  • Type system: HM Algorithm W (Substitution = Vec<(TypeVar,Type)>, mgu + occurs check, generalize/instantiate over Type::Scheme); Pony-inspired Capability lattice (is_subtype_of via join, is_sendable) with LinearIso/Linear consumption tracking in CapabilityAnalyzer (src/effect_checker.rs): at-most-once (no double-use) is enforced for every binding; exactly-once (must-use) is enforced for let-bound linear values (2026-08-02, with a transparent-rebind exemption for bare let a = x aliases) and for values already bound in the initial context, e.g. function/lambda/behavior parameters whose capability is seeded into the CapContext (2026-08-07, extended to the FFI and LSP capability-analysis paths); Koka-inspired row-polymorphic EffectRow (Closed/Open with Region). Records are row-polymorphic too: an open record carries a row tail encoded as the reserved ".." pseudo-field holding a row Type::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_access in src/typechecker.rs), so fn(r) r.x + r.y generalizes over any record with x and y; closed records unify exactly (unify_closed_records — field-count mismatch is a type error, so fn(r: {x: Int}) rejects a 2-field literal).

Important Files

  • src/main.rs — CLI entry; hand-rolled arg parser; run_source/check_source pipeline; #[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-tagged Value, Frame, VM, step/run, effect handlers, JIT hook, callback traits.
  • src/mir_wasm.rs — WASM backend: MIR→.wasm compiler (behind wasm-backend feature). SIMD lowering framework.
  • src/wasm_runtime.rs — Wasmtime host runtime with guard pages, inlining, SIMD, AOT compilation.
  • src/runtime/mod.rsRuntime god-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/Tooling Preferences

  • Runtime: Rust stable, edition 2021. Linux/macOS (Windows planned).
  • Linker: GNU bfd forced on x86_64 Linux via .cargo/config.toml (not lld) for Cranelift/PyO3 compatibility.
  • Python: PyO3 0.29 abi3 limited-API; build.rs symlinks libpythonX.Y.so for Fedora.
  • Allocator: mimalloc (#[global_allocator] in main.rs).
  • Cargo features: default = ["python", "sqlite", "lsp", "ai-runtime"] (PyO3 interop, libsql/Turso persistence, tower-lsp server, AI runtime). wasm-backend is optional (off by default) — enables wasm-encoder WASM compiler, wasmtime host runtime, and --backend wasm|wasm-run|wasm-aot CLI modes. All features are optional; --no-default-features --features <subset> builds a leaner binary.
  • No external test/criterion/proptest crates — standard #[test] only.

Testing & QA

  • Framework: standard Rust #[test] + #[cfg(test)]. No proptest/quickcheck/criterion. No #[ignore]/#[should_panic]/async tests.
  • Organization: two styles — (a) inline mod tests at file foot (lexer.rs, parser.rs, typechecker.rs, effect_checker.rs, value_layout.rs, vm.rs, most runtime/*.rs, jit/*, python/*, ffi/*, lsp/mod.rs, plus every source file in crates/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 in nulang-ai. The suite covers src/integration_tests/mod.rs (end-to-end pipeline via run_source/assert_int/run_source_with_runtime plus 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 inline mod tests across lexer.rs, parser.rs, typechecker.rs, effect_checker.rs, value_layout.rs, vm.rs, runtime/*.rs, jit/*.rs, python/*, ffi/*, and every crates/nulang-ai/src/*.rs. Per-file breakdowns are too volatile to maintain manually — run cargo 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 --release for optimized runs.
  • Gate scripts: verify_implementation.py (forbidden-pattern scans for known anti-patterns — Box'd frames, string leaks, crdt_reg temp Vec, timer BinaryHeap rebuild, check-then-unwrap — + asserts JIT integration, escape-analysis deadness, scheduler-stats and cycle-detector wiring, then runs cargo test and cargo check --tests against a zero-warning baseline) and verify_report.py (validates codebase_analysis_report.md: required sections, ≥5 code snippets, referenced src/*.rs paths exist). Each exits 0 only on full pass.
  • Audit: .cargo/audit.toml ignores RUSTSEC-2026-0186 (memmap2 unsound; vulnerable APIs unused; upgrade blocked on cranelift-jit).

Known Hazards (for assistants)

  • The escape_analysis.rs module 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, all TAG_*, plus sext48/tag_int/tag_bool). src/vm.rs, src/jit/{runtime,compiler,typed_compiler,simd_compiler}.rs, and src/python/marshal.rs all import from it — do not reintroduce local copies. The one exception is TAG_PYTHON (0x7FF6), defined in src/python/bridge.rs and imported by marshal.rs; it was chosen not to collide with TAG_CLOSURE (0x7FF7) or TAG_STRING (0x7FFE).
  • Remote actor messages carry the behavior name on the wire (Packet::ActorMessage.behavior_name); the receiving node resolves it via Runtime::behavior_id_for against the target actor's behavior table on delivery (process_network_packets in src/runtime/distributed.rs), falling back to behavior id 0 for unknown names — mirroring local send_message's unwrap_or(0). String payloads cross the wire by content, never by pool id: Packet::ActorMessage.string_table carries the UTF-8 text, populated on send by distributed::resolve_wire_strings (from the current actor's module pool) and interned on delivery by distributed::intern_wire_strings into the target actor's module pool via VM::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 in SpawnRequest initial state (remotely-spawned actors have native handlers and no module pool).
  • Cluster membership entries carry their gossip version in the _incarnation metadata key on NodeInfo.metadata (absent = baseline 1 in gossip_payload, 0 in merge-compare). merge_membership only applies status/address changes on a strictly higher incarnation; equal incarnation only refreshes last_heartbeat. join_cluster seeds start at incarnation 1 so gossip can't clobber the authoritative seed address; handle_heartbeat bumps the entry incarnation on status promotions so they propagate.
  • LamportTime/LamportClock are defined in crdt.rs and imported by crdt_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 by verify_implementation.py).
  • The receive expression (receive { | Behavior(params) => expr }) is wired end-to-end with selective-receive dispatch. MIR lowering (lower_receive in src/mir_lower.rs) resolves arm behavior names to behavior-table indices (same suffix-match rule as send) and emits mir::RValue::ReceiveMatch (bytecode OpCode::ReceiveMatch 0x8F): a spec constant "max_params:id1,id2,..." carries the candidate behavior ids, the VM calls ActorVmCallbacks::try_receive_match(&ids) (mailbox scan in Mailbox::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-any Receive (nil when the mailbox is empty or outside an actor context) — non-blocking, no suspension. The timed form receive { arms } after ms => body instead emits OpCode::ReceiveWait (0xA0, same spec-constant/register contract as ReceiveMatch, 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 onto actor.suspended_execution like 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-executed ReceiveWait writes 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/spawn and the deferred receive-wait wake: send and spawn work from inside scheduler-driven bytecode behaviors — BytecodeRuntimeCallbacks::send_message calls Runtime::send_message_by_id, and BytecodeRuntimeCallbacks::spawn_actor shares Runtime::spawn_from_module with RuntimeVmCallbacks::spawn_actor. The one piece of send_message_by_id that is unsafe mid-behavior is the receive-wait wake hook: resume_suspended_receive_wait runs vm.resume() on the shared rt.vm, which would nest VM execution inside the sender's still-running behavior and clobber the shared frames. The runtime therefore tracks vm_execution_depth (vm_exec_begin/vm_exec_end wrap 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 onto Runtime.pending_receive_wakes instead of resuming; vm_exec_end drains the backlog when the outermost call returns, looping until empty because a resumed behavior can itself send and re-queue a wake (draining_receive_wakes keeps the drain iterative rather than recursive, and sets current_actor to the resumed actor around each resume so Message.sender is attributed correctly). vm_exec_end must run only AFTER any suspend-state capture (vm.take_suspended_state) on every path — a drained resume calls vm.restore_suspended_state, which would overwrite the frames an un-captured suspend still needs. Nested ask from behaviors remains unsupported (the BytecodeRuntimeCallbacks::ask_actor trait default returns nil).
  • BEAM fault-tolerance language surface: perform Actor.link/unlink/monitor/demonitor/trap_exit/exit/register/unregister/whereis/set_priority dispatch through ActorVmCallbacks::perform_builtin_effect (no user handler installed) into Runtime::perform_actor_builtin — both runtime callback impls (RuntimeVmCallbacks, BytecodeRuntimeCallbacks) reach it; the standalone VM nil-no-ops every Actor.* effect (matching the outside-an-actor contract). spawn link Actor {..} / spawn monitor Actor {..} are parser desugars to spawn + Actor.link/Actor.monitor on the spawner. Actor scheduling priority: Actor.priority: ActorPriority {High, Normal=default, Low} (set via Actor.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_match stays FIFO and ignores Message::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/FieldS retain a stored heap pointer and release the overwritten slot's old value (src/vm.rs); (2) OrcaGc::free_object releases slot references when a container is freed (src/runtime/gc.rs) — so every container slot MUST hold a counted reference, which is why the FieldS barrier is mandatory; (3) plan_drops in src/mir_codegen.rs emits OpCode::Drop at conservative liveness points, and the Drop handler clears the register to nil so duplicate drops are no-ops — removing the nil-clearing reintroduces double-decrement. OrcaHeader counts (ref_count/foreign_count/sticky) and GcStats are 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.payload is Arc<Vec<Value>>: payload values are shared via Arc to avoid cloning on every receive_match scan. The VM never mutates incoming payloads, so Arc is safe. Construct with Arc::new(vec![...]) — a bare vec![...] is a type error.
  • DST determinism: run_scheduler_deterministic_with_rng (with the seed-taking run_scheduler_deterministic wrapper) drives actor selection via pick_ready_actor_deterministic over 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_all every GC_PUMP_INTERVAL steps, process_gc_ops + deferred retry at quiescence — the DST path applies foreign-ref decrements exactly like the production run_scheduler's drained-queue drain). ClusterState::set_rng seeds gossip/repair picks (DeterministicRng implements rand_core::RngCore). Runtime::enable_distribution_with_transport lets tests run real Runtimes over the in-memory DeterministicNetworkTransport; 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 the NetworkTransport trait (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_buffer is VecDeque, not RefCell<VecDeque>: all methods that access the skip-buffer take &mut self because they run exclusively on the owning shard's scheduler thread. Mailbox carries unsafe impl Sync — the SegQueue fields are lock-free and the mutable skip_buffer is thread-confined.
  • TypeMetadata.regs is [KnownType; 256], not HashMap<usize, KnownType>: a flat array replaces the previous HashMap for deterministic O(1) access with no hashing overhead. KnownType::Unknown (the default) represents untyped registers. TypeMetadata::is_empty() returns true when every register is Unknown. infer_reg_types in src/jit/typed_compiler.rs and callers in src/jit/mod.rs/src/jit/tests.rs use .is_empty() instead of .reg_types.is_empty().
  • WASM backend (src/mir_wasm.rs): the wasm-backend feature enables wasm-encoder (WASM binary encoding) and wasmtime (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, so FUNC_IMPORT_COUNT (5) differs from the total import count (6). When adding new imports, remember to update both the import-order-preserving rebuild_imports() and the function-index constants (IMPORT_*). SIMD lowering in mir_wasm.rs uses raw byte emission via Function::raw() because wasm-encoder 0.220 lacks SIMD instruction variants in its Instruction enum. The unreachable after each block end is 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" uses default-features = false with cranelift, runtime, std, anyhow features. The Error type is wasmtime::Error (NOT anyhow::Error — they are distinct in wasmtime 46, though wasmtime::Error::msg() provides string construction). Host import callbacks use Caller<'_, HostState> via Linker::func_wrap. The nulang_init export has type () -> i64 (returns the tagged program result), NOT () -> (). AOT compilation (wasmtime compile) requires the wasmtime CLI tool installed on the build host.