Releases: nurl-lang/nurl
v0.9.8
[0.9.8] — 2026-06-15
Added
-
NAT- and mobile-traversing distributed transport (§7.4). A complete
pubkey-addressed overlay where a peer is a public key, not an address —
it reaches that key over a direct peer-to-peer path when it can and a relay
when it must, and never drops to zero reachability. Built bottom-up:
net/securedgram.nu(WireGuard-style encrypted UDP over Noise + a session
AEAD with a sliding replay window, plus endpoint roaming so a peer survives
a network change),net/stun.nu(RFC 8489 server-reflexive discovery),
net/nat.nu(candidate gathering, NAT-type classification, UDP hole punch),
net/relay.nu(DERP-style opaque forwarding plus group multicast —
broadcast to your own group),net/rendezvous.nu(signaling-only directory),
net/transport.nu(the flat seam:transport_send/broadcast/recv,
direct-when-possible/relay-when-forced with promote/demote), and SWIM
membership (net/membership.nu) hardened by Lifeguard
(std/lifeguard.nu) with a failure-detector control loop
(net/failuredetector.nu). On top sit the sharding + replicated-state
layers: a consistent-hash ring (dist/ring.nu), state-based CRDTs
(dist/crdt.nu— PN-Counter, LWW-Register, OR-Set) and their gossip wiring
(dist/replicator.nu, anti-entropy scoped to a key's replica set). Documented
end to end indocs/DISTRIBUTED.md. -
The Crown — distributed computation (§7.5). Turns the distributed state
above into distributed work.dist/identity.nugives each peer a replica
id;dist/job.nuis the keystone — submit a task keyed byk, the ring owner
executes it via a registered handler, the result is recorded idempotently, and
a key that re-homes mid-flight is forwarded to the new owner so the job
still completes.dist/lease.nuadds fencing tokens (epoch monotonicity +
idempotency keys) so a side-effecting task fires at most once across a
split-ownership window. Liveness under load is handled by SWIM
self-refutation plus a heartbeat on a dedicated OS thread
(dist/heartbeat.nu), so a 100%-CPU node is not falsely evicted. The whole
story is verified by a deterministic chaos-simulation harness
(dist/sim.nu): a virtual clock + in-process message bus with seeded fault
injection (drop, latency/jitter→reorder, partition/heal) drives the real
stdlib logic, with scenarios for converge-under-loss, keystone-across-
partition, at-most-once-side-effect, and CPU-pinned-not-evicted — all
byte-reproducible goldens, ASan-clean. -
Push-To-Talk voice app —
pttvoice/. A distributed PTT voice app on the
overlay: captures the microphone, Opus-encodes it (48 kHz mono, 20 ms
frames via a libopus FFI binding), and pushes a talkspurt either to one peer
(unicast — p2p when punchable, relayed otherwise) or to the whole group
(multicast); a receiver decodes and plays it. ALSA capture/playback
(audio.nu), the codec (opus.nu), the voice wire frame (proto.nu), and
the app (ptt.nu) live in a self-contained folder. Verified live over a
loopback relay (group broadcast and peer unicast).build.shnow detects
libopus and ALSA (droppingstdlib/runtime.{opus,asound}sentinels)
andnurl.shauto-links-lopus/-lasoundwhen those FFI symbols appear. -
Playground “🎙️ PTT Chat” demo with channels. A new
/pptchattab: a
page with a microphone button and an embedded NURL→WebAssembly module
(nurlapi/static/pptchat.nu→pptchat.wasm) that reads the mic through the
audioFFI and paints a live VU meter + frequency spectrum, framing the
distributed voice tech. Channels: no id → the sharedpublicchannel;
+ Create channel mints a random id and navigates to/pptchat/<id>;
opening that URL joins the same channel (the URL is the invite, the future
shared-secret/QR), with a Copy-link button. -
Parallel sanitizer test suite.
compiler/tests/run_san_tests.shnow runs
AddressSanitizer/UBSan checks in parallel (NURL_SAN_JOBS, default = cores),
cutting the sanitized CI leg from minutes to under one — matching the already-
parallel functional runner. -
HTTP client cookie jar —
stdlib/ext/cookies.nu(critic B23). The
server side writesSet-Cookie(ext/http_auth.nu); this is the missing
client half.cookie_jar_setparses oneSet-Cookievalue (Domain,
Path, Expires, Max-Age, Secure — Max-Age wins over Expires, an
already-expired cookie deletes its stored match) defaulting Domain/Path
from the request host/path;cookie_jar_headerreturns theCookie:
value for a request, applying RFC 6265 domain matching (§5.1.3,
host-only vs subdomain), path matching (§5.1.4), Secure gating, and
expiry, longest-path-first. Pure string-in/string-out — decoupled from
the HTTP client types, so it round-trips a session over HTTP/1.1, h2,
or any header source.now(unix seconds) is passed explicitly for
deterministic expiry. Lock:compiler/tests/cookies_basic.nu
(host-only vs Domain, path ordering, Secure, Max-Age/Expires expiry,
replacement, Max-Age=0 deletion, malformed rejection); ASan+UBSan+LSan
clean. -
Benchmark harness —
stdlib/std/bench.nu+nurlpkg bench(critic
C4).std/bench.nutimes a no-arg closure over many iterations and
reports ns/op (via the monotonic clock) and allocations/op.
bench_run name iters bodyruns a short untimed warmup then a timed
loop;bench_auto name bodyauto-scales the iteration count until a
pass clears ~50 ms, for stable numbers on sub-microsecond operations.
bench_reportprints one line;bench_result_*accessors expose the
raw numbers. The allocation metric is backed by a new runtime hook,
nurl_alloc_count(a relaxed-atomic counter on every
nurl_alloc/nurl_zalloc— which is what stdlib vec/string/struct
blocks route through), snapshotted around the timed loop so warmup is
excluded.nurlpkg benchdiscoversbenches/*.nu, compiles each at
-O2, runs it, and streams its report (no goldens — wall time is
machine-dependent; a bench fails only on a compile error or nonzero
exit). Shipsbench/stdlib_hotpath.nu(string build / vec push / sort
micro-benches). Locks:compiler/tests/bench_basic.nu(deterministic
surface — report formatting, the alloc counter on a vec cycle vs a
no-op body, iteration bookkeeping) and
compiler/tests/nurlpkg_bench_smoke.sh(runner discovery / streaming /
summary / exit codes). -
nurlpkg test— user-facing test runner (critic C3). Ships the
compiler suite's per-test pattern as a tool:nurlpkg testdiscovers
tests/*.nu, compiles and runs each, and reportsPASS/FAILwith a
summary (exit 0 iff every test passes). A test passes on exit 0; if a
tests/outputs/<name>.txtgolden exists, the program's stdout must
match it byte-for-byte instead. Tests run in sorted order for
determinism. The build driver is./nurl.shby default, overridable
via$NURL_CC(a command taking<flags> <src> <outbin>) for an
installed toolchain. Smoke-tested by
compiler/tests/nurlpkg_test_smoke.sh(all four verdict paths +
all-pass/any-fail exit codes + the empty-tree message). -
REPL —
tools/repl(nurl repl) (critic C1). An interactive
read-eval-print loop on a process-per-eval model: top-level definitions
(@functions,&FFI,$imports, and:types / enums / globals)
accumulate into a persistent session, while every other line is spliced
into a freshmain, compiled with./nurl.sh -O0, and run — its stdout
is echoed back. A new definition is validated by a fastbuild/nurlc
frontend pass before it joins the session, so a typo never poisons later
evaluations. Line editing + history come fromstd/term.nu(the B10
work); on a non-tty (pipe / script) it falls back to plain buffered
reads. All REPL chrome — prompts, acks, errors,:help— goes to
stderr, so stdout carries only the evaluated program's output. Meta-
commands::help/:h,:quit/:q,:defs,:reset,:save FILE.
Multi-line definitions are read until brackets balance. Build with
./tools/repl/build.sh; smoke-tested bycompiler/tests/repl_smoke.sh
(definitions + globals persist across lines, a bad definition is
isolated, stdout stays clean). Note: process-per-eval re-initialises a
:global on every evaluation — definitions and pure functions persist,
but mutation does not accumulate across lines. -
Bitset —
stdlib/std/bitset.nu(critic B18, collections round-out).
A fixed-size bit array over 64-bit limbs:bitset_set/bitset_clear
/bitset_flip/bitset_test(all bounds-checked, so the unused high
bits of the last limb stay clear),bitset_set_all/bitset_clear_all,
a popcount-backedbitset_count,bitset_any/bitset_all/
bitset_none, the in-place combinersbitset_and_with/bitset_or_with
/bitset_xor_with,bitset_clone, and an ascendingbitset_each_set.
Storage is a flatnurl_zallocword buffer peeked/poked by limb. NURL
has no native XOR or NOT operator, so the module uses the exact,
carry-free identitiesa ^ b = (a|b) - (a&b)and~m = -1 - m. Lock:
compiler/tests/bitset_basic.nu— cross-limb set/clear/flip, out-of-
range no-ops, popcount,all()on a full set, the three combiners with
an XOR-identity bit check, and clone independence; ASan+UBSan+LSan clean. -
LRU cache —
stdlib/std/lru.nu(critic B18, collections round-out).
A fixed-capacityLruCache [V]over string keys, backed by a HashMap
(key → slot) plus an intrusive doubly-linked recency list over
preallocated slot arrays with a free list — solru_get/lru_put/
lru_contains/lru_removeare all O(1) and a c...
v0.9.7
[0.9.7] — 2026-06-11
Added
-
Enum payload residuals diagnosed — ghost variants and unsized
generics are hard errors now (compiler/nurlc.nu, critic A7).
An unknown/unimported type name in an enum variant's payload position
parses as a SEPARATE variant (same-file forward references already
resolve via the pre-scan, so this fires only for typos and missing
imports) — the intended payload silently vanished and downstream
code emitted out-of-boundsextractvalue/ brokenstoreIR, or
silently read a sibling variant's slot. Three new hard errors:
payload-arity checks at the match arm ("match arm binds 1 payload(s)
but variant 'V' declares only 0 …") and at the enum literal, both
naming the unknown-type-parses-as-variant cause; and an
unknown-generic check inparse_type_paren—( Vec i )with no
generic-struct template in scope and no materialised instantiation
dies at the use site naming the missing$import, instead of
clang's "loading unsized types is not allowed" far from the cause
(zero-type-arg( Type )trait-impl targets are exempt). Locks:
should_fail_ghost_variant_match.nu,
should_fail_ghost_variant_construct.nu,
should_fail_unknown_generic_type.nu. False-positive sweep: full
suite 339 PASS + nurlapi + examples clean. -
"Statement has no effect" warning — the last silent prefix-arity
cascade is now diagnosed (compiler/nurlc.nu, critic A2). A
statement that produces a value without being a call or control flow
(bare local identifier, operator expression like+ a 1, a#cast,
a.field read) discards that value silently — under prefix
notation with fixed arity and no closing token, this is exactly the
residue left when an operator short an argument swallows the next
statement's leading token. The bare-literal flavor was already a
hard error (dangling operand); these shapes name real bindings, so
they warn. Value-block tail expressions (the block result), calls,
and?/??statements (their arms may be effectful) are exempt.
The diagnostic embeds the dead statement's own line — by the time
the block iterator sees the flag, the lexer already points at the
next statement. Tree-wide false-positive check: nurlc.nu
self-compile, the full stdlib, nurlapi, and examples produce ZERO
warnings. Locks:should_warn_dead_value.nu(four dead shapes warn;
tail/call/return stay silent), andshould_warn_caret_xor.nunow
also catches the previously silent deadbin: i x ^ a b. This
closes the last undiagnosed half of the prefix-arity cascade family
(critic §4); the A3 closing-delimiter decision can cite it as the
mitigation. -
std/bigint: arbitrary-precision division and modulo —
bigint_div/bigint_rem(stdlib/std/bigint.nu), closing the last
gap in the bigint arithmetic surface. The magnitude core is Knuth
Algorithm D (TAOCP vol. 2, §4.3.1) over the base-2¹⁶ limbs: D1
normalization reuses the existing small-multiply helper (top divisor
limb ≥ base/2, so every trial digit is off by at most one), the
multiply-and-subtract step uses a per-limb {0,1} borrow (no negative
shifts), and the rare add-back step is exercised by both classic
Hacker's Delightdivmnutrigger vectors. A single-limb divisor
short-circuits through__mag_divmod_small_inplace. Semantics are
truncated division exactly like the native/and%: the quotient
rounds toward zero, the remainder takes the dividend's sign, and
x == (x/y)*y + x%yholds for everyy ≠ 0. Division by zero panics
(recoverable viarecover) — a defect, not a data error, so it is not
threaded through!. Regressioncompiler/tests/bigint_div.nu: all
four sign combinations, zero/a<b/exact edges, both add-back vectors,
a 39-digit ÷ 21-digit case, a 60-round deterministic invariant sweep
(reconstruction,|r| < |y|, remainder sign) over growing multi-limb
operands, and the recovered divide-by-zero panic; ASan+UBSan clean,
leak-free. Additionally verified against Python on 300 random cases
(mixed limb counts/signs, near-power-of-2¹⁶ divisors).
Fixed
-
Auto-drop: fn-returned by-value structs with owned fields now
transfer ownership to the caller (compiler/nurlc.nu, critic A4c).
Two bugs closed.^ @ T { ( nurl_str_cat … ) }(direct construction
return) leaked the field — the callee never bound it so never
registered a drop, and the caller never registered one either.
^ vwherevis a bound struct was worse: a use-after-free —
the callee's scope-exit drop freed v's owned field while the
returned-by-value copy still aliased it, so the caller read freed
memory. The fix mirrors the existing owned-string return flag: the
callee skip-drops the escaping struct binding and publishes its exact
owned-field list (<fname>__ret_owned_fields), which the caller's
: T x ( f )re-registers through the same
mem_register_agg_owned_fieldspath — exactly one drop, at the
caller's scope exit. Ownership composes through^ ( mk )call chains
and reaches nested struct fields. Safe against double-free with
stdlib's manual*_freeconventions: only raw-s/slice fields filled
by a fresh allocation in a direct agg-literal return register for
transfer — stdlib's struct returns useString/Vechandle fields
(untracked) and build incrementally before^ binding(which never
registers agg fields), so their manual frees stay correct. Verified:
full san suite 0 SAN_FAIL,tools/leakcheckzero, suite 340 PASS,
and a targeted incremental-build manual-free probe stays single-drop.
Regressionret_struct_owned_transfer.nu(direct / bound / chain /
nested shapes, ASan+LSan zero). No nurlc IR change — fixed point holds
without a bootstrap refresh. -
Auto-drop: arm-local trailing declarations leaked;
^ ( call )
string ownership now propagates; aliasing escapes transfer ownership
(compiler/nurlc.nu, critic A4). Three coupled fixes: (1) a:
declaration as an arm's LAST statement made the arm look
value-producing (gen_let_or_struct left the RHS type in last_type),
which suppressed the Phase 2D fall-through drop — leaking the binding
on every?/??/loop arm ending in a decl — and emitted a bogus phi
over the discarded value; declaration statements now publishvoid.
(2)__fn_ret_str_owned__was only set for identifier returns, so
@ helper → s { ^ ( nurl_str_cat … ) }was never marked
__ret_owned=strand: s x ( helper )leaked one buffer per call;
gen_ret now consults the outermost call's__last_call_ret_owned__
for direct parenthesised-call returns, making ownership compose
through helper chains. (3) The widened tracking exposed missing
ownership TRANSFER on aliasing escapes:= outer xand ternary/match
arms whose value is a bare load of an owned binding now cancel that
binding's scheduled drop (mem_remove_owned_str; the arm delta-drop
protocol switched from prefix-length to word-membership to stay
consistent under mid-list deletion). Conservative direction
throughout: worst case a leak, never a use-after-free — the
pre-transfer behavior freed buffers that had escaped through phis,
which miscompiled the compiler itself (gen_cast's
: s norm ? … xv ( nurl_cg_reg cg )returned a freed register
name). Bootstrap snapshot refreshed (--refresh-bootstrap).
Regressions:arm_local_trailing_drop.nu+
ret_owned_propagation.nu, both ASan+LSan zero, with manual-free
double-free locks on the transfer paths. Known residual filed as
critic A4c: fn-returned structs with owned fields still transfer
nothing (needs an ownership-model decision against the stdlib's
manual*_freehandle conventions). -
server_stopfrom another thread freed the listener under blocked
pool workers (stdlib/ext/http_server.nu).server_run_pool's
documented shutdown — callserver_stop sfrom another thread while
workers block in accept — was a heap-use-after-free: workers hold no
reference on the listener, so the stop'snurl_tcp_closedropped the
last ref and freed the struct while every worker was still polling
itsshutting_downflag and wake-pipe fd (3/3 reproducible under
ASan; single-threadedserver_runraced identically).server_run
andserver_run_poolnow retain the listener for the whole
run→join window and release it only after no worker can touch the
handle — the same contractserver_run_asyncalready followed for
its accept fiber. The two-phasetcp_shutdown_listener→ join →
server_stoppattern remains valid; it is simply no longer the only
safe shutdown. Regressioncompiler/tests/http_server_stop_direct.nu
drives both fixed paths with a direct cross-thread stop (ASan-clean
10/10 underNURL_NET_TESTS=1). Closes critic.md B19 together with
the earlier accept-wake fix (f470571). -
recoverleaked the closure's captured environment
(stdlib/std/panic.nu).recoverdecomposes its closure into
(fn_ptr, env_ptr)and hands them to the C trampoline; passing the
raw env pointer onward suppresses the parameter's auto-drop (the
compiler must assume the env escapes — and inthread_spawn, whose
shape this mirrors, it really does). Butnurl_recoveris
synchronous: once it returns, the closure can never run again, so the
env was simply leaked — one allocation perrecovercall with a
capturing closure, panic or not.recovernow frees the env right
afternurl_recoverreturns (NULL-safe for capture-less closures),
on both the normal and the unwind path. Found via ASan on the new
bigint_divdivide-by-zero regression; the existing
recover_basic/http_server_panicgoldens are unaffected (output
is unchanged — only the leak is gone). -
**...
v0.9.6
[0.9.6] — 2026-06-08
Added
-
HTTP/2 client — native, multiplexed (
stdlib/ext/http2_client.nu).
Completes the HTTP/2 stack (server + client). Reuses the direction-neutral
framing/HPACK:h2_client_connect_tls(TLS + ALPNh2) /
h2_client_connect_h2c/h2_client_attach→h2_client_submit(N
concurrent streams) →h2_client_run_until_complete→
h2_client_take_response, plus one-shoth2_get/h2_request. Full HPACK
(connection-global decoder), per-stream + connection flow control,
WINDOW_UPDATE / SETTINGS / PING / GOAWAY / RST_STREAM. Added runtime
nurl_tcp_connect_tls_alpn. Exampleexamples/h2_client.nu. -
MCP server: stateful sessions, SSE stream, and
sampling/createMessage
reverse RPC (stdlib/ext/mcp_session.nu). The pure, socket-free stateful
core: anMcpSessionStorekeyed by a CSPRNGMcp-Session-Id, a per-session
outbound notification queue, and server→client reverse-RPC correlation.
mcp_http.nugainsmcp_http_handler_session(mints/validates session ids,
drains the queue to SSE on GET, settles reverse-RPC responses, tears down on
DELETE) plus chunkedmcp_sse_*helpers for a long-lived stream. -
MCP server:
completion/completeargument autocompletion.
mcp_registry_add_completion r ref_type ref_id handlerregisters a
completion provider for a prompt argument (ref/prompt) or resource template
(ref/resource);completion/completeresolves it and returns the
spec-shaped{completion: {values, total, hasMore}}. Unknown refs yield an
empty list (per spec). Works over both stdio and HTTP transports. -
MCP server:
resources/subscribe+notifications/resources/updated.
Per-session resource subscriptions:mcp_session_subscribe/
_unsubscribe/_is_subscribed, andmcp_session_notify_resource_updated
which fans an update notification out to every subscribed session over the
SSE queue. The session HTTP handler services subscribe/unsubscribe against
the request'sMcp-Session-Id. -
XML parser + serializer —
stdlib/ext/xml.nu. Parses the common subset
(elements, attributes, nesting, self-closing, text, comments, CDATA,
declarations, the 5 predefined entities + numeric refs) into anXmltree;
xml_stringifyround-trips with entity encoding. Accessors + builders. Out
of scope: DTD/DOCTYPE, namespace resolution. -
YAML parser + serializer —
stdlib/ext/yaml.nu. Parses a pragmatic
subset into the sharedJsonvalue (so everyjson_*accessor works on a
parsed doc): block mappings/sequences, the seq-under-a-key idiom, plain and
quoted scalars resolved per the YAML 1.2 core schema, flow collections,
comments,---/...markers.yaml_stringifyemits block style with
round-trip-safe quoting. Out of scope: block scalars, anchors/aliases/tags,
multi-doc streams. -
Timezone / DST support in
stdlib/std/time.nu. Local-time conversion +
DST driven by a POSIX TZ string (EST5EDT,M3.2.0,M11.1.0,IST-5:30, …) —
the format IANA tzdata compiles each zone's current ruleset into (covers
US/EU/AU).tz_utc/tz_fixed/tz_parse/tz_offset_at/tz_is_dst
/time_from_unix_tz/time_now_tz/time_to_unix_tz/
time_format_iso_tz. Not in scope: IANA region-name lookup. -
Growing arena + typed allocation —
stdlib/std/arena.nu.
arena_growing chunk_szreturns a chained-chunk arena: when the current
chunk fills, a fresh chunk is linked in and old ones are never moved, so
every pointer handed out before a grow stays valid (the canonical
pointer-stable arena model).arena_alloc_n [T] a count → *Tallocates
countcontiguous items ofT.arena_new/arena_with_capremain fixed
(NULL on overflow) — byte-identical behaviour for existing callers. -
Counting semaphore —
stdlib/std/thread.nu.sem_new/sem_acquire
/sem_try_acquire/sem_release/sem_avail/sem_free, built on the
existingMutex+Cond. The permit count lives in a heap cell, so a
by-value copy (e.g. a worker-closure capture) shares state across threads —
the classic tool for bounding concurrency (see the nurlapi compile gate
below). -
readlink(2)FFI —stdlib/std/fs.nu.fs_readlinkreads a symlink's
target as an ownedStringvia a direct& \c`binding (no runtime change).nurlpkgnow reads and verifies an existingdeps/` link
target, catching transitive name collisions. -
Seedable, deterministic PRNG —
stdlib/std/rng.nu(xoshiro256**).
Companion tostd/random.nu's OS CSPRNG, which draws from the kernel
entropy pool and cannot be seeded.rng.nugives a reproducible stream:
( rng_seed s )expands any i64 seed into a 256-bit xoshiro256** state
via SplitMix64 (so even0/1produce well-distributed, uncorrelated
streams), and the same seed yields a byte-identical sequence on every
platform and build — the determinism guarantee extends here because the
generator is integer-only (no float state,>>lowers to a logical
lshron theu64-typed words). Surface:rng_next(raw 64-bit),
rng_below(unbiased, rejection-sampled[0,n)),rng_range,rng_u01
(uniform double in[0,1), 53-bit),rng_bool,rng_free. Opaque-handle
lifecycle (same shape asArena/Channel/String); state mutates in
place. Not a CSPRNG — predictable, so never for security; use
std/random.nuthere. Regressioncompiler/tests/rng_seedable.nupins
the exact stream for seeds0and0x1234against an independent
reference implementation, and checks determinism,rng_belowbounds, and
the inverted-range guard.
Changed
-
nurlapi: bound concurrent compiles to stop OOM crashes. The container
runs a 16-worker pool, so up to 16 requests run at once; each compile spawns
clang -O2 -flto(hundreds of MB), and 16 simultaneously could exceed the
container memory cap and get the whole process OOM-killed ("container is not
listening"). The accept pool stays wide (light requests keep flowing) but the
heavy compile step is now bounded by a counting semaphore:POST /build*and
POST /mcptake a permit before dispatch and release after (panic-safe),
default 4, tunable viaNURL_COMPILE_SLOTS. -
Reverse proxy: binary-safe streaming response bodies. Streaming
responses were forwarded via a NUL-terminated carrier and truncated at the
first embedded NUL (only SSE/JSON/text survived). The proxy now reads the
stream's true byte length and copies exactly that many bytes. New
bytes_extend_raw+http_stream_next_bytes(→( Vec u )). -
ROADMAP.md rewritten as a concise, forward-looking document (status →
toward-1.0 → planned), with the per-feature history delegated to this
changelog. Bootstrap snapshot refreshed.
Fixed
-
Compiler: struct payloads in enum match slots 1 and 2. A multi-field
struct value carried as an enum payload anywhere but the first slot
(: | E { Nil V i Pt }) emitted invalid IR (store %Pt <ptr>) and was
rejected by clang — construction heap-boxed it for every slot but the
match/unbox path only reconstructed slot 0. Slots 1/2 now mirror slot 0. -
Compiler: recursive auto-
Dropfor boxed-payload enum trees. A
% Drop-free enum whose variants box struct/enum payloads now gets a
compiler-generated recursive drop, so nested owned payloads are freed at
scope exit instead of leaking. -
Compiler: borrow provenance for auto-
Dropenum bindings off a
borrow-returning accessor. A binding taken from an accessor that returns a
view into its parent is no longer treated as owned, fixing a double-free. -
Compiler: drop the scrutinee of
^ ?? owned { … }that returns a
scalar. The owned match scrutinee in a returning??whose arms yield a
scalar was leaked; it is now dropped before the return. -
Compiler: reject unbalanced braces / stray top-level tokens. Malformed
brace nesting and stray tokens at top level are now hard errors with source
locations instead of reaching the backend. -
C64 emulator correctness fix (
examples/).
v0.9.5
[0.9.5] — 2026-06-04
Added
-
Playground shows its deployed version, and the API auto-deploys on
release tags. The playground header now carries a version pill — a
__NURL_VERSION__placeholder inindex.htmlis stamped at image-build
time from theNURL_VERSIONbuild-arg (devfor local builds). A new
.github/workflows/api-deploy.ymlbuilds the API image on av*tag (or
manual dispatch), pushes it to Docker Hub under the exact semver
(nurllang/nurl:vX.Y.Z— no:latest), pinscloudflare/Dockerfile's
FROMto that tag and runswrangler deploy, so a git tag is now a
reproducible playground release. The Docker image was renamed
hindurable/nurl→nurllang/nurl;registry-deploy.ymlis now
manual-only (the registry changes rarely). -
MQTT-over-WebSocket transport —
mqtt_connect_ws. Adds a WebSocket
transport alongside the raw TCP/TLS path so a client can reach a broker's
MQTT-over-WS endpoint (e.g.wss://host:8084/mqtt) — handy when a firewall
only permits the WS port inbound.wss://enables TLS with certificate
verification and negotiates themqttsubprotocol automatically; the codec
and framed packet reader stay transport-blind behind two chokepoints. New
entrypointsmqtt_connect_ws/mqtt_connect_ws_cfg;mqtt_disconnect
also sends a WS Close frame, andmqtt_reconnectrejects WS clients (no URL
to redo the upgrade).stdlib/ext/mqtt.nu. -
Package manager → MLP: login/search/info, yank, token-revoke, catalog UI
(ROADMAP §4). Rounds the registry out into a minimum lovable product.- CLI ergonomics.
nurlpkg loginstores a per-registry publish token
in~/.nurl/credentials(chmod 600) —publish/yankresolve the token
$NURL_TOKEN→ credentials, so it no longer has to live in the
environment.nurlpkg logout [--revoke]forgets it (and optionally
revokes it server-side).nurlpkg search <q>andnurlpkg info <name>
query the registry (infowith no arg still prints the local manifest).
Newstdlib/ext/credentials.nu. - Registry hygiene.
nurlpkg yank|unyank <name> <version>flips a
version's yanked flag (owner-only, viaPOST /api/v1/{yank,unyank}); the
resolver already skips yanked versions, so a yanked release disappears
from resolution.nurlpkg logout --revoke(POST /api/v1/revoke) deletes
the presented token from D1. - Catalog UI. The Worker's
/is now a searchable package list and
/packages/<name>a detail page (versions with yank state, latest
dependencies, an install snippet);GET /api/v1/search?q=backs the CLI. - Client helpers:
pkg_search(pkg_fetch.nu),pkg_yank/pkg_revoke
(pkg_publish.nu). Regressioncompiler/tests/credentials_basic.nu
(set/get/upsert/multi-registry/remove; gatedNURL_CREDS_TESTS=1, clean
under ASan/UBSan). Whole feature set verified end-to-end against the
Worker underwrangler dev: login → creds-based publish → search → info
→ yank (install then fails ResolveNoMatch) → unyank → catalog → logout
--revoke → publish rejected (PubAuth).
- CLI ergonomics.
-
Transitive registry dependencies —
nurlpkg publishsendsX-Nurl-Deps.
Publishing now includes the manifest's registry dependencies (a JSON
[{name, req}]built by__deps_json) as theX-Nurl-Depsheader, which
the registry records in the package index.pkg_publishgained a
deps_jsonparameter. With the deps in the index,resolve_registry
pulls sub-dependencies transitively — previously the index always
recordeddeps: [], so only leaf registry packages installed correctly.
Verified end-to-end against the local Cloudflare Worker: publishtdep-b,
publishtdep-a(depends ontdep-b ^1.0), theninstalla consumer of
onlytdep-a→ both land indeps/and the lock. Registry now supports
real dependency graphs.stdlib/ext/pkg_publish.nu,tools/nurlpkg/main.nu. -
Package registry service — Cloudflare Worker + R2 + D1 (
registry/,
ROADMAP §4 phase 6). The deployable server side of the ecosystem, in
TypeScript. The read path serves the staticindex/<name>.json+
content-addressedpkgs/<name>/<name>-<v>.tar.gzfrom R2 (cacheable, no
compute); the write pathPOST /api/v1/publishauthenticates a Bearer
token (peppered SHA-256 looked up in D1), enforces first-publisher name
ownership + version immutability, recomputes the tarball SHA-256
server-side (never trusts a client digest), and writes the tarball +
updated index to R2. Identity bootstraps via GitHub OAuth (/login→
/auth/callbackmints a one-time CLI token). D1 schema in
migrations/0001_init.sql(users / tokens / packages / versions).
Implements exactly the wire contract the NURL client already drives.
Validated end-to-end locally (no Cloudflare account): under
wrangler dev(miniflare R2 + D1), the realnurlpkgbinary completes a
full publish → install round-trip plus immutability (409) and bad-token
(401) rejections —registry/test-local.sh. Ships with
registry/DEPLOY.md, aregistry-deploy.ymlGitHub Actions workflow
(guarded so a placeholder token can't trigger a broken deploy), and
secrets kept out of the repo (wrangler secret putfor
GITHUB_CLIENT_SECRET/TOKEN_PEPPER; GH Actions secrets for the
Cloudflare deploy token). This completes the registry-backed package
manager:nurlpkg publish+nurlpkg installagainst a deployable
registry, all pure-NURL on the client and standing up locally today. -
Package publishing —
stdlib/ext/pkg_publish.nu+nurlpkg publish
(ROADMAP §4 phase 5). The write side.pkg_packwalks a project tree
into a.tar.gz(excludingdeps,.git/dotfiles,nurl.lock,
target,build);pkg_publishuploads it withPOST <registry>/api/v1/publish,Authorization: Bearer <token>, and
X-Nurl-Package/X-Nurl-Versionheaders (binary body via
http_request_bytes), mapping status to PubAuth (401/403) / PubConflict
(409, version immutability) / PubRejected.nurlpkg publishpacks the
current project, prints its size + SHA-256, and uploads using the token
from$NURL_TOKENand the registry from$NURL_REGISTRY→
[package].registry→ default; a missing token or any non-2xx exits
non-zero. The registry recomputes the checksum server-side — no
client-supplied digest is trusted. Regression
compiler/tests/pkg_pack_basic.nu(offline pack + gunzip + tar_parse
membership: nested source included, deps/ + dotfiles excluded), clean
under ASan/UBSan + leak-free. Verified end-to-end against a static
pythonregistry: a full publish → install round-trip (a library
packed, uploaded with a Bearer token, then resolved + installed into a
consumer'sdeps/), plus immutability (409), bad-token (401), and
no-token rejections. This is the exact contract the Cloudflare
Worker + R2 write endpoint (phase 6) will implement. -
Verified registry install —
stdlib/ext/pkg_fetch.nu(ROADMAP §4
phase 4b). The I/O side that turns a resolvedLockPkginto files on
disk against a static-HTTP registry (R2 + CDN shape).pkg_fetch_index
GETs<registry>/index/<name>.json;pkg_install_onedownloads
<registry>/pkgs/<name>/<name>-<v>.tar.gz, verifies its SHA-256
against the recorded checksum, gunzips, and path-safetar_unpacks it
into<dest>/<name>— composing the whole pure-NURL package stack (http
binary body + sha256 + gzip + tar). Capstone regression
compiler/tests/pkg_install_e2e.nustands up a loopback NURL registry
server (serves a realtar_create+gziptarball + an index carrying
its true checksum) and drives the full pipeline resolve → download →
verify → unpack end-to-end, plus a wrong-checksum rejection
(PkgChecksumMismatch);NURL_NET_TESTS=1. Clean under ASan/UBSan.nurlpkg installis now registry-aware. It resolves the manifest's
registry deps (foo = "^1.2"or{ version, registry }), downloads +
verifies + unpacks each intodeps/<name>, and writes anurl.lock
whose registry entries carrysource = "registry+<url>"+ the tarball
checksum(path deps keep their local source). The registry URL comes
from$NURL_REGISTRY→[package].registry→ a built-in default. A
failed download or checksum mismatch makesinstallexit non-zero.
Verified end-to-end against a staticpython -m http.serverregistry
serving a GNU-tar --format=ustar | gzippackage (differential interop):
the happy path installs + locks with thesha256sum-computed checksum,
and a tampered index checksum is rejected with the package left
uninstalled. -
Binary-safe HTTP response body —
http_body_bytes/http_body_len.
http_body_strreads the response body through a NUL-terminated carrier
(truncates at the first embedded NUL). The newhttp_body_bytesreturns
an owned, length-accurate( Vec u )copy, andhttp_body_lenexposes
the byte count — required for binary downloads (package tarballs, images,
compressed payloads). Completes the binary HTTP story alongside the
earlier binary-safe request body. Regression:
compiler/tests/http_response_binary.nu(loopback server replies a
5-byteA B \0 C Dbody; client confirms full length + the NUL via
http_body_bytes;NURL_NET_TESTS=1). Clean under ASan/UBSan.
stdlib/ext/http.nu. -
Registry resolution core —
stdlib/ext/registry_index.nu+
stdlib/ext/resolver.nu(ROADMAP §4 phase 4). The read side of the
package registry. A registry serves a static JSON index per package at
<registry>/index/<name>.json(versions, each with a tarball SHA-256
checksum,yankedflag, anddeps); tarballs live at the
content-addressed `/pkgs//<...
v0.9.4
Added
-
Keyword arguments — default parameter values + named call arguments.
A trailing parameter may carry a default:@ f s a s b =xi n = 3 → R
(the default is a single source token — literal / const / atom). A call
may then omit defaulted trailing arguments —( f val )— and/or pass
arguments by name in any order, mixed with leading positional ones:
( f a: 1 b: 2 ),( f val n: 5 ),( greet greeting:Hiname:Bob).
Implemented as a call-site desugaring to an ordinary positional call:
scan_fn_sigsrecords each function's parameter names + default sources;
gen_callfills omitted trailing defaults inline, and routes a call that
usesname:labels throughgen_call_kwargs, which evaluates arguments
in source order and assembles them in parameter order. Existing positional
calls take the unchanged path (byte-identical IR — bootstrap fixed point
holds). Regression:compiler/tests/kwargs.nu. Current limits (documented
in the grammar): not on generic functions, FFI/variadic, or parameters
with theinout/sinkconvention;**kwargs-style collection is not
provided (pass aJson/struct). -
BLAKE3 hash (pure NURL) — completes the hash family. New
stdlib/std/hash_blake3.nuimplements full BLAKE3 (the ChaCha-derived
compression function, 1024-byte chunks split into 64-byte blocks with
CHUNK_START/CHUNK_END flags, the binary Merkle tree of chaining values,
and the ROOT-flagged final node), exposed viablake3_bytes/
blake3_hexinstdlib/std/hash.nu(unkeyed, 32-byte output). All-NURL
u32 wrapping arithmetic, little-endian, binary-clean over( Vec u )—
no C at all (compiler and runtime untouched). Verified
digest-for-digest against the official BLAKE3 reference across every
structural path (empty, sub-block, the 1024-byte single chunk, the
1025-byte two-chunk boundary, balanced multi-chunk trees up to 5000
bytes); regressioncompiler/tests/blake3.nu; clean under ASan/UBSan/LSan.
Closes the ROADMAP "Extended Hash Family" item — SHA-1/256/512, MD5,
HMAC, and BLAKE3 are all shipped. -
volatile_load/volatile_storecompiler intrinsics for MMIO. Emit
load volatile/store volatileas pure IR (no runtime call, so they
work on a freestanding target). The optimizer can no longer hoist an MMIO
read out of a polling loop (LICM), reorder accesses, or coalesce repeated
reads/writes — the missing piece for spinning on a device status register
at-O2. The access width comes from the typed pointer argument (*T),
so one pair covers i8/i16/i32/i64.stdlib/hal/mmio.nu
(mmio_read32/write32/set32/clear32) now uses them, so the ESP32
UART/GPIO drivers no longer need the-O0workaround. Regression:
compiler/tests/volatile_mmio.nu; verified at-O2the volatile load
stays inside the loop body. -
ESP32 bare-metal register HAL (
stdlib/hal/esp32.nu). Pure-NURL GPIO
and UART0 over the chip's memory-mapped registers (built on
stdlib/hal/mmio.nu) — no ESP-IDF, no FFI. GPIO output enable / set /
clear, and a blocking UART console (esp32_uart_putc/getc/puts
with FIFO-count helpers), with register addresses taken from the ESP32 TRM
and cross-checked against ESP-IDF'ssoc/*_reg.h. Demonstrated by the new
fully-NURL UART echo example (examples/esp32/idf-uart). -
C64 emulator example (
examples/c64). A MOS 6510 / Commodore 64
emulator in pure NURL — a singlecore.nuengine shared by a native CLI
and a WebAssembly browser front-end. The CPU core passes Klaus Dormann's
6502_functional_test(the canonical 6502 correctness oracle, validated
headlessly), and with stock KERNAL/BASIC/CHARGEN ROMs the machine boots
through the full power-on sequence — PLA banking, CIA1 jiffy IRQ — to the
BASICREADY.prompt.
Fixed
-
nurlfmt split hex/binary/octal integer literals. The tokenizer's
numeric scanner stopped at the first non-decimal digit, so0x3FF44008
became two tokens (0+ identifierx3FF44008) and the reformatted
source miscompiled — silently, because--checkis idempotent on its own
broken output.tools/nurlfmt/tokenize.nunow scans a0x/0b/0o
prefix and its body as one token. Verified by the
nurlfmt_idempotent.shgate (450 files, IR-transparent) and by restoring
the hex literals in theexamples/esp32/*register maps that had been
worked around with decimal constants. -
SQLite production hardening (Tier 1 + Tier 2).
stdlib/ext/sqlite.nu
is now binary-safe and resource-safe:- NUL-safe text I/O.
sqlite_column_textreads the column's exact
byte length viasqlite3_column_bytes(wasstrlen, which truncated
at the first embedded NUL), andsqlite_bind_textnow takes aString
and passes an explicit byte length tosqlite3_bind_textinstead of
-1— strings with embedded NULs round-trip intact. - BLOB support. New
sqlite_bind_blob(Vec u→sqlite3_bind_blobSQLITE_TRANSIENT) andsqlite_column_blob(sqlite3_column_blob+
_bytes→ ownedVec u) — the binary-safe write/read path.
sqlite_open_v2with open flags.SQLITE_OPEN_READONLY/
READWRITE/CREATE/URI/NOMUTEX/FULLMUTEX/NOFOLLOW
constants exposed;sqlite_openis nowREADWRITE|CREATEover
open_v2. A read-only connection refuses writes (newSqliteReadOnly
error variant) instead of silently creating a file.sqlite_busy_timeoutwrapssqlite3_busy_timeoutsoSQLITE_BUSY
blocks-and-retries under concurrent access rather than failing
immediately.% Dropauto-close.DatabaseandStatementimplement the Drop
trait; a scope-local handle — including one unwrapped from a
! Database E/! Statement Eresult in a match arm — closes itself
on every path (Ok, Err, early return) with no manual
sqlite_close/sqlite_finalize. Teardown zeroes the handle slot after
closing, so a stale internal re-entry is a no-op. Verified leak-free
and double-free-free under ASan + UBSan (compiler/tests/sqlite_hardening.nu).- Tier 3 — datatypes & transactions.
sqlite_bind_double/
sqlite_column_double(REAL columns),sqlite_column_is_null,
sqlite_begin/commit/rollback, and a closure-based
with_transactionthat COMMITs onOkand ROLLBACKs onErr
(propagating the original error). - Tier 4 — hardening for untrusted SQL/DB. Extended result codes are
enabled on every open, so constraint failures now map to distinct
variants (SqliteConstraintUnique/…ForeignKey/…NotNull/
…PrimaryKey/…Check). Addedsqlite_last_insert_rowid;
sqlite_set_defensive/sqlite_enable_load_extension/
sqlite_harden(DEFENSIVE on + extension-loading off — blocks
corruption/RCE from a hostile DB);sqlite_limit(bound query
complexity); a closure-basedsqlite_set_authorizer/
sqlite_clear_authorizerthat installs a sandbox callback with the
exact C ABI libsqlite expects (the closure's compiled function +
captured env are passed asxAuth+pUserData, the same mechanism
thread_spawnuses forpthread_create— no C bridge); and PRAGMA
helperssqlite_journal_wal/sqlite_foreign_keys/
sqlite_synchronous. Verified under ASan + UBSan
(compiler/tests/sqlite_tier34.nu).
- NUL-safe text I/O.
Changed
- Match-arm payload bindings now participate in auto-drop. A
% Drop
type bound as a??match-arm payload (e.g.?? r { T db → … }) — or a
:let inside a match arm — is now dropped at arm scope exit, on the same
void-arm-only rule used for owned strings/structs. Previously such
bindings were never dropped (a latent leak); this is what lets the SQLite
handles above close automatically in the idiomatic result-unwrap flow.
Documentation
-
ROADMAP brought up to date. The Status header now reads Grammar v2.1
(was v2.0) and points atspec/grammar.ebnf. Items that were marked pending
but are in fact shipped are now[x]: the async runtime (stackful M:N
fibers — the Coroutines-vs-async/await decision is settled), HTTP server
Phase 8 (production hardening) and Phase 9 server-side (TLS+SNI+ALPN+
mTLS+reload, HTTP/2, WebSocket — client-side remains), the optional
-lcurlsentinel-gated linking, and thenurlc_lastgood.nurefresh
lifecycle (documented via--refresh-bootstrap). Added an explicit
"What's actually left" summary to the Status section (HTTP/2+WebSocket
client-side; mobile/no_stdtargets; SQLite BLOB/double; reverse-proxy
binary bodies; blake3; MCP SSE/sessions/auth; theruntime.cfile-split;
a compiler-embedded LLM; bench peers). Stale build-size figures left only
in dated historical "shipped" entries (records, not current claims). -
Removed hard-coded build-artifact sizes from the reference docs. The
~480 KB nurlc.wasm(docs/PLAYGROUND.md) and~1.6 MB
nurlc_lastgood.ll(docs/BUILDING.md) figures drift every build and
mislead when the real artifact differs. Build sizes belong in the
changelog/release notes (tied to a specific version), not in
instructional docs. -
Cleaned stale
GOTCHAS.md item N/§Nreferences out of code comments.
Afterdocs/GOTCHAS.mdlost its numbered list, ~44 source comments (in
compiler/nurlc.nu, thenurlc_lastgood.nusnapshot mirror, nine
compiler/tests/*.nu, andstdlib/ext/{http_middleware}.nu) still pointed
at item/section numbers that no longer exist. Each now points at the real
home (escape/lifetime →docs/MEMORY.md§2.3, grammar →docs/LIMITATIONS.md)
or simply describes the behaviour inline. Thenurlc_lastgood.nuedits are
comment-only — verified to produce byte-identical IR, so the committed
bootstrapnurlc_lastgood.llis unchanged; the build still reaches its
fixed point and the full test suite passes. -
**`doc...
v0.9.3
[0.9.3] — 2026-05-31
Summary
A full Game Boy (DMG) emulator written in NURL now plays commercial
games with sound. examples/gameboy/ passes Blargg cpu_instrs 11/11,
instr_timing and 02-interrupts, is 100 %/pixel-perfect on dmg-acid2,
and runs Tobu Tobu Girl end to end — full gameplay plus a complete
4-channel APU mixed to stereo — in the browser at /gameboydemo via the
WebAssembly target. Building it drove three new language/compiler
features (hex/binary integer literals, pointer/aggregate global
initialisers, hex literals in match) and turned one
silently-accepted bare-literal statement into a hard compile error.
Generics now range over option and pointer element types — Vec ?T,
vec_get [?T] → ??T, ??T parameters/returns and nested ?? matching
all compile (five front-end root-cause fixes). The PostgreSQL client is
production-grade (stdlib/ext/postgres.nu + examples/psql.nu),
including option-typed nullable params and getters
(pg_exec_params_opt, pg_get_opt), verified live against
PostgreSQL 16 under AddressSanitizer.
HTTP/2 + HPACK + WebSocket conformance suites remain green: h2spec 2.6.0
reports 146/146 cases against examples/h2c_server.nu; the
autobahn-testsuite fuzzing client reports 294 OK / 4 NON-STRICT /
3 INFORMATIONAL / 0 FAILED across all 301 RFC 6455 cases against
examples/ws_echo.nu. Both binaries run under ASan + UBSan
without findings.
Bootstrap fixed point at 1 772 342 B (stage1 ≡ stage2 byte-identical
IR).
nurlc.wasm 501,204 bytes
Added
-
Game Boy (DMG) emulator —
examples/gameboy/. A cycle-aware Sharp
LR35902 core (every opcode + CB-prefix, exact Z/N/H/C flags + DAA,
EI/DI IME enable-delay, HALT + HALT-bug, DIV/TIMA timer, interrupt
dispatch) passing Blarggcpu_instrs11/11,instr_timingand
02-interrupts; a BG/window/sprite PPU that is 100 %/pixel-perfect
on dmg-acid2 (0/23040 diff — LYC raster + window internal line
counter); MBC1/3/5 mappers, joypad and OAM DMA; and a complete
4-channel APU (2 square w/ sweep, 4-bit wave RAM, 15-bit-LFSR
noise, 512 Hz frame sequencer, NR50/51 mix, DMG high-pass) mixed to
stereo. The engine is split into a sharedcore.nuwithgb.nu(CLI)
andgb_wasm*.nu(wasm32-wasi → canvas) front-ends; the browser demo
at/gameboydemoauto-starts Tobu Tobu Girl and plays it with
sound through the playground audio shim. Two sub-instruction timing
fixes (TIMA increments on the DIV falling edge; the fetch M-cycle is
clocked before the instruction body) took it from a title-screen crash
to full gameplay. Build the wasm at-O2(lower-Oleaks the C
shadow-stack pointer on the interrupt-dispatch path). -
Generics over option / pointer element types. Option (and pointer)
element types are now first-class generic type arguments:
vec_get [?String] → ??String,Vec ?T/vec_push/vec_set/
vec_free_with,??Tas a parameter and return type, and nested
?? o { T inner → ?? inner { … } }matching all compile — every one of
these previously failed at compile time. Five front-end root-cause
fixes, each verified by a full bootstrap + test-suite run and an
ASan-clean probe: (1)parse_type_optoptfor the fused??Ttoken;
(2)capture_type_arg_src+nurl_src_to_llvm+ anopt_
mangle/demangle round-trip so compound type args like[?String]are
one substitutable word; (3);-separated closure parameter types so an
aggregate type ({ i1, %String }) no longer truncates at its first
space; (4) slice-vs-pointer store discrimination; (5)int → aggregate
zeroinit. Test corpus on branchfeature/generic-option-types(PR #21). -
Hex / binary integer literals —
0xFF,0b1010. Added to the
number lexer; the token carries the parsed value and keeps its spelling
for diagnostics. Two companion compiler fixes: pointer- and
aggregate-typed global initialisers (: s g 0→global i8* null,
: String g 0→zeroinitializer,inttoptrfor a nonzero address),
and hex-literal normalisation inmatch(int-patterns?? op { 0xCB → … }and enum field-constraintsCode 0xFF → …are rewritten to
decimal before theicmp, since LLVM reads0x…as a hex float).
Regression testcompiler/tests/hex_literals.nu. -
Production-grade PostgreSQL client +
psqlCLI.
stdlib/ext/postgres.nureaches production grade: aPgParamsbuilder
(pg_bind_text/str/int/bool/null) for typed + NULL parameter binds the
libpq/pgx way,pg_prepare/pg_exec_prepared,pg_run,
pg_begin/commit/rollback, typed getters (pg_get_int/f64/bool),
pg_reset/pg_err_msg/pg_server_version/pg_escape_literal/
pg_escape_identifier, and — now that generics range over option types
— option-typed nullable params/getterspg_exec_params_opt ( Vec ?String ),pg_get_opt → ?String,pg_get_opt_int → ?i. New
examples/psql.nu(aligned-table renderer, command tags, multi-line
;accumulation,\dt \d \l \du \conninfometa-commands,-c "SQL"
one-shot) andexamples/pg_optional.nu. Verified live against
PostgreSQL 16 under ASan (PRs #20 / #22). -
Audio output in the WASM playground. An
env.audio_out_pushhost
shim streams packed-stereoi64samples to 48 kHz Web Audio, letting
WASM programs emit sound; demonstrated byexamples/audio_tone.nuand
used by the Game Boy demo's APU output. -
Trait bounds on generic functions —
[A: Trait]. A generic type
parameter may now carry one or more trait bounds:@ my_max [A: Ord] A x A y → A { … }. Trait-method dispatch inside a generic body already
resolved to the concreteimplthrough monomorphisation (dispatch is
keyed on the first argument's LLVM type, which becomes concrete at
instantiation); the bound adds the up-front guarantee.scan_impl_decl
now registers each% Trait Type {}asTrait##<llvm>in
g_trait_syms;gen_generic_fn_storerecords per-tparam bounds; and
check_generic_bounds(called fromgen_callat every generic call
site) verifies each bounded tparam's concrete type has the impl —
turning a missing impl from a cryptic unresolved-call link error into a
clear "type 'X' does not implement trait 'Y' required by bound A: Y"
diagnostic. Generic detection ingen_fn_declextended to recognise a
colon anywhere in the[…](a slice param's type never contains one).
This removes the need to passOrd/Hash/eqclosures into generic
helpers when animplexists. Testscompiler/tests/trait_bounds.nu
(positive, i + String) andshould_fail_trait_bound.nu(bound
violation → COMPILE FAIL). Bootstrap fixed point holds (stage1 ≡ stage2
byte-identical at 1 730 148 B). -
??match guards + or-patterns. Two additions togen_match:- Guards —
Pattern payloads ? <cond> → body. The guard is
evaluated after payload binding (so it can read the bound
payloads); a false guard falls through to the next arm. Implemented
by recording the guard's source span during arm parse and replaying
it vianurl_lex_set_posat the arm body, branching to the body or
the next arm. A guarded arm does NOT satisfy exhaustiveness for its
variant — a catch-all (unguarded or_) is still required. Not
allowed on a_wildcard arm or combined with an or-pattern. - Or-patterns —
A | B | C → body: several tag-only named
variants share one body (emit_or_chainlowers the alternatives to
a tag-compare chain). No payload binding or literal constraints; all
listed variants count toward exhaustiveness.
Test
compiler/tests/match_guards_or.nu. Bootstrap fixed point holds
(stage1 ≡ stage2 byte-identical at 1 720 428 B). - Guards —
-
Compile-time const folding for integer globals. A top-level
integer const (: i NAME …, or u / sized ints — notb) may now take
a prefix expression over integer literals instead of a single literal:
+ - * / << >> & | ^^(not%, which collides with the trait/impl
decl sigil at scan time).const_eval_intingen_const_declfolds it
to one value. Fixes the long-standing wart where e.g. the
two's-complement minimum needed a niladic helper —stdlib/std/int.nu
now exposes: i INT_MIN - -9223372036854775807 1directly
(int_min_valretained, delegating to it). Transparent (computes a
value, hides no control flow); fits the parse-directed architecture.
Testcompiler/tests/const_eval.nu. Bootstrap fixed point holds. -
selectover channels —?? { … }— Go-style select. A??
whose scrutinee is immediately{(no value to match) is a channel
select; each arm[T] ch → bind { body }receives from one channel
and the construct proceeds with the first ready arm. With no_
default it BLOCKS until some channel is ready (value sent or channel
closed); a_ → { … }default makes it non-blocking.bindis the
?Tthe receive yields (None ⇒ closed). Arms are heterogeneous (each
channel may carry a different element type) and tried in source order.
Implemented ingen_select(compiler/nurlc.nu) as a desugaring that
synthesises NURL source from the verbatim user channel-exprs + bodies
and compiles it through a sub-lexer — no raw IR, no new lexer token.
The blocking rendezvous (a sharedSelectWaiterarmed on every
channel, fired by senders/closers under the channel mutex) lives in
stdlib/std/channel.nuvia the type-erasedchan_raw_poll/
chan_raw_arm/chan_raw_disarm/select_waiter_*helpers — the
element type drops out of the orchestration, so one non-generic code
path serves channels of any type. Test
compiler/tests/select_basic.nu(deterministic default / value /
closed / priority cases always-on; concurrent blocking path gated on
NURL_NET_TESTS=1). Bootstra...
v0.9.2
[0.9.2] — 2026-05-28
Release summary
The "pure-NURL playground" release. play.nurl-lang.org no longer runs
Python at runtime — nurlapi/ is a 3 000+-LOC NURL HTTP server that
replaces the FastAPI playground, ships a full Model Context Protocol
server over /mcp (15 tools, 7 resources, 1 prompt), and serves five
cross-compile build targets end-to-end (native ELF, wasm32-wasi,
mingw-w64 PE32+, macOS Intel, macOS Apple Silicon + the rest of the
/build_target registry). One static NURL binary is PID 1 inside the
runtime image; the api/ Python container remains for parity testing.
The release also closes a long-standing reliability bug: a
nurl_poke / nurl_peek byte-vs-slot index overrun in
server_run_pool (and three other call sites) that scribbled 7×N
bytes past a worker-handles buffer. The symptom — random route /
closure corruption under thread load — had been misdiagnosed for
months as a Vec[T] stride hazard; ASan caught the real root cause
when nurlapi's router grew past 20 routes. Bootstrap fixed point
holds at 1 620 300 B (stage1 ≡ stage2 byte-identical IR).
Headline wins:
- Pure-NURL playground —
nurlapi/main.nu(980 → 3 094 LOC over
the release window) over the stdlib HTTP / router / json /
multipart / static stack. Zero Python at runtime; one static
binary as PID 1. - MCP server over
/mcp— Streamable HTTP transport, FastMCP
handshake parity, opaque stateless session IDs (survives container
restarts), 16-worker concurrent request handling (50 concurrent
tools/listin 65 ms wall on a single host). - Multi-stage Windows build fix —
clang -cthen
x86_64-w64-mingw32-gcclink. Closes the last "structured error
only, no real .exe" gap;/build_windowsnow produces a 1.18 MB
PE32+ executable end-to-end. http_routerHEAD + OPTIONS out of the box — every registered
route now answers HEAD (RFC 7231 §4.3.2) and OPTIONS (§4.3.7)
without handler changes.- Critical fix:
nurl_pokeslot-index overrun — four call sites
instdlib/ext/http_server.nu,stdlib/ext/postgres.nuand
compiler/tests/thread_basic.nu. ASan-verified regression test
nurl_poke_slot_index.nuadded; thehttp_routerboxed-handle
workaround comment block was rewritten to credit the real cause. - Playground init no longer blocks on a CDN —
@bjorn3/browser_ wasi_shimmoved from a top-level ES-module import to a lazy
import()insiderun(). The health pill and dropdowns now
initialise even when esm.sh is unreachable.
Full test corpus + sanitiser corpus (ASan + UBSan + LSan, 281 tests)
green. Bootstrap fixed point unchanged at 1 620 300 B.
Added — pure-NURL playground server (nurlapi/)
Replaces the Python FastAPI playground end-to-end. One static NURL
binary as PID 1; the runtime image is a slim Debian-bookworm stage
that only needs clang-16 + the cross-compile toolchains baked in.
Route surface (POST unless noted):
/build→ native Linux x86_64 ELF/build_wasm→ wasm32-wasi (usesprepare_ir_for_wasiIR shim;
wasm32 ABI rename + libc shims formalloc/puts/writeetc.)/build_windows→ mingw-w64 PE32+ via two-stageclang -cthen
x86_64-w64-mingw32-gcclink (static libcurl chain when the
runtime.win.curlmarker is present)/build_macos→ Mach-O via zig cc/build_target→ multi-target dispatch (linux-x64-musl,
linux-arm64-musl/-gnu, linux-riscv64-musl, macos-x64, macos-arm64,
windows-x64) over a single shared_build_zig_crosshelperGET /examples,GET /examples/*name— bundled example listing +
individual sourceGET /stdlib,GET /stdlib/*path— recursive .nu listing (84
entries) + per-file{name, source, bytes}JSONGET /tests,GET /tests/*path— same shape for the compiler
test corpus (281 entries)GET /targets— target registry (the dropdown the UI builds from)GET /readme//readme.md,/roadmap//roadmap.md,
/gotchas//gotchas.md,/grammar.ebnf— doc passthroughs +
HTML-rendered alternates (pure-NURL Markdown→HTML renderer:
headings, fenced code, bold/em, code spans, links, autolinks,
images, ul/ol, blockquote, hr; tables + nested lists deferred)GET /LICENSE-MIT,/LICENSE-APACHE,/NOTICE,/license,
/license/{mit,apache}— license endpoints (raw + HTML-wrapped)GET /openapi.json— minimal but valid OpenAPI 3.1 doc enumerating
every public routeGET /health→ toolchain status (60+ stdlib modules + per-tool
liveness)GET /mcp-info→ MCP server probe (tools / resources / prompts
catalogue + aclient_config_examplebuilt from the request's
Host header orNURL_PUBLIC_URL)GET /download/:id/:file→ built artifact download- OAuth 2.0 / OIDC rubber-stamp stubs so Claude-Desktop-class MCP
clients that probe.well-known/*succeed:oauth-protected- resource(+/mcp),oauth-authorization-server,openid- configuration,POST /register,GET /authorize(instant 302),
POST /token - Static UI at
/,/favicon.{ico,svg},/static/*,/*path
Build response shape mirrors api/ (Python) exactly: combined
stdout / stderr, parsed nurlc_errors[] ({file, line, col, message}), ll_artifact + binary_artifact with {name, bytes, download_url, token}, nurlc_returncode / clang_returncode,
uses_canvas / uses_audio. New shared helpers:
parse_nurlc_diagnostics, combine_stderr, make_artifact_json,
stamp_build_response, nurlc_failure_response,
push_native_runtime_libs.
nurlapi/Dockerfile — two-stage Debian bookworm image. Stage 1
bootstraps nurlc, downloads WASI SDK 24 + Zig 0.13, builds a static
libcurl-mingw for the Windows target, pre-builds the six zig-target
runtime objects + warms the zig per-target libc cache so the first
/build_target per target is a fast cache hit rather than a cold
multi-second libc compile, compiles the nurlapi binary itself.
Stage 2 is a slim runtime image (clang-16 + cross-compile toolchains
only, no Python).
nurlapi.sh — bring-up wrapper. ./nurlapi.sh builds + runs on
port 8000; ./nurlapi.sh bind skips the Docker rebuild and bind-
mounts the freshly-built local binary + stdlib + examples + tests
- root docs into the running container (inner dev loop: edit
.nu
→./nurlapi.sh bind→ hit endpoint, ~30 s vs ~10 min). Flags:
--no-cache/--port=N/--rm/--detach/--build-only/
--name=NAME/--help.
nurlapi/e2e_test.{py,sh} — end-to-end test driver covering every
endpoint.
Added — full MCP server over /mcp
stdlib/ext/mcp_http.nu + nurlapi/main.nu. Streamable HTTP
transport (POST /mcp), FastMCP handshake parity:
initialize(protocolVersion2025-03-26, FastMCP capabilities
shape,instructionsblurb verbatim, serverInfonurl-playground 0.9.2)ping,tools/list(15 tools),tools/call(dispatch by name)resources/list(7nurl://URIs),resources/readprompts/list(1 prompt:nurl_coding_assistant),prompts/getnotifications/*(no reply, 202 Accepted)- JSON-RPC 2.0 batches (top-level array → array reply,
notifications dropped per spec)
Tools (full Python parity):
- Build (5) —
nurl_build_native,nurl_build_wasm,
nurl_build_windows,nurl_build_macos,nurl_build_target
(enum-typed target id) - Browse (3) —
nurl_list_examples,nurl_list_stdlib,
nurl_list_tests - Read (7) —
nurl_read_example,nurl_read_stdlib,
nurl_read_test,nurl_read_grammar,nurl_read_readme,
nurl_read_roadmap,nurl_read_gotchas
Build tools dispatch via loopback HTTP to the server's own /build
endpoints (NURL_MCP_LOOPBACK_URL, default http://127.0.0.1:8000):
zero duplication of the canonical handler logic.
Reliability fixes vs Python reference:
- Session persistence across container restarts. Python
validatedMcp-Session-Idagainst a per-process in-memory
whitelist — fresh pod = fresh whitelist = previously-issued sids
rejected. NURL approach: session ID is opaque to the server.
First request (no sid) → server generates 16-hex-char sid, echoed
inMcp-Session-Idresponse header; subsequent requests with sid
→ echoed verbatim, no validation. Pod restart → client sid still
accepted, no reconnect required. - Burst handling. Python's asyncio event loop serialised
request handling. NURL piggybacks onserver_run_pool's
16-pthread worker pool — every POST /mcp gets its own worker
thread, no serialisation. Measured: 50 concurrenttools/list
in 65 ms wall on a single host (~770 req/s peak).
Streamable HTTP content-negotiation in stdlib/ext/mcp_http.nu:
when the client sends Accept: text/event-stream (every official
SDK does), the JSON-RPC reply is wrapped as event: message\r\ndata: <json>\r\n\r\n with Content-Type: text/event-stream +
Cache-Control: no-cache. Legacy clients without the Accept header
still get plain application/json.
Added — http_router HEAD + OPTIONS
router_handle now answers HEAD and OPTIONS requests without each
handler having to know about them — same shape FastAPI / Express /
most modern HTTP frameworks ship by default.
- HEAD (RFC 7231 §4.3.2): treated as a GET for the purpose of
route matching — falls through to the GET handler, then pins
Content-Lengthto the GET body's would-have-been length and
clears the body Vec so the wire-level serialisation emits headers- blank line + zero bytes.
- OPTIONS (RFC 7231 §4.3.7): the router answers directly, no
handler involved. Walks every registered route, collects distinct
methods whose pattern matches the request path, auto-adds HEAD
(when GET is among them) and OPTIONS (always), returns 204 No
Content with the assembledAllow:header. Root-level catc...
v0.9.1
[0.9.1] — 2026-05-26
Release summary
The "critic v0.9.0 backlog closes" release. v0.9.0's external peer
review (critic.md, 24 May 2026) distilled into IMPROVEMENTS.md
across Tiers A–E; v0.9.1 ships the entire remaining set (20 of 20
actionable items) and a handful of opportunistic perf + stdlib wins
that landed in the same window. IMPROVEMENTS.md is retired this
release — the surviving long-running item (Mobile / Embedded
targets) graduated to ROADMAP §4.
Headline wins:
- Borrow checker promoted to hard errors by default (BORROW.md
Phase 8 final). Five bug classes are now compile errors: use-after-
move, alias double-free, closure escape, aliased mutable-borrow at
call sites, iterator invalidation.--no-borrowckis the escape
hatch. - Networking complete: UDP datagrams (dual-stack IPv4/IPv6 with
multicast) + standalone DNS resolver (getaddrinfo/
getnameinfo) land alongside the existing TCP / TLS / HTTP stack. - JSON parser ~34× faster — pure-NURL
stdlib/ext/json.nu
rewritten around a single-pass scanner; 479 ms → 14 ms on the
bench/json_parsemicro-benchmark. - First peer benchmarks shipped —
bench/v1 compares NURL with
Python / Rust / Node on three reproducible micro-benches plus an
HTTP-server-vs-Rust-hyper / Node-http sweep. NURL parity with Rust
on compute-bound work; NURL holds the lowest tail latency across
the whole HTTP concurrency sweep (p99 0.62 ms at C=200 vs Rust's
6.19 ms). - CI lit up — GitHub Actions workflow with parallel build-test +
sanitizer (ASan + UBSan) jobs. - Formal language spec —
docs/spec.md(~1 000 lines) covers the
semantic side the grammar EBNF doesn't. - Generic signal handling + structured logging + silent-
miscompile diagnostics round out the stdlib + compiler surface.
Bootstrap fixed point at release time: stage1 ≡ stage2 byte-identical
IR at 1 620 300 B. Full test corpus + sanitizers green.
Added — UDP datagram sockets + full DNS resolver (Tier D #6, 2026-05-26)
Closes the last IMPROVEMENTS.md Tier D Roadmap §3 item the v0.9.0
critic flagged as missing for v1.0. Three new public surfaces:
-
stdlib/std/udp.nu— pure-NURL wrapper over runtime §18b. Dual-
stack IPv4/IPv6 by default (wildcardudp_bind("", 0)creates an
AF_INET6socket withIPV6_V6ONLY=0so a single fd serves both
v4 and v6 peers; literaludp_bind("127.0.0.1", 0)stays IPv4-only
on purpose). Sync + fiber-aware async on every send/recv: inside a
fiber,udp_recv_from/udp_send_topark on the reactor on
EAGAIN; outside any fiber, they fall back to blocking I/O
transparently. Exposed surface:- lifecycle:
udp_bind,udp_bind_any,udp_connect,udp_close - send/recv:
udp_send_to,udp_send_str_to,udp_recv_from,
udp_send,udp_recv(last two for connected-mode UDP) - address:
udp_peer_addr(borrowed),udp_local_addr(owned
String — how the caller discovers the kernel-assigned ephemeral
port afterudp_bind("", 0)) - options:
udp_set_timeout,udp_set_nonblock,udp_set_broadcast - multicast:
udp_join_group,udp_leave_group,
udp_set_multicast_ttl,udp_set_multicast_loop.ifacearg is
intentionally minimal (IPv4 IP literal or numeric ifindex; no
if_nametoindexso Win32 doesn't need-liphlpapi).
- lifecycle:
-
stdlib/std/dns.nu— pure-NURL wrapper over runtime §18c.
System-resolver-based (getaddrinfo/getnameinfo), no c-ares
dep. Three entry points:dns_resolve host→! ( Vec String ) NetErrof A/AAAA literals
in the kernel's preferred order, dedup'd.dns_resolve_port host port→ same, but each entry is formatted
as"ip:port"(IPv4) or"[ip]:port"(IPv6, RFC 3986 §3.2.2)
ready for directtcp_connect/udp_connect.dns_reverse ip→? String(Some only when there's a real PTR
record —NI_NAMEREQD).
-
Runtime §18b / §18c (
stdlib/runtime.c) — implementation:NurlUdp { fd, err_kind, family, peer }opaque handle (~16 % the
size ofNurlTcp— UDP has no TLS state to track).- 22 new
nurl_udp_*and 3 newnurl_dns_*exports; WASI stub
row at the bottom degrades every call toNetOther/
strdup("")sowasm32-wasibuilds still link. - Reuses §18's
NURL_NET_ERR_*error space +nurl__net_map_errno / _wsamapping helpers; multicast group-family branching uses a
newnurl__parse_numeric_addr(AI_NUMERICHOST) so callers
don't have to thread family flags through the NURL API. - DNS results come back as newline-separated heap strings; NURL
splits and dedupes are deferred to the wrapper (__dns_split_lines).
Acceptance:
compiler/tests/udp_basic.nu— always-on (loopback only, no live
network). Covers send_to/recv_from roundtrip, peer-addr capture,
connected-mode send/recv on a separate pair, zero-length datagram,
wildcard dual-stack bind, broadcast + multicast TTL / loop
setsockopt smoke. Exit 0, output matchescorrect.txtbaseline.compiler/tests/dns_basic.nu— always-on (uses literals + the
/etc/hostslocalhostmapping that every Linux/macOS/Win box
has, no external DNS). Covers resolve + resolve_port for both IPv4
and IPv6, IPv6 bracketed-port formatting, reverse lookup, empty-
input →Err NetOther.
Runtime LOC delta: stdlib/runtime.c 4 790 → 5 606 (+816, +17 %)
including the WASI stub row. Bootstrap fixed point unchanged at
1 620 300 B (stage1 ≡ stage2 byte-identical IR) — the runtime
extension is pure stdlib, no compiler IR perturbation.
Added — HTTP peer-bench (Tier D #3, 2026-05-25)
bench/run_http.sh + bench/http_server.{nu,js} +
bench/rust_http_server/ (Cargo + hyper 1.9 + tokio multi-thread)
close the long-standing "no Rust hyper / Node http peer comparison"
gap that critic v0.9.0 §10 flagged. Drives oha 1.8.0 against three
hello-world servers at four concurrency levels (1, 10, 50, 200),
median of 3 × 10 s per cell. Captured numbers + commentary in
bench/HTTP_RESULTS.md:
| Server | C = 1 | C = 10 | C = 50 | C = 200 |
|---|---|---|---|---|
| NURL | 14 451 | 68 960 | 60 897 | 59 044 |
| Rust | 14 507 | 47 703 | 86 699 | 114 694 |
| Node | 8 708 | 16 726 | 17 108 | 15 555 |
(req/s, higher is better, best in bold per column.)
Highlights:
- NURL is parity with Rust hyper at C=1 (within < 1 %), and is 1.45× ahead
of hyper at C=10 — NURL's 8-worker pool fits the workload while tokio's
12-worker default is over-provisioned at that concurrency. - Rust hyper pulls ahead at C ≥ 50, peaking at 115 k/s vs NURL's 59 k/s
(1.94×) at C=200. - NURL has the lowest tail latency across the whole sweep: p99 0.62 ms
at C=200 vs Rust's 6.19 ms and Node's 20.95 ms. - Node http plateaus at ~16 k/s — textbook single-event-loop signature.
The Go net/http half of the originally-asked-for comparison is
deferred: Go was not installed on the bench host at capture time.
bench/run_http.sh has the lane reserved and bench/README.md
documents the gap — a PR adding bench/http_server.go would re-publish
a four-column table.
Added — More examples + refreshed catalogue (Tier D #4, 2026-05-25)
Two-part deliverable closing ROADMAP §6 "More Examples":
-
examples/find_clone.nu— grep-style recursive search over
files / directories with three modes:find_clone PATTERN [PATH ...] # literal substring find_clone --list PAT[,PAT...] [PATH ...] # comma-separated alternatives find_clone --regex PAT [PATH ...] # POSIX-extended regexPATH is one or more files or directories — directories recurse,
dotfiles are skipped, and with no PATH the tool reads stdin. Output
ispath:line:contentsper match; exit 0 on any match, 1 on no
match, 2 on usage / I/O error. Closure-shaped matchers
(make_literal_test,make_list_test,make_regex_test) so
scan_linesis mode-agnostic;walk_dirreturns -1 on
not-a-directory so the dispatcher falls back to the file scanner
cleanly. Built on top ofstdlib/std/fs.nu(read_file,
dir_list),stdlib/ext/regex.nu(regex_compile/_test), and
the existingnurl_str_*helpers. Pure CLI I/O — runs on the
public playground. -
examples/README.mdrefresh — from a 3-of-36 catalogue to all
36 rows, organised by category (CLI tools / Algorithms / Data
formats / Language showcase / HTTP & RPC / LLM API / SDL canvas).
Each row carries a one-line description plus a playground or
local tag describing where it can run. playground = pure
compute + stdin / argv / file I/O (runs onplay.nurl-lang.org
as-is); local = needs network, a server listening port, an
ANTHROPIC_API_KEY, SDL2, or microphone access.
The critic-suggested agent-loop variants and MCP-client demo were
deliberately omitted: examples/claude_agent.nu already covers the
agent shape, and an MCP-client-from-the-public-playground would need
either secret injection (API keys) or WASM outbound sockets,
neither of which the playground exposes today.
Added — GitHub Actions CI (critic v0.9.0 Tier D #2, 2026-05-25)
.github/workflows/ci.yml lifts the previously-local build + test +
sanitiser gate to PR-level. Two parallel jobs on ubuntu-latest:
build-test— installs clang + optional FFI dev libs
(libcurl4-openssl-dev,libssl-dev,libsqlite3-dev,
libpq-dev,zlib1g-dev,libzstd-dev) so every
stdlib/runtime.<lib>sentinel lights up, then runs./build.sh
(bootstrap stage1 ≡ stage2 fixed point + the fullrun_tests.sh
corpus). 15-min timeout.sanitizers— same setup, run...
v0.9.0
[0.9.0] — 2026-05-24
Changed — refactor/nurlify branch
Picks up where refactor/pure-nurl left off and drives stdlib/runtime.c
the rest of the way down. Per the PURIFY.md tracker:
stdlib/runtime.c: 6 265 → 4 540 LOC (−1 725, −27.5 %). Combined
with the prior branch the total reduction since v0.8.0 is 8 879 →
4 540 LOC (−4 339, −48.9 %) — over half of the C runtime is gone.
Bootstrap fixed point held on every shipped phase; full test corpus
green.- PURIFY §17 random.
rand_u64/rand_hex_strported to pure
NURL instdlib/std/random.nu. Onlynurl_rand_fillstays C —
thegetrandom/arc4random_buf/BCryptGenRandomplatform
branching is genuinely syscall-shaped FFI. - PURIFY §4 file ops batch.
nurl_read_file_bytes/
_write_file_bytes/_file_read_chunk/_read_n_bytes/
_errno_kindmoved to pure NURL. Theg_last_bytes_lensideband is
gone —fread/fwritewrite directly into theVec[u]data
buffer andvec_set_lenrecords the count.EACCES/EPERM/
EEXISTadded tonurl_native_constant;errno_kindnow lives in
stdlib/core/posix.nu. - PURIFY §22 gzip.
nurl_gzip_compress/_decompressmoved to
pure-NURL FFI instdlib/ext/compress.nuoverdeflateInit2_/
deflate/deflateEnd+inflateInit2_/inflate/
inflateEnd. Two tiny C accessors (nurl_z_setup/
nurl_z_total_out) bridge the platform-varyingz_streamfield
layout (LP64 vs LLP64uLongwidth). - PURIFY §14 HTTP response accessors. The 7 accessor C functions
(status/err_kind/body/body_len/header_count/
header_name/header_value) deleted fromruntime.c. Pure-NURL
equivalents instdlib/ext/http.nuread theNurlHttpResponse
heap struct vianurl_peek(p, slot)over its 6-i64 slot layout.
Static asserts inruntime.cpin the layout at compile time so a
future field reorder breaks the native build instead of silently
miscompiling NURL reads.nurl_http_response_freestays C because
it walksheaders[]deallocating each name / value pair plus the
body. - PURIFY §14b HTTP libcurl backend + multi-stream orchestration
driven from pure NURL. Syncnurl_http_perform_full_toand
multi-stream_open_to/_next/_pump_headersplus the 5
stream accessors live instdlib/ext/http.nu; 22 monomorphic
trampolines stay C (nurl_curl_*setopt/multi/
stream-state) because libcurl's variadiccurl_easy_setoptand the
raw-fn-pointer callbacks (nurl__http_write_body/
_write_header) can't cross the FFI directly.NurlHttpStream's
three historicalintfields widened tolong longfor a clean
14×i64 slot layout;static_assertpins it. Live verified
against httpbin. - PURIFY §2 SIMD CSV scanner ported to pure NURL
(stdlib/ext/csv.nu, −508 C). The vectorised newline / delimiter
scanner is now NURL @-fns overnurl_peekof a heap-side byte
window. runtime.cprose cleanup (commitsd558844,f88d7bb):
trimmed verbose multi-paragraph explanations, phase-by-phase
migration history and prose that just restated what the code does
— kept one-line function-purpose intros and the non-obvious "why"
notes (TLS / SNI race discipline, fiber park-unlock ordering,
wasm32 layout caveats, libz LP64 / LLP64 differences). Net −913
comment-only LOC.
Changed — JSON-to-production branch
jsonext goes production-ready.stdlib/ext/json.nu:- Typed
JsonErrorreplaces the bareParseErr— carries
kind(BadFormat/Empty/TrailingGarbage/Overflow),
pos(0-based byte offset),line(1-based) andcol
(1-based). Location is computed once per failure and travels
with the error value — no global state, so nested
json_parsecalls and multi-threaded use are both safe.
json_format_errorrenders the standard message; build your
own from the fields if you need a custom shape. - RFC 8259 strict mode. Non-conforming numbers (leading zeros
like01,+5, lone.5,1.) are nowBadFormatinstead of
parsing to the prefix —json_stringify ∘ json_parseis
guaranteed-valid JSON. - New constructors —
json_int n/json_float xfrom
primitives (noi8*roundtrip),json_arr_new/json_obj_new
for empty containers. - Duplicate-key behavior documented. Parser preserves
duplicate keys as-is;json_obj_getreturns the first match in
source order;json_obj_setreplaces the first match in source
order. - Call sites updated across
stdlib/ext/anthropic.nu,
stdlib/ext/mcp{,_client,_http,_stdio}.nu,nurlapi/main.nu,
examples/serde_demo.nu,tools/nurl-lsp/jsonrpc.nu.
- Typed
Changed — refactor/pure-nurl branch
The refactor/pure-nurl branch (42 commits, 2026-05-23 → 2026-05-24)
took the bulk of stdlib/runtime.c out of C and into pure NURL —
either as pure-NURL @-fns or as direct & \c`/& `pthread`/& `sqlite3`` FFI declarations. Per the PURIFY.md tracker:
stdlib/runtime.c: 8 879 → 6 265 LOC (−2 614, −29.4 %). The
bootstrap fixed point held on every shipped phase and the full
test corpus stayed green.- Python removed from the bootstrap.
compiler/nurlc.pyand
compiler/src/*.pyare gone. Stage 0 now links the committed
compiler/nurlc_lastgood.llsnapshot directly via clang. The
only build-time dependency is clang/LLVM 14+. Refresh the
snapshot with./build.sh --refresh-bootstrapwhen a
grammar/runtime-ABI change leaves the current snapshot unable
to compile currentnurlc.nu. Box[T]/Cell[T]/Rc[T]/Arc[T]heap-stable
allocator surface —stdlib/core/box.nu,stdlib/core/cell.nu,
stdlib/std/rc.nu,stdlib/std/arc.nu.% Dropauto-fires;
nurl_native_sizeof+nurl_atomic_i64_*runtime primitives
added. This unblocked Phases 6 / 8 / 11 / 12 of the purification.- PURIFY phases shipped (per-phase detail in PURIFY.md Part VII):
- Phase 1 §3 char classification (
stdlib/core/char.nu, −11 C) - Phase 2 §15 logging level (
stdlib/std/log.nu, −7 C) - Phase 3 §11 libm + integer helpers (
& \m`/& `c`` FFI, −17 C) - Phase 4 §17 crypto MD5/SHA-1/256/512 + HMAC
(stdlib/std/hash_*.nu, −541 C) - Phase 5 §2 string ops over libc (strlen/strcmp/strncmp/strstr/
memcmp/memmem/atoll/atof/memcpy/strdup via preamble, −682 C) - Phase 6 §19 threads / mutex / cond (pthread
& \pthread`FFI instdlib/std/thread.nu, −162 C; mingw-w64 winpthreads linked via-lpthread`) - Phase 7 §4 + §13 file & dir syscalls — incremental over many
batches (realpath / write_file_safe / file_size / mmap / fread
fallback / dir_list POSIX, −158 C combined) - Phase 8 §16 + §16b process spawn (fork/exec/poll,
||and
&&added as language tokens for the spawn-error sideband,
−245 C) - Phase 9a §7 + §8 codegen counters + last-type sideband
(pure-NURL @-fns innurlc.nu, −71 C) - Phase 9b §6b symbol table (3 parallel grow-by-2× arrays,
inner loops via direct*s/*ipointer arithmetic, −72 C;
~0.95× of C runtime — LTO inlines everything and the parallel
layout is cache-friendlier than the C interleaved struct) - Phase 9c §5 HashMap deleted entirely (the canonical
stdlib/std/hashmap.nuHashMap[s i] is the one-true map for
every consumer; the migration also fixedhash_stringfrom
O(n²) → O(n) by switching from per-bytenurl_str_getto a
direct*ubyte walk, −101 C) - Phase 10 §6a Lexer (the big one, −592 C). Full state
machine + 4-deep lookahead ported to pure-NURL @-fns over a
280-byte heap handle. Uncovered + fixed a subtle escape-handling
bug: only\n \t \r \\are real escapes; any other\X
(including\`) writes the lone\and advances one byte. - Phase 11 §23 DoS protection (
stdlib/std/dos.nu, −180 C) - Phase 12 §21 SQLite bridge — pure-NURL FFI over 18 libsqlite3
symbols (stdlib/ext/sqlite.nu, −330 C) - §12 Time —
clock_gettime+nanosleepFFI (stdlib/std/time.nu,
−38 C; macOS usesCLOCK_MONOTONIC = 6vs1elsewhere, read
at runtime vianurl_native_constant) - §13 batch 2/3 — stdin + dir_list POSIX FFI (−80 C)
- §11 strtod sideband eliminated with an endptr buffer (−20 C)
- Phase 1 §3 char classification (
||and&&operators added as language tokens — strict
binary, bool-only short-circuit. Alternative to the chainable
|/&for cases that are more readable as a||/&&
chain. Grammar v2.0 documents them. Same LLVM IR as|/&
on i1 left operands../check.sh <file.nu>— per-file syntax/type check tool;
runsnurlcagainst a single source file in ~0.2 s vs build.sh
~60 s. Use in iterate-fix loops before kicking the full build.- Test runner output split into
success.txt+failures.txt
so a failed test is greppable without scrolling through the
green output. - Parenthesised-operator diagnostic. A
(begins a call, so
( . obj field )/( | a b )/( + x y )etc. now produce
a precise call-siteerror:instead of a far-away LLVM-verifier
complaint. (Listed earlier in this section under the original
feature work; reiterated here as it landed in this branch.) - Call-arity diagnostics. Every call's argument count is
checked against the callee's declared parameter count; a
mismatch points at the call site (same listing remark). - Prefix arity-cascade diagnostic. Short-an-argument prefix
operator over-reads now name the offending token and point back
at the line where the cascade started. mcp_response_get_result—mcp_client's 1-arg result
extractor renamed for consistency with the rest of the surface.
Fixed — `refactor/pure-n...
v0.8.0
[0.8.0] — 2026-05-20
Added
-
Native
^^XOR operator. Two adjacent carets lex as a single
^^token (the lexer pairs them only when adjacent —^ ^with a
space is still two return tokens).^^is a strictly-binary
operator lowered to LLVMxor: bitwise XOR on integer operands,
logical XOR onboperands. Float operands are a compile error
(LLVM has no floatxor). Replaces the old(a | b) - (a & b)
identity workaround.^alone remains the return operator.
Grammar (spec/grammar.ebnf) andnurlfmtupdated; regression
testsxor_op.nu+should_fail_xor_float.nu. -
inoutandsinkparameter conventions (BORROW.md Phase 4,
Option B — mutable value semantics).in/inout/sinkare
contextual keywords recognised only as a parameter's leading token
(no lexer change);inis the default.
A parameter markedinoutis an exclusive mutable borrow: the
callee mutates the caller's binding in place.inout Tlowers to a
by-address<T>*parameter — the body reads/writes the caller's
storage with no local copy — replacing the*T-parameter and
return-the-struct mutation idioms. The argument must be a mutable
(: ~) binding; aninoutfunction must be defined before it is
called. Exclusive-access check (BORROW.md Phase 5): a binding
passedinoutmust be the only argument path to its value at that
call — passing it again, as a secondinoutor a plain by-value
argument, is awarning:.
A parameter markedsinkconsumes (takes ownership of) its
argument: it lowers to an ordinary by-value parameter, and the
borrow checker records the argument binding as moved so a later
use is a use-after-move.sinkv1 applies toVecand other
manually-managed handles; passing a compiler-auto-dropped value
(owned string / slice /Dropvalue / struct with owned fields)
to asinkparameter is rejected pending drop-ownership transfer. -
Static borrow checker, on by default (BORROW.md Phases 0-3 + 6 +
8-partial). A diagnostic analysis pass (disable with
--no-borrowck) that never changes generated code — a
borrow-clean program compiles to byte-identical IR. Closes four
bug classes withwarning:diagnostics: use-after-move (a binding
read after its ownership moved), alias double-free (: T b aof an
owned heap value movesa), stack-reference escape (a closure
capturing a: ~-mutable struct by pointer that is returned,
pushed into a container, spawned onto a thread, or assigned into a
longer-lived binding — a region-based check), and iterator
invalidation (mutating a container —vec_push/vec_free/… — from
inside a~-foreach that iterates it). Ownership + borrow rules
documented in the newdocs/MEMORY.md. -
Tail-call optimisation in the @-fn dispatch path.
gen_ret
now flags the upcoming return-value expression as
tail-position;gen_callsnapshots + clears the flag on entry,
so only the outermost call in the return expression is treated
as tail (argument-evaluation recursions stay non-tail). In the
regular @-fn dispatch path the LLVMcallbecomestail call
when (a) the flag was set, (b)rlt == fn_ret_tyso LLVM
accepts the marker, (c) the callee is not variadic, and (d)
gen_retsaw no pending owned-string / owned-slice / owned-
struct-field / user-drop / defer in scope at flag-set time
(any of those would emit drop calls between the tail call and
ret, which LLVM would silently demote).Deliberately chose
tailovermusttail:tailis a hint
LLVM may drop when its safety analysis can't confirm the
rewrite (alloca-escape through an arg, etc.), so a
misclassification only costs an optimisation.musttailis
verifier-enforced and would fail on NURL's owning ABI where
the same source-level signature lowers to different LLVM
types across call sites.Effect: tail-recursive functions no longer blow the stack —
compiler/tests/tco_deep_recursion.nuruns a 5_000_000-deep
countdown in O(1) stack (~7 ms wall-clock). Trait/impl,
closure-loaded var, and fn-pointer-parameter dispatch paths
intentionally still emit a plaincall(different shapes; not
the deep-recursion targets TCO exists for).Coexists with
--gDWARF emission:tools/dwarf_test.shstill
passes all five phases. -
DWARF Phase 6 composite-type rendering. User structs and
generic-instantiation handles (%Vec__u8,%String,%FmtTok,
user% Point, …) now resolve undernurlc --gto a
!DICompositeType(tag: DW_TAG_structure_type, …)carrying one
!DIDerivedType(tag: DW_TAG_member, …)per field — instead of
the previous i64 placeholder.gdb ptype Pointlists the fields
with their NURL names + base types;print prenders the value
as{x = 3, y = 7};print p.xevaluates a single field.Field roster lives in the existing symbol table next to the
per-field__idx_N__typeentries —gen_struct_decland the
generic-instantiation emitter now also record
<sname>__field_countand<sname>__idx_N__name. New helpers
dbg_size_bits/dbg_align_bits/dbg_align_upcompute
LLVM-natural cumulative field offsets so the emitted
!DIDerivedTypemember offsets match the actual layout
clang/LLVM uses. Self-referential structs (a cell holding a
pointer to itself, etc.) are safe — the composite id is interned
ing_dbg_type_symsbefore the per-field recursion descends
throughdbg_type_id_for, so a back-edge returns the cached id
instead of looping.Regression:
compiler/tests/dwarf_struct.nuexercises the
codegen path in the standard test corpus;tools/dwarf_test.sh
picks up a fifth phase that drives gdb in batch mode to assert
ptype+print+ field-access over the new test. Bootstrap
fixed point holds — non-debug IR is byte-identical.Closes the open Phase 6 follow-up in
DWARF.md. Phase 7
(per-instantiation source-line precision for generics) remains
deferred.