This changelog is organized by stability tier (see
GOVERNANCE.md§2), not by release. The tier determines what may change and how. The crate version inCargo.tomlis the implementation version; the language version ([package.metadata] language-version, andLANGUAGE_VERSIONinsrc/format/constants.rs) is what this changelog tracks — it moves only on RFC-ratified change.
Language version: 1.0.0-frozen (since 2026-07-19; RFCs 0001, 0002).
Will never break. A change here is a new language and requires a new major version + migration.
- RFC 0001 — Format Stability. Established versioned, frozen binary
formats for durable artifacts and the wire protocol.
.nbcbytecode artifact format version 1 (magicNLBC, header withformat_version,language_version, BLAKE3source_hash). Codec:CodeModule::to_nbc/from_nbcinsrc/format/nbc.rs.- NUL0 wire protocol handshake version 1 (16-byte
{magic "NUL0", version u32, node_id u64}). Unknown versions are refused, never reinterpreted.src/runtime/network.rs. - Value layout version 1 (
src/value_layout.rs, i64-tagged). - Migration registry
src/format/migrate.rsas the sole legal home for format upgrades. v1→v1 identity. FormatErrorenum:Truncated,BadMagic,UnsupportedVersion,IncompatibleLanguage,LengthMismatch,UnknownOpcode,BodyDecode,BadConstant.
- RFC 0002 — Frozen Core. Defined Nulang Core, the minimal frozen subset:
fn/let/if/match/closures,Int/Bool/String/Unit/Nil/Vec/Map/tuples/records/enum, HM inference over this subset,IO.printandIO.readonly,valcapability only. Every Core program valid today is valid in every future version. - Stability contract published as
SPEC2.md§"Format Stability" andGOVERNANCE.md.
Breaking changes require an accepted RFC and a deprecation cycle of at least two major versions.
-
let rec f(x) = ... in ...works at module level. Recursive local bindings already parsed in expression position; module-level entry failed becauseparse_module_lethit the parameter list ("Expected =") with the parser already past thelettoken, blocking the expression fallback.parse_module_letnow rewinds toletwhen the name is followed by(. Pinned by parser + integration tests (PLAN doc-pass gap 3 closed). -
type X = <full type>accepts any alias body.type Buffer = [Int],type T = Int,type F = (Int) -> Int,type R = &ref Intnow parse as aliases (previously "Expected variant name, found ["); variants (Some(T) | None) and records are unchanged. Primitive type names lex asUpperIdent, so they are routed to the alias path too (PLAN doc-pass gap 5 closed). SPEC2 §4.5.1's stale bare-row shorthand prose removed — effect rows require braces;! Typeis the typed-error surface (PLAN doc-pass gap 4 closed, doc side). -
Parameter-level LinearIso must-use verified end-to-end. 5 new conformance cases (cap_30–34) prove exactly-once enforcement for
linearisofunction and behavior parameters through the compiled binary: single use ok, double use rejected, never used rejected, explicitconsume xdischarge ok, behavior-param consume ok. Conformance suite: 305/305. -
Parser fix:
Nil-led sum types. The gap-5 type-declaration routing sentNil(a primitive type name) to the alias path, breakingtype Stream[T] = Nil | Cons(...)—Nilis the canonical empty variant of a sum type, not a degenerate alias body. Newtype_decl_body_is_aliasexempts it; pinned by a parser unit test and the generics_07/typeclass_08 conformance cases. -
Conformance contract updates: generics_08's stderr assertion no longer depends on internal fresh-type-var numbering; workflow_09/11 expect the deliberate post-2d56e33 contract (failing saga steps surface a diagnostic and exit nonzero, compensation trace unchanged).
-
LSP protocol-level integration tests. 6 tests drive the full JSON-RPC dispatch path (
tower_lsp::LspServicewith realRequestobjects — the same service the stdio server runs), closing the "no protocol-level integration tests" gap: initialize capability round-trip, publishDiagnostics pushed on didOpen/didChange (empty for well-formed docs, parse-error diagnostics for broken ones), hover signature, completion keywords, documentSymbol outline, and the shutdown/exit lifecycle (requests after exit fail with ExitedError). New direct depsfutures/tower-service(both already in the lockfile) behind thelspfeature. -
Message-reorder DST scenario.
NetworkTransportgainsset_reorder/flush_held(default no-ops); the deterministic transport delivers consecutive packets to a peer swapped (bounded adjacent reorder — nothing lost or duplicated). New 25-seed sweep: three nodes form the cluster under reordered heartbeats/gossip/acks, a 30-message remote burst delivers exactly 30 (AtMostOnce), and GCounter replicas converge under reordered delta sync. -
GC-during-send DST scenario. The deterministic scheduler now pumps GC on the production cadence (deferred frees mid-run, foreign-ref decrements + deferred retry at quiescence) — the DST path previously never applied
process_gc_ops, so heap-churn scenarios could not run. New 60-seed sweep: nested heap-array trees sent across actors with in-flight foreign bumps, receiver holds, deferred frees, and seed-permuted GC interleavings; every seed delivers intact contents with exactly the held set of live objects (no premature free, no leak). -
Node-crash DST scenario.
DeterministicCluster::crash_node/restart_nodemodel a hard crash + fresh-node restart (skipped from the pump, links cut, Runtime replaced with the same node id). New 20-seed sweep: survivors mark the crashed nodeFailedthrough the real virtual-clock failure detector, the restarted node rejoins through a survivor, the cluster reconverges, and a remote message delivers to an actor on the restarted node — the seed-sweepable, sleep-free counterpart of the real-TCP crash/rejoin test. -
CRDT-sync-race DST scenario.
DeterministicClusternow drivesrt.sync_crdts()per round (the harness models a Rust embedder — CRDT replication stays an embedder API per SPEC2 §12.5, deliberately not auto-driven by the production loop). New 40-seed sweep: a GCounter minted on node A must appear on node B via the round-1 full-state sync, both nodes increment local replicas under seed-permuted interleavings, and both replicas converge to the summed total on every seed (no lost update). -
DST seed sweeps are env-scalable; nightly 10⁴-seed job wired.
src/dst.rs::dst_seed_countreadsNULANG_DST_SEEDS(defaults: 2000 single-node, 50 cluster, 30 cross-shard in-suite)..github/workflows/dst-nightly.ymlruns the sweeps at 10⁴ seeds on a nightly schedule + manual dispatch, failing loudly on any invariant violation (quiescence, AtMostOnce delivery, cluster convergence) — the PLAN.md Phase 1 bullet 2 "10⁴-seeds-per-commit" deliverable. -
Cluster/network determinism (DST). The deterministic harness now drives multi-node clusters of real
Runtimes with no wall-clock reads affecting state:Runtime::enable_distribution_with_transportaccepts any transport (the in-memoryDeterministicNetworkTransportfor tests);ClusterState::set_rngseeds gossip/repair picks; the deterministic scheduler drains cross-shard channels and takes a caller-owned RNG (run_scheduler_deterministic_with_rng); heartbeat wire timestamps come from the virtual clock when one is installed.DeterministicCluster(test-gatedsrc/runtime/cluster_dst.rs) pumps N nodes with lockstep virtual clocks and one seed-permuted node order. New tests: same-seed bit-reproducible evolution, 50-seed remote AtMostOnce delivery sweep, 3-node partition→Failed→heal→deliver through the real failure detector, 30-seed cross-shard delivery sweep. PLAN.md Phase 1 bullet 2 (DST) cluster/network determinism closed. -
Value-level capability constructors for every reference capability.
&cap exprnow constructs a reference with the requested capability for all eight capabilities (&iso,&trn,&val,&box,&tag,&ref,&lineariso,&linear); bare&exprremains&ref(backward compatible). Previously&expralways produced arefreference while&iso T/&val T/&trn T/&box Twere accepted in annotations only — the capability system's biggest missing surface (SPEC2 §3.9, PLAN.md "Gaps found by the doc pass" item 1). Semantics: the unique constructors (&lineariso,&linear,&iso,&trn) move a bare-variable operand exactly likeconsume x— a second&iso xon the same binding is a capability error — while the shared constructors (&ref,&val,&box,&tag) alias without consuming. Capabilities are compile-time only, so every constructor erases to a plain value move at runtime (OpCode::Move). The formatter now prints&cap(previously every&-expression formatted asref, breaking round-trips). Pinned by parser/analyzer/integration tests and differential-corpus entries.
-
lineariso/linearcapability annotations now parse. The lexer emits dedicatedLinearIso/Lineartokens, butparse_capability(the:capannotation path,src/parser.rs) only matched them as identifiers, so:cap linearisoand:cap linearalways failed to parse. Fixed by matching the dedicated tokens directly; pinned by a Rust regression test and 8 conformance cases. (The parameter-capability path had the correct match all along.) -
Conformance suite reached 300 behavior cases (Phase 1 acceptance criterion).
conformance/run.pyis green 300/300, including two new cases pinning the capability downgrade lattice (cap_22iso→trn→ref,cap_23trn→val). -
WASM effect-dispatch ABI:
nulang_dispatchreturns the effect-result length. The host import is now(i32,i32,i32,i32) -> i64(bytes of the effect result written to the ring buffer; 0 = no result), mirroringio_read's length-return contract so a guest lowering can read the result back from linear memory. Mirrors the pool host side inwasmtime-actor-pooland the parallelwasmfxbackend. No compiler lowering emits the call yet — effects other than IO.print/println/read andArray.lengthare still rejected at compile time. -
Single-argument
perform Timer.sleep(ms)in a workflow step no longer hangs. The step used to suspend forever (only the two-argument durable formTimer.sleep(name, ms)worked). The timer-wheel wake now resumes the suspendedPerformAsyncwith a full VM resume, and the completion bookkeeping (step_index advance,StepCompletedevent, checkpoint) runs exactly like the signal-wait/LLM resume paths. The resume distinguishes completion from re-suspension by the VM result, nottake_suspended_state— that accessor returns the completed frame state after a normal finish, so a blind re-capture re-stalled the actor (the residual hang this fix closes). Pinned bytest_workflow_timer_sleep_single_arg_resumes. -
Constrained generic functions with typeclass bounds work on type-variable receivers.
fn eq_check[T: Eq](a: T, b: T) -> Bool { a.eq(b) }used to type-check and then crash at runtime ("Not a function: nil") — the dictionary transform only resolved literal receivers. The HIR now resolves the dictionary forDictKind::Paramreceivers and the call site passes the concrete dictionary argument. Pinned byconformance/behavior/typeclass_06_constrained_generic_runtime_crash.nula. -
Recursive generic ADTs construct correctly, and generic type parameters are skolemized in the function body. §7.8's
type Tree[T] = Leaf | Node((Tree[T], T, Tree[T]))(and a second recursive shape) type-check their own constructor calls, and a body that pins its declared type parameter to a concrete type (fn fresh[T]() -> T { 0 - 1 }) is now rejected at the definition instead of failing later at a mismatched call site. Pinned byconformance/behavior/generics_03/07/08_*.nula. -
event_sourcedfields with non-trivialapplyhandlers survive crash + recovery.emit_eventpersists the field's post-apply value (apply runs inline before the snapshot) andrecover_actorrestores it — recovery no longer reconstructs a bare event count that silently dropsapply's contributions. Pinned bytest_event_sourced_apply_handler_recovery. -
Prelude types are now usable in type annotations.
Ok(42)andSome(x)type-checked in every module, butfn f(x: Option[Int])failed to parse with "Unknown type name" — the prelude's type declarations are prepended to the AST only after the user module parses, so the parser never saw them. EveryParsernow seeds the prelude's resolvedOption[T]/Result[Ok, Err]into its imported-type cache (the same pathimport stdlib::*uses); localtype Option[T]declarations still shadow. Pinned bytest_prelude_types_resolve_in_annotationsandtest_local_type_shadows_prelude_in_annotation. -
Doc-example verification is fully green and covers
///doc comments. The defaultverify_doc_examples.shCI invocation was red: 16 docs-site blocks taught invalid syntax (pre-thenifblocks;Err(e)on a prelude whose constructor isError(e); recursive ADT payloads written as bareList[T]variant args where the parser requires a tuple payloadCons((T, List[T])); an unclosed fence inindex.mdx; andsend x get(self), which is untypeable because arefcapability is not sendable — rewritten assend selffrom inside a behavior). All rewritten against the current compiler. The script now also verifies every```nulangblock inside///doc comments of.nulasources, pinned with a runnable round-trip example insrc/stdlib/json.nula'sparsedocs. Default run: 54 passed / 0 failed / 0 skipped. -
A failing workflow step is no longer silent. Previously a step error (e.g. a non-exhaustive match) produced no diagnostic at all — no stderr, exit 0 — only a difference in which compensations ran revealed it. The runtime now records a durable
WorkflowEvent::StepFailed(with the step name and error message) alongside saga compensation, and the CLI printsworkflow step '<name>' failed: <error>to stderr and exits nonzero. Pinned bytest_workflow_step_failure_is_recorded_and_surfaced. -
Saga compensation and workflow step dispatch no longer shift when a plain
actoris declared before aworkflowin the same module. Compensation pairs now carry the step's absolute (whole-module) behavior index, and a workflow actor'sbytecode_offsetsare compressed to its own steps (local ids 0..step_count-1, matchinglayout_workflow_behavior_table) instead of the module's full list — previously the first step ran the preceding actor's first behavior and its compensation was patched onto the wrong behavior, silently. Pinned bytest_saga_compensation_ignores_non_workflow_actors. -
spawn@nodereferences route cross-node by bare actor-ref value. Actor-ref Values carry only a 48-bit id (no node), so a remote-spawn handle used to fall into the local mailbox path — messages were silently misdelivered/dropped andask remotehardcoded the local node. The runtime now keeps a bare id → node reverse index (populated at remote spawn, on SpawnResponse, on wire sends, and on inbound messages for reply-by-ref), andsend/askon any known remote ref routes over the wire. Messages sent to a spawn@node placeholder before its SpawnResponse arrives are queued in wire form and flushed to the real actor id; the placeholder value keeps routing even aftertake_spawn_responseconsumes the response. Local actors win on id collision (fresh_actor_idstarts at 1 on every node), andRAsknow accepts actor-ref targets and stages behavior args like the localAskopcode (both were broken). Pinned by three cross-node TCP tests plus a strengthened RAsk unit test. -
send remote/ask remotenow fall back to local delivery single-node instead of silently dropping messages, andask remotereturns the callback's value. The distribution wrapper resolves a remote address to local delivery when the node is local or the transport is unwired, andRAskuses the same result-register convention as the localAskopcode (previously a register-write mismatch returned the wrong value). Pinned bytest_distributed_remote_address_local_fallbackand the strengthenedtest_distributed_callbacks_invoked(which now asserts the RAsk result value). Cross-node routing ofspawn@nodereferences is the companion change above (2026-08-13); the node id is not carried in actor-ref values, so routing goes through the runtime's reverse index.
The following are classified Stable as of 1.0.0-frozen. They have not changed in this version; they are recorded here to establish their tier.
- The full HM type system and inference rules (
src/typechecker.rs). - The effect-row system: closed/open rows, regions (
src/effect_checker.rs). - The capability lattice (
iso/trn/ref/val/box/tag/lineariso) and subtyping (src/effect_checker.rs). - The actor surface:
spawn,send,receive, supervision (src/runtime/,src/vm.rs). CRDT operations and merge semantics— correction (2026-08-02): this was misclassified.src/runtime/crdt.rs/crdt_reg.rsimplement a real, Rust-level-tested delta-sync CRDT protocol (CrdtManager, 8 types), but it has no.nula-observable surface: no type-selector syntax, noCrdt.*effect module, and thestate crdtfield tag (SPEC2.md§9.10/§12.5) does not route through it — see those sections' implementation-status notes. Nothing Stable-tier is broken by this correction because there was never a language-level contract to keep; the Rust API itself remains internally stable, it just isn't a GOVERNANCE.md-tiered language surface until an RFC wires it up.
-
RFC 0005/0007 —
entitykeyword and event sourcing.entitydesugars topersistent actorwithevent_sourceddefault state model.eventsandapplyblocks for typed event declarations and automatic state mutation.emit EventName(args)type-checked against entity event declarations.after ms => exprstandalone sugar. Entity events validated at compile time; unknown events produce type errors. -
RFC 0008 — Migration contracts.
version: Nandmigration from N to M { ... }blocks parsed inside entity declarations. AST/HIR/bytecode metadata wired through pipeline. Migration state bodies and event-migration handlers are now type-checked. -
RFC 0009 — Organization primitives.
organizationkeyword parsed and desugared toentitywith durable defaults.is_organizationflag tracked through AST → HIR → bytecode. -
RFC 0003 Item 6 — Backend trait boundary.
JitBackend,WasmBackend,CryptoProvider,ForeignInterop,HttpProvidertraits defined insrc/backends/mod.rs. JIT and WASM wired behind traits. -
MIR register spilling. Functions with more locals than fit in the register file (238 usable registers) now spill excess locals into a frame-local
Vec<Value>viaSpillLoad/SpillStoreopcodes (0xF5/0xF6). Fix (2026-07-24): replaced post-processing spill rewrite with inline SpillLoad/SpillStore emission during codegen, removing the 17-slot capacity limit entirely. Round-robin temp register allocation (r12/r13/r14) prevents clobbering in multi-operand spilled reads. Net -112 lines. Unblocks the self-hosting bootstrap compiler (RFC 0003 Item 3). -
Self-hosting bootstrap: Stage 5 (closures with env capture). The
bootstrap/compiler_core.nulaPratt evaluator now supportsfn(x) => bodylambdas, function applicationf(arg), and environment capture (let a = 3 in (fn(x) => a + x)(5)→ 8). Closure encoding: 30-bit flag with packed param-hash, body-start, and captured binding. Out-of-band sentinel1 << 40distinguishes "no left operand" from value 0. -
Formal semantics: all three Core theorems proved.correction (2026-08-02): false as of today. This was true at this commit, but the very next commit (ac9ef5d, 2026-07-26 — Lean 4.16.0 compatibility fix) honestly disclosed in its own message that it reverted 9 theorem bodies tosorry(a custom recursorweakeningdepended on broke under the newer toolchain); no doc was ever updated to match. Current state: onlycanonical_formsis proved intypes.lean—progress,preservation, andtype_soundness(the three headline claims) are allsorry. The capability lattice proofs (capabilities.lean) genuinely are proved (5 theorems); onlylinear_at_most_oncethere issorry.effects.lean's two theorems are vacuousTruestubs, not proofs. Seespec/formal/README.mdfor the corrected, per-theorem scope table. -
RFC 0013 — Authenticated, encrypted transport (2026-08-05).
TlsConfigenum (MutualTls/SelfSigned/PlaintextInsecure) replaces the opt-inOption<TlsConfig>. MutualTLS nodes present certificates signed by a cluster CA, verify peer certificates, and derive node identity from the certificate's BLAKE3 fingerprint instead of the spoofable socket-address hash.server_namefield for configurable TLS SNI. Short read timeout (50ms) withWouldBlockretry enables concurrent read/write over TLS connections, so heartbeats and gossip flow. Integration tests cover connection, cert mismatch rejection, plaintext-mTLS interop rejection, and two-node cluster convergence. NUL0 wire protocol unchanged (version 1).src/runtime/network.rs,src/runtime/cluster.rs,src/runtime/tests.rs. -
Error handling syntax.
catch expr => body,fail expr(structured short-circuit return), andT ! Ereturn-type syntax (fn div(a: Int, b: Int) -> Int ! String). Errors propagate through?operator —expr?is sugar forcatch expr => |e| fail e. Desugaring, type inference, and codegen wired insrc/parser.rs,src/typechecker.rs,src/hir_lower.rs,src/mir_lower.rs, andsrc/mir_codegen.rs. -
Transport resilience.
send remoteandask remotekeywords enforce network-sendable (val/tag) capability constraints at the call site.ask remote actor behavior(args) timeout Naccepts an optionaltimeoutclause for request-response with deadline semantics. Capability enforcement lives insrc/effect_checker.rs; transport modifiers parsed insrc/parser.rs. -
RFC 0010 — 100-Year Language Architecture. Documented design rationale for multi-century relevance. Deliverables implemented:
- LLM→Inference effect alias:
perform LLM.ask(p)andperform Inference.ask(p)are synonyms; both resolve toEffect::Inference. TheLLM.asksurface is a deprecated alias (src/effect_checker.rs,src/mir_lower.rs,src/runtime/mod.rs,src/stdlib.rs). - Keyword lifecycle governance:
GOVERNANCE.md§2a defines keyword introduction, reservation, deprecation, and removal rules. - Keyword namespace cleanup: Five formerly-reserved keywords
(
where,priv,loop,node,subworkflow) removed from the lexer and now lex as plain identifiers.awaitre-reserved (July 2026) for future async/await support (src/lexer.rs). - Keyword inventory documented in
SPEC2.md§Implementation Status and verified against the implementation.
- LLM→Inference effect alias:
-
AI façade removal (2026-08-02). Deleted the
src/ai/façade module (mod.rsre-exports +runtime_impls.rs) so the crate boundary is visible at every callsite. Core now imports directly throughuse nulang_ai::…;, never throughcrate::ai::.AiRuntimeRegistry(pipelines + debates) andSupervisorTeamRegistrymoved fromsrc/runtime/{ai_registry,supervisor_registry}.rstocrates/nulang-ai/src/registry.rs;SupervisorTeamRegistry::rungains a trait-generic signature (R: SupervisorRuntime) matchingAiRuntimeRegistry::run_pipeline. Trait impls forRuntimemove tosrc/runtime/ai_impls.rswhere the orphan rule requires them.LlmStatestays insrc/runtime/llm.rsbecause it is executor infrastructure (persistent worker thread + channels polled by the scheduler), not a library type. Net effect:src/ai/no longer exists; core importsnulang_ai::directly; the two-crate split is explicit at every callsite. No behavior change. -
ai-runtimefeature: the AI runtime — pure types live in thenulang-aiworkspace crate (crates/nulang-ai/) with zero dependencies on the core language crate. Core imports them directly viause nulang_ai::…;behind#[cfg(feature = "ai-runtime")]— there is no façade module. All AI effects dispatch through the genericPerformAsyncopcode (0xC6) witheffect_opstrings ("Inference.ask","Pipeline.run", etc.). The monolithic AI opcode range (0x9D–0xC5:LlmAsk,PipelineNew…DebateRun) has been removed. Runtime integration lives insrc/runtime/ai_impls.rs(trait impls forRuntime),src/runtime/agent.rs(agent LLM completion pipeline), andsrc/runtime/llm.rs(LlmStateworker thread). Behind--features ai-runtime(enabled by default).LLM.askis a deprecated alias forInference.askand emits a compiler warning (RFC 0010). -
pythonfeature: PyO3 interop (src/python/). Behind--features python. -
sqlitefeature: libsql/Turso persistence. Behind--features sqlite. -
lspfeature: the tower-lsp language server (src/lsp/). Behind--features lsp. -
ai-runtimefeature: the AI runtime (crates/nulang-ai/workspace crate, imported directly throughuse nulang_ai::…;— no façade module) — LLM providers (OpenAI, Ollama), pipelines, debates, supervisor teams, memory subsystems, and usage tracking. Behind--features ai-runtime(enabled by default). Changed in 1.0.0-frozen: all AI effects now dispatch through the genericPerformAsyncopcode (0xC6) witheffect_opstrings ("Inference.ask","Pipeline.run", etc.). The dedicatedLlmAskopcode and thePipelineNew…DebateRunopcode range (0x9D–0xC5) have been removed. AI types live in thenulang-aicrate with zero core dependencies; the coreActorVmCallbackstrait no longer carries AI-specific methods. TheLLMeffect redirects toProvider.askunder the hood. -
AOT native backend (
src/aot/), JIT tiering (src/jit/). -
Stdlib modules. Standard library modules provide reusable generic data structures and operations:
stdlib::core(base utilities),stdlib::list(map/filter/fold/reverse),stdlib::string(split/join/trim/replace),stdlib::set(add/remove/contains/union/ intersect),stdlib::map(insert/get/remove/keys/values), andstdlib::http(get/post request builders). Modules live undersrc/stdlib/and are resolved viaNULANG_STDLIB, the executable- relative path, or the dev-fallbacksrc/stdlib/. -
Typeclass declarations (Phase 4).
classandimplkeyword support:class Eq[T] { fn eq(self: T, other: T) -> Bool }declares a typeclass with optional superclasses (class Ord[T]: Eq).impl Eq Int { fn eq(self: Int, other: Int) = self == other }registers a concrete instance. Class/instance tables inTypeChecker(src/typechecker.rs). Typechecker integration (dictionary-passing transform): method calls on concrete types (1.eq(2)) resolve through the instance table and type-check against the impl dictionary; missing instances ("hi".eq("there")with noimpl Eq String) produce compile-time errors. HIR lowering for runtime dictionary construction is implemented:Decl::Impllowers tohir::Decl::Constant, producing a module-level function that evaluates to a record of method closures. Field access routing through the dictionary at call sites is implemented: method calls on concrete types (1.eq(1)) lower to dict-constant calls, field accesses, and method invocations at the HIR level, producing correct runtime results. Full end-to-end verified with integration tests. -
RFC 0003 — Content-addressed functions. Proposal document (
RFC/0003-content-addressing.md): defines a deterministic content-hash-based code identity scheme for distributed code deployment, cache invalidation, and reproducible builds across heterogeneous Nulang runtimes. Status: Draft. Content hashing infrastructure (BLAKE3source_hashin.nbcartifacts) is available per RFC 0001; full code-identity registry and content-addressed deployment are not yet implemented. -
::import resolution. Module imports now support::-delimited paths:import stdlib::set,import mypkg::utils::math. The resolver (src/resolver.rs) mapsstdlib::*prefixes to the standard library directory and general::paths to filesystem-relative module files.
-
Triple-quoted strings and
\u{...}escapes. Triple-quoted multi-line strings (\"\"\"...\"\"\") and\u{...}unicode escape sequences implemented. Triple-quoted strings support standard escapes; interpolation is unsupported. Surrogate and out-of-range code points are rejected with aLexError. Implementation:src/lexer.rs. (Stable) -
**exponentiation operator. Right-associative, precedence above*(Pratt level 13), tokenized asStar2. Wired through the full pipeline: lexer (src/lexer.rs), parser (PrattPREC_EXP), typechecker, HIR lowering, and bytecode.a ** b ** cparses asa ** (b ** c). -
Structured error messages.
NuErrorenum insrc/types.rswith per-variantexpected/foundfields,ErrorCodeclassification, automatic fix suggestions (suggestion()), andformat_rich()for colorized multi-line diagnostics with source excerpts and carets. Constructor helpers (type_mismatch,missing_effect, etc.) produce rich errors with minimal boilerplate at each call site. -
Language correctness fixes (all Stable,
src/):- Let-chain stack overflow: long chains of consecutive
letbindings are now flattened iteratively in the parser (sequentiallet-statement peeling) and HIR lowering (lower_let_chain), eliminating deep-recursion overflow on blocks with 40+ lets (src/parser.rs,src/hir_lower.rs). - Spawn field-initializer overrides:
spawn A { f = v }now correctly overrides the actor's declared default for fieldf. Overrides are encoded in bytecode (spawn_init_overridesinCodeModule) and applied at VM spawn time, replacing any matching default (src/vm.rs,src/mir_codegen.rs,src/bytecode.rs). Backward-compatible: older.nbcartifacts missing the field deserialize with an empty vec viaserde(default)(src/format/nbc.rs). - Clearer immutable-binding error: the type error for reassigning a
letbinding ("cannot assign to immutable binding 'x'; mutable locals (var) are not yet supported. Use 'let x = <new value> in ...' to shadow the binding.") now explains the constraint and suggests the shadowing workaround (src/typechecker.rs). - Prefix
catchsyntax:catch expr fallbackis now accepted in addition to the postfix formexpr catch fallback; desugars identically (src/parser.rs).
- Let-chain stack overflow: long chains of consecutive
-
Package manager subcommands (Experimental,
src/package/commands.rs):nula init(scaffold a package withNulang.toml,src/main.nula,.gitignore),nula list(print locked dependencies),nula clean(remove.nbcbuild artifacts),nula add <name> [--path|--git|--version](add/update a dependency and re-resolve the lockfile),nula remove <name>(remove a dependency and update the lockfile),nula run --watch/nula watch(build, run, and re-run on source changes via mtime polling), andnula doc [--open](generate Markdown API docs from doc comments and declarations). -
REPL enhancements (Experimental,
src/repl.rs)::help <topic>(topics: syntax, types, actors, effects, commands),:load <file>(load and evaluate a.nulafile),:type <expr>(show the inferred type without evaluating), tab completion (identifiers, keywords, REPL commands, stdlib modules), and automatic multi-line input when braces/parens/brackets are unclosed (prompt changes to....). -
New stdlib modules (Experimental,
src/stdlib/):result: Result combinators (unwrap,map,flat_map). TheResulttype (Ok(T) | Error(E)) is defined instdlib::core(auto-loaded).option: Option combinators. TheOptiontype (Some(T) | None) is defined instdlib::core.datetime:DateTimerecord type with calendar fields.math: trigonometry (sin,cos,tan,asin,acos,atan,atan2), logarithms (ln,log2,log10), power/root (pow,sqrt), rounding (ceil,floor,round,trunc), constants (PI,E).fs: wrapper functions around theFSbuilt-in effect (see below).test: assertion helpers powered by theTestbuilt-in effect (see below).
-
FSfilesystem effect (Experimental). Built-in effect wired into the standalone VM:perform FS.read(path) -> String,perform FS.write(path, content) -> Unit,perform FS.append(path, content) -> Unit,perform FS.exists(path) -> Bool. Effect-aware type signatures (! {FS}) are enforced. Declared insrc/stdlib.rs; wrapper functions insrc/stdlib/fs.nula. -
Testassertion effect +nula testrunner (Experimental).perform Test.assert(cond, msg),perform Test.assert_eq(a, b),perform Test.assert_true(cond), andfail_with(message). The test runner (nula test [--filter <substr>]) discovers.nulatest files under the package'stests/directory, executes each, and reports pass/fail counts with optional name filtering (src/stdlib/test.nula,src/package/commands.rs). -
LSP enhancements (Experimental,
src/lsp/mod.rs):.and::completion trigger characters for automatic invocation, field-access completion (onself.fields, record fields, and actor state),textDocument/didSavehandler that re-checks the file on save, and completion items sorted by category (locals > functions > types > variantskeywords > effects) via
sort_textprefixes. -
Example programs. 15 verified, runnable example programs under
examples/withexamples/README.md: from basic IO and arithmetic through functions, pattern matching, records, higher-order functions, algebraic effects, actors, loops, the pipe operator, arrays, JSON parsing, HTTP requests, Option/Result combinators, and range expressions. -
varbindings (Experimental). Mutable local variables viavar x = 0(declaration) andx = x + 1(reassignment).varbindings are tracked separately fromletin the typechecker and codegen, producingStoreandLoadbytecode ops for mutation —src/parser.rs,src/typechecker.rs,src/mir_codegen.rs. -
Record-update syntax (Experimental).
{ base .. field = value }creates a new record with overridden fields. The..is parsed withPREC_RANGEprecedence; the parser disambiguates record-update from range-in-block by checking for=after the right operand —src/parser.rs. -
Tuple field access (Stable). Numeric indices on tuples:
t.0,t.1. Chained access (t.0.1) works directly on nested tuples without parenthesization —src/parser.rs,src/hir_lower.rs. -
Range expressions (Experimental).
a .. bproduces an inclusive- exclusive range atPREC_RANGEprecedence (level 3, between pipe and logical-or). Ranges work inforloops (for i in 0 .. 5 { … }) and can appear bare in blocks ({ a .. b }) —src/parser.rs. -
Language correctness fixes (all Stable,
src/):else-on-newline: anelsekeyword following a newline after}is now accepted inif/elsechains —src/parser.rs.String.+fix for variables:a + bwhere both operands arelet-bound string variables now correctly concatenates instead of returning0—src/vm.rs.let..inscoping fix: block-levellet x = V in BODYnow correctly scopesxtoBODYonly, not to the remainder of the enclosing block —src/hir_lower.rs.
-
String.from_char(Stable).perform String.from_char(code)creates a single-character string from a Unicode code point; returnsnilfor invalid code points (surrogates, out of range) —src/stdlib.rs,src/vm.rs. -
Httpbuiltin effect (Experimental).perform Http.get(url)andperform Http.post(url, body)wired into the standalone VM viaureq. Returns the response body as aStringon success,nilon error —src/stdlib.rs,src/vm.rs. -
Arraybuiltin effect (Experimental).perform Array.length(arr),perform Array.push(arr, elem),perform Array.new(n, init),perform Array.set(arr, idx, val), andperform Array.slice(arr, start, end)wired into the standalone VM with value semantics (all return new arrays) —src/stdlib.rs,src/vm.rs. -
Numeric conversion primitives (Experimental).
Int.to_float,Float.to_int(truncates toward zero),Float.to_string,String.to_int(returns 0 for invalid input), andString.to_float(returns 0.0 for invalid input) —src/stdlib.rs,src/vm.rs. -
JSON parser (Experimental). Pure-Nulang recursive-descent JSON parser in
stdlib::json:parse(json: String) -> JsonValuehandles all JSON value types with proper escape processing, andstringify(value: JsonValue) -> Stringproduces valid JSON output. UsesString.to_float,Float.to_string,String.from_char, andArray.*primitives —src/stdlib/json.nula. -
All 13 stdlib modules functional (Experimental).
core,list,string,set,map,test,fs,option,result,datetime,math,json, andhttpall parse, import, and resolve correctly with all VM primitives available —src/stdlib/. -
LSP: code lenses, document links, enriched hover (Experimental,
src/lsp/mod.rs):textDocument/codeLensshows reference counts above function/actor declarations;textDocument/documentLinkcreates clickable links fromimportstatements to resolved module files;textDocument/hovernow includes doc comments (extracted from preceding///lines), effects, and formatted type signatures. -
LSP: completion documentation (Experimental,
src/lsp/mod.rs): keyword and built-in effect completion items now carry markdown documentation strings with code examples in theirdocumentationfield. -
Bootstrap: curried closure capture (Experimental,
bootstrap/compile_hex.nula): The bootstrap bytecode compiler now correctly compiles curried functions with closure capture —(fn(a) => fn(b) => a + b)(1)(2)→ 3. Fixed swapped CapStore/CapLoad opcodes at body start and fn_end, added missing Move for captured parameter at definition time, and corrected the environment register mapping from the raw capture register to r11.
-
Http.serveworks in the standalone (actor-free) VM (Stable,Httpeffect): previously only the runtime-backed callbacks handledserve, so an actor-free program (nulang file.nulawith no actor decl) got "Unhandled effect".StandaloneVmCallbacksnow dispatchesHttp.servewith the handler's module + function-table index, bindingHttpServerStatedirectly; the server is leaked so it keeps serving for the process lifetime. Regression testtest_http_serve_standaloneproves an actor-free program binds a port and serves a request end-to-end. Note: a pure standaloneHttp.serveprogram still exits whenmainreturns (the process dies), so run it from a runtime-backed or blocking program. -
nula new --templatelibrary grows to 7 templates (Experimental, package manager): addsdistributed(spawn + message-passing worker actors),ai-agent(actor backed byperform Inference.ask), andweb(HTTP client viaHttp.get/Http.postwith JSON). Each validated end-to-end vianula new→nula run. The plannedHttp.serve-based server template is deferred pending a CLI dispatch fix (see PLAN.md Phase 4 D6). -
RFC 0014 — durable-actor re-spawn on node failure (Draft): design for PLAN.md Phase 5 deliverable 7 part (c). Specifies the confirmed-gone gate (
Removedmembership state via positiveNodeGoodbyeor majority-gated timeout promotion), a gossip-replicated durable-actor location directory with epoch-based self-demote (no two live copies), snapshot replication to a deterministic shadow node atcheckpoint_actor, the newRestartPolicy::RespawnOnNodeLosssupervisor policy, and reuse of the existingPacket::MigrateActortransport. Implementation pending; deliberately not included: Raft/consensus (standing deferral) and silent automatic re-spawn without an explicit policy. -
Node-death recovery (Stable, distributed runtime): when the failure detector declares a peer node
Failed, the local runtime now invalidates that node'sRemoteActorCacheentries (sends fail fast instead of stale-resolving) and deliversDOWN-with-noconnectionsystem messages to every local actor that had linked or monitored an actor on the dead node. NewExitReason::NoConnection(wire tagnoconnection, DOWN payload code 6) distinguishes node loss from a crash. InboundPacket::Link/Monitornow register remote watchers and inboundPacket::Downdelivers DOWN to local watchers (previously dropped). Supervisor-policy re-spawn of durable actors on another node remains intentionally unimplemented pending the old-node-confirmed-gone gate. -
Formatter completeness (Experimental,
src/fmt.rs):nulang fmtnow formats everyDecl/Exprconstruct instead of refusing files containingworkflow,agent,class,impl,let-binding,given,effect,module,import,extern,database,crdt,state_machine, named handler, orrecorddeclarations, orspawn/handle/receive/emit/migrate/cap-annotate/type-annotateexpressions. Output is canonical and idempotent (reformatting is a no-op). Class/impl method params and returns use the parser'sUnit/bare-Type::Varomitted-annotation sentinel and are skipped rather than emitted as spurious: Unit. AddedCrdtType::keyword()(inverse offrom_keyword). 9 new unit tests; all 33examples/*.nulaformat without errors and re-parse. -
RFC 0003 Item 14 — transport hygiene complete.
quinnremoved entirely (noquinndep,quic_transport.rsdeleted).reqwestandrustlsare confined to their composition-root trait impls:ReqwestHttpProvider(theHttpProviderimpl) insrc/backends/mod.rsandrustlsinsidesrc/runtime/network.rs(theNetworkTransportimpl).Runtimeholdshttp: Box<dyn HttpProvider>(defaultReqwestHttpProvider) delegating throughhttp_post_json/http_get;Transportblanket-impls overNetworkTransport(alreadyBox<dyn NetworkTransport>). No core-language file imports quinn/rustls/reqwest directly — a 2125 runtime can swap the transport and HTTP client without touching the language. -
Bootstrap Stage 2: multi-fn programs + recursion through the self-hosting pipeline (Experimental,
bootstrap/):verify.shcheck 6 proves whole-program compilation —desugar_fns.pylowers top-levelfndefinitions into a let-binding chain,compile_hex.nulacompiles it to hex, and the VM runs the resulting.nbc(multi-fnadd(double(3))→ 7). Recursion also works through the pipeline (let fib = fn(n) => ... in fib(10)→ 55). 11/11 checks pass. Documented remaining blocker: the 3-argumentnperformpath (String.charAt, 2 args) corrupts its effect-name constant due to the host compiler's MIR register-spill bug (compile_hex.nula'scomphas 178 locals;src/mir_codegen.rs).String.length(1-arg) andIO.printwork. -
Bootstrap self-hosting pipeline verified end-to-end (Experimental,
bootstrap/):compile_hex.nula(a Nulang Core program) compiles Core source → hex bytecode;fixup_hex.pypatches jump/constant/closure offsets;hex2nbc.pyemits a runnable.nbc; the VM executes it — a Nulang program compiling Nulang Core with no Rust compiler in the loop (RFC 0003 Item 3, Stage 1→2 bridge).bootstrap/verify.shgained a pipeline check (5 expressions: arithmetic,let,if,not, closure application) and now supportsNULANG_BIN=to skip thecargo runrebuild. Fixed thefalsekeyword hash bug incompiler_core.nulaandcompile_hex.nula(read_identreturns the low-16 hash;false= 13715, not 79251, so the literal was never recognized — barefalse→ nil,not false→ false). Verified: 20-expression oracle comparison against the Rust compiler, all matching;verify.sh9/9 checks pass. -
Debug Adapter Protocol server (Experimental,
src/dap/,--dap).nulang --dapspeaks DAP over stdio (the sameContent-Lengthframing as the LSP) so editors such as VS Code can debug.nulaprograms: source breakpoints, continue/step-in/step-over/step-out, pause, stack traces (frames + source lines), scopes, local-variable inspection, andevaluate(local lookup + literals). Architecture: a reader thread parses framed requests; a server loop dispatches them; a dedicated debuggee thread owns the VM with aDebugHookinvoked before every interpreted instruction (JIT disabled while attached) that returns theDebugPausesentinel to stop. The v1 debuggee runs on the standalone VM — top-level code, functions, closures, and effect handlers (IO.print/IO.read) are fully debuggable; actorspawn/send/receiveare no-ops, matching the standalone VM's outside-an-actor contract. Program stdout is captured and forwarded as DAPoutputevents so it never corrupts the DAP stream. In-process test harness:run_dap_server_ioover arbitrary buffers. -
Debugger line table & per-function debug info (Experimental). The MIR pipeline now records a source-line map (
CodeModule.line_table: bytecode pc → 1-indexed line, one entry per source statement) and per-function metadata (CodeModule.debug_functions: name, code range, named locals with their registers).mir_lowerthreads eachhir::Stmtspan into theFunctionBuilder;mir_codegentranslates statement indices to bytecode PCs. Both fields are additiveserde(default), so pre-existing.nbcartifacts deserialize unchanged. -
par { ... }independence annotation (Experimental).par { e1; e2; ... }declares that the sub-expressions have no data dependencies on each other. Semantics are identical to a sequentialBlock(evaluated in order, last expression wins); the distinctExpr::Parnode is preserved through the frontend so later passes can exploit the independence (e.g. parallel lowering/codegen), mirroring nanolang'sparblock. Wired through lexer, parser, typechecker, effect checker, capability analyzer, HIR lowering, formatter, and LSP.
.nbcexport table (Stable,src/bytecode.rs).ExportTableEntrystruct (name/kind/index/type_sig) added toCodeModulewith#[serde(default)]for backward compatibility.add_export()convenience method. Consumers can link against library exports with full type signatures. (RFC 0003 Item 17)CodeModule::from_bootstrap_json()(Stable,src/bytecode.rs). Parses the bootstrap emitter's JSON format into a runnableCodeModule. Accepts hex instruction strings, typed constants (Int/Float/Bool/String), and export table entries. (RFC 0003 Item 3)- Bootstrap self-hosting pipeline (Experimental,
bootstrap/). Stage 1 emitter (emitter.nula) outputs structured JSON for 3 Core programs (literal, add, conditional). Host converter roundtrips through.nbc. End-to-end integration test verifies the full pipeline. (RFC 0003 Item 3) - WASM Component Model WIT generator (Experimental,
src/witgen.rs). Maps 5 built-in Nulang effects (IO, Timer, Random, Signal, Provider) to WASI 0.2+ WIT interfaces.extract_effects_from_source()scans forperform Effect.op(...)patterns.--backend wasm-componentCLI flag writes.witalongside.wasm. (RFC 0003 Item 16) - Formal semantics in Lean 4 (Experimental,
spec/formal/). 6 modules formalize the Nulang Core type system:Types.lean(type language, substitution, mgu),Capabilities.lean(capability lattice, subtyping, join, sendability),Effects.lean(effect rows, subsumption, union),Syntax.lean(Core expression AST, free vars, capture-avoiding substitution),Typing.lean(typing context, judgment Γ ⊢ e : τ). Soundness theorems (Substitution Lemma, Preservation, Progress, Type Soundness) are stated as conjectures.lake buildpasses. (RFC 0003 Item 2) .nbcdependency type innulapackage manager (Experimental,src/package/).nbcfield inNulang.toml[dependencies].PackageSource::Nbcvariant with full resolver pipeline (lockfile, content hash, dedup). (RFC 0003 Item 17)- Distributed trace context propagation (Stable,
src/runtime/).trace_id: Option<String>onMessagestruct, propagated from wire (Packet::ActorMessage) through cross-shard delivery to local send. (RFC 0003 Item 15) - Backend trait wiring (Stable,
src/backends/). 8 backend traits (JitBackend, WasmBackend, ForeignInterop, StorageBackend, Transport, CryptoProvider, HttpProvider, TlsProvider) fully trait-erased from the core language.create_default_jit()factory insrc/backends/mod.rs.
No stability promise. The 0.x series is the alpha development track. Language version 1.0.0-frozen is the first version with a published stability contract; everything before it is implicitly Experimental.