Skip to content

Releases: nurl-lang/nurl

v0.9.8

15 Jun 20:23
d03d981

Choose a tag to compare

[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 in docs/DISTRIBUTED.md.

  • The Crown — distributed computation (§7.5). Turns the distributed state
    above into distributed work. dist/identity.nu gives each peer a replica
    id; dist/job.nu is the keystone — submit a task keyed by k, 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.nu adds 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.sh now detects
    libopus and ALSA (dropping stdlib/runtime.{opus,asound} sentinels)
    and nurl.sh auto-links -lopus/-lasound when those FFI symbols appear.

  • Playground “🎙️ PTT Chat” demo with channels. A new /pptchat tab: a
    page with a microphone button and an embedded NURL→WebAssembly module
    (nurlapi/static/pptchat.nupptchat.wasm) that reads the mic through the
    audio FFI and paints a live VU meter + frequency spectrum, framing the
    distributed voice tech. Channels: no id → the shared public channel;
    + 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.sh now 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 writes Set-Cookie (ext/http_auth.nu); this is the missing
    client half. cookie_jar_set parses one Set-Cookie value (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_header returns the Cookie:
    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.nu times a no-arg closure over many iterations and
    reports ns/op (via the monotonic clock) and allocations/op.
    bench_run name iters body runs a short untimed warmup then a timed
    loop; bench_auto name body auto-scales the iteration count until a
    pass clears ~50 ms, for stable numbers on sub-microsecond operations.
    bench_report prints 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 bench discovers benches/*.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). Ships bench/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 test discovers
    tests/*.nu, compiles and runs each, and reports PASS/FAIL with a
    summary (exit 0 iff every test passes). A test passes on exit 0; if a
    tests/outputs/<name>.txt golden exists, the program's stdout must
    match it byte-for-byte instead. Tests run in sorted order for
    determinism. The build driver is ./nurl.sh by 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 fresh main, compiled with ./nurl.sh -O0, and run — its stdout
    is echoed back. A new definition is validated by a fast build/nurlc
    frontend pass before it joins the session, so a typo never poisons later
    evaluations. Line editing + history come from std/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 by compiler/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-backed bitset_count, bitset_any / bitset_all /
    bitset_none, the in-place combiners bitset_and_with / bitset_or_with
    / bitset_xor_with, bitset_clone, and an ascending bitset_each_set.
    Storage is a flat nurl_zalloc word buffer peeked/poked by limb. NURL
    has no native XOR or NOT operator, so the module uses the exact,
    carry-free identities a ^ 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-capacity LruCache [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 — so lru_get / lru_put /
    lru_contains / lru_remove are all O(1) and a c...

Read more

v0.9.7

11 Jun 07:18
0fa1947

Choose a tag to compare

[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-bounds extractvalue / broken store IR, 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 in parse_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), and should_warn_caret_xor.nu now
    also catches the previously silent dead b in : 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 Delight divmnu trigger 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%y holds for every y ≠ 0. Division by zero panics
    (recoverable via recover) — a defect, not a data error, so it is not
    threaded through !. Regression compiler/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.
    ^ v where v is 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_fields path — 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 *_free conventions: only raw-s/slice fields filled
    by a fresh allocation in a direct agg-literal return register for
    transfer — stdlib's struct returns use String/Vec handle 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/leakcheck zero, suite 340 PASS,
    and a targeted incremental-build manual-free probe stays single-drop.
    Regression ret_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 publish void.
    (2) __fn_ret_str_owned__ was only set for identifier returns, so
    @ helper → s { ^ ( nurl_str_cat … ) } was never marked
    __ret_owned=str and : 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 x and 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 *_free handle conventions).

  • server_stop from another thread freed the listener under blocked
    pool workers
    (stdlib/ext/http_server.nu). server_run_pool's
    documented shutdown — call server_stop s from another thread while
    workers block in accept — was a heap-use-after-free: workers hold no
    reference on the listener, so the stop's nurl_tcp_close dropped the
    last ref and freed the struct while every worker was still polling
    its shutting_down flag and wake-pipe fd (3/3 reproducible under
    ASan; single-threaded server_run raced identically). server_run
    and server_run_pool now retain the listener for the whole
    run→join window and release it only after no worker can touch the
    handle — the same contract server_run_async already followed for
    its accept fiber. The two-phase tcp_shutdown_listener → join →
    server_stop pattern remains valid; it is simply no longer the only
    safe shutdown. Regression compiler/tests/http_server_stop_direct.nu
    drives both fixed paths with a direct cross-thread stop (ASan-clean
    10/10 under NURL_NET_TESTS=1). Closes critic.md B19 together with
    the earlier accept-wake fix (f470571).

  • recover leaked the closure's captured environment
    (stdlib/std/panic.nu). recover decomposes 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 in thread_spawn, whose
    shape this mirrors, it really does). But nurl_recover is
    synchronous: once it returns, the closure can never run again, so the
    env was simply leaked — one allocation per recover call with a
    capturing closure, panic or not. recover now frees the env right
    after nurl_recover returns (NULL-safe for capture-less closures),
    on both the normal and the unwind path. Found via ASan on the new
    bigint_div divide-by-zero regression; the existing
    recover_basic / http_server_panic goldens are unaffected (output
    is unchanged — only the leak is gone).

  • **...

Read more

v0.9.6

08 Jun 06:55
33b658f

Choose a tag to compare

[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 + ALPN h2) /
    h2_client_connect_h2c / h2_client_attachh2_client_submit (N
    concurrent streams) → h2_client_run_until_complete
    h2_client_take_response, plus one-shot h2_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. Example examples/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: an McpSessionStore keyed by a CSPRNG Mcp-Session-Id, a per-session
    outbound notification queue, and server→client reverse-RPC correlation.
    mcp_http.nu gains mcp_http_handler_session (mints/validates session ids,
    drains the queue to SSE on GET, settles reverse-RPC responses, tears down on
    DELETE) plus chunked mcp_sse_* helpers for a long-lived stream.

  • MCP server: completion/complete argument autocompletion.
    mcp_registry_add_completion r ref_type ref_id handler registers a
    completion provider for a prompt argument (ref/prompt) or resource template
    (ref/resource); completion/complete resolves 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, and mcp_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's Mcp-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 an Xml tree;
    xml_stringify round-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 shared Json value (so every json_* 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_stringify emits 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_sz returns 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 → *T allocates
    count contiguous items of T. arena_new / arena_with_cap remain 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
    existing Mutex + 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_readlink reads a symlink's
    target as an owned String via 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 to std/random.nu's OS CSPRNG, which draws from the kernel
    entropy pool and cannot be seeded. rng.nu gives a reproducible stream:
    ( rng_seed s ) expands any i64 seed into a 256-bit xoshiro256** state
    via SplitMix64 (so even 0/1 produce 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
    lshr on the u64-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 as Arena/Channel/String); state mutates in
    place. Not a CSPRNG — predictable, so never for security; use
    std/random.nu there. Regression compiler/tests/rng_seedable.nu pins
    the exact stream for seeds 0 and 0x1234 against an independent
    reference implementation, and checks determinism, rng_below bounds, 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 /mcp take a permit before dispatch and release after (panic-safe),
    default 4, tunable via NURL_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-Drop for 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-Drop enum 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

04 Jun 21:39
ac18964

Choose a tag to compare

[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 in index.html is stamped at image-build
    time from the NURL_VERSION build-arg (dev for local builds). A new
    .github/workflows/api-deploy.yml builds the API image on a v* tag (or
    manual dispatch), pushes it to Docker Hub under the exact semver
    (nurllang/nurl:vX.Y.Z — no :latest), pins cloudflare/Dockerfile's
    FROM to that tag and runs wrangler deploy, so a git tag is now a
    reproducible playground release. The Docker image was renamed
    hindurable/nurlnurllang/nurl; registry-deploy.yml is 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 the mqtt subprotocol automatically; the codec
    and framed packet reader stay transport-blind behind two chokepoints. New
    entrypoints mqtt_connect_ws / mqtt_connect_ws_cfg; mqtt_disconnect
    also sends a WS Close frame, and mqtt_reconnect rejects 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 login stores a per-registry publish token
      in ~/.nurl/credentials (chmod 600) — publish/yank resolve 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> and nurlpkg info <name>
      query the registry (info with no arg still prints the local manifest).
      New stdlib/ext/credentials.nu.
    • Registry hygiene. nurlpkg yank|unyank <name> <version> flips a
      version's yanked flag (owner-only, via POST /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). Regression compiler/tests/credentials_basic.nu
      (set/get/upsert/multi-registry/remove; gated NURL_CREDS_TESTS=1, clean
      under ASan/UBSan). Whole feature set verified end-to-end against the
      Worker under wrangler dev: login → creds-based publish → search → info
      → yank (install then fails ResolveNoMatch) → unyank → catalog → logout
      --revoke → publish rejected (PubAuth).
  • Transitive registry dependencies — nurlpkg publish sends X-Nurl-Deps.
    Publishing now includes the manifest's registry dependencies (a JSON
    [{name, req}] built by __deps_json) as the X-Nurl-Deps header, which
    the registry records in the package index. pkg_publish gained a
    deps_json parameter. With the deps in the index, resolve_registry
    pulls sub-dependencies transitively — previously the index always
    recorded deps: [], so only leaf registry packages installed correctly.
    Verified end-to-end against the local Cloudflare Worker: publish tdep-b,
    publish tdep-a (depends on tdep-b ^1.0), then install a consumer of
    only tdep-a → both land in deps/ 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 static index/<name>.json +
    content-addressed pkgs/<name>/<name>-<v>.tar.gz from R2 (cacheable, no
    compute); the write path POST /api/v1/publish authenticates 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/callback mints 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 real nurlpkg binary completes a
    full publish → install round-trip plus immutability (409) and bad-token
    (401) rejections — registry/test-local.sh. Ships with
    registry/DEPLOY.md, a registry-deploy.yml GitHub Actions workflow
    (guarded so a placeholder token can't trigger a broken deploy), and
    secrets kept out of the repo (wrangler secret put for
    GITHUB_CLIENT_SECRET / TOKEN_PEPPER; GH Actions secrets for the
    Cloudflare deploy token). This completes the registry-backed package
    manager: nurlpkg publish + nurlpkg install against 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_pack walks a project tree
    into a .tar.gz (excluding deps, .git/dotfiles, nurl.lock,
    target, build); pkg_publish uploads it with POST <registry>/api/v1/publish, Authorization: Bearer <token>, and
    X-Nurl-Package / X-Nurl-Version headers (binary body via
    http_request_bytes), mapping status to PubAuth (401/403) / PubConflict
    (409, version immutability) / PubRejected. nurlpkg publish packs the
    current project, prints its size + SHA-256, and uploads using the token
    from $NURL_TOKEN and 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
    python registry: a full publish → install round-trip (a library
    packed, uploaded with a Bearer token, then resolved + installed into a
    consumer's deps/), 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 resolved LockPkg into files on
    disk against a static-HTTP registry (R2 + CDN shape). pkg_fetch_index
    GETs <registry>/index/<name>.json; pkg_install_one downloads
    <registry>/pkgs/<name>/<name>-<v>.tar.gz, verifies its SHA-256
    against the recorded checksum
    , gunzips, and path-safe tar_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.nu stands up a loopback NURL registry
    server
    (serves a real tar_create+gzip tarball + 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 install is now registry-aware. It resolves the manifest's
    registry deps (foo = "^1.2" or { version, registry }), downloads +
    verifies + unpacks each into deps/<name>, and writes a nurl.lock
    whose registry entries carry source = "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 makes install exit non-zero.
    Verified end-to-end against a static python -m http.server registry
    serving a GNU-tar --format=ustar | gzip package (differential interop):
    the happy path installs + locks with the sha256sum-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_str reads the response body through a NUL-terminated carrier
    (truncates at the first embedded NUL). The new http_body_bytes returns
    an owned, length-accurate ( Vec u ) copy, and http_body_len exposes
    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-byte A B \0 C D body; 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, yanked flag, and deps); tarballs live at the
    content-addressed `/pkgs//<...

Read more

v0.9.4

02 Jun 04:51
8845c57

Choose a tag to compare

Added

  • Keyword arguments — default parameter values + named call arguments.
    A trailing parameter may carry a default: @ f s a s b = x i 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_sigs records each function's parameter names + default sources;
    gen_call fills omitted trailing defaults inline, and routes a call that
    uses name: labels through gen_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 the inout/sink convention; **kwargs-style collection is not
    provided (pass a Json/struct).

  • BLAKE3 hash (pure NURL) — completes the hash family. New
    stdlib/std/hash_blake3.nu implements 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 via blake3_bytes /
    blake3_hex in stdlib/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); regression compiler/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_store compiler intrinsics for MMIO. Emit
    load volatile / store volatile as 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 -O0 workaround. Regression:
    compiler/tests/volatile_mmio.nu; verified at -O2 the 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's soc/*_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 single core.nu engine 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
    BASIC READY. prompt.

Fixed

  • nurlfmt split hex/binary/octal integer literals. The tokenizer's
    numeric scanner stopped at the first non-decimal digit, so 0x3FF44008
    became two tokens (0 + identifier x3FF44008) and the reformatted
    source miscompiled — silently, because --check is idempotent on its own
    broken output. tools/nurlfmt/tokenize.nu now scans a 0x/0b/0o
    prefix and its body as one token. Verified by the
    nurlfmt_idempotent.sh gate (450 files, IR-transparent) and by restoring
    the hex literals in the examples/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_text reads the column's exact
      byte length via sqlite3_column_bytes (was strlen, which truncated
      at the first embedded NUL), and sqlite_bind_text now takes a String
      and passes an explicit byte length to sqlite3_bind_text instead of
      -1 — strings with embedded NULs round-trip intact.
    • BLOB support. New sqlite_bind_blob (Vec usqlite3_bind_blob
      • SQLITE_TRANSIENT) and sqlite_column_blob (sqlite3_column_blob +
        _bytes → owned Vec u) — the binary-safe write/read path.
    • sqlite_open_v2 with open flags. SQLITE_OPEN_READONLY /
      READWRITE / CREATE / URI / NOMUTEX / FULLMUTEX / NOFOLLOW
      constants exposed; sqlite_open is now READWRITE|CREATE over
      open_v2. A read-only connection refuses writes (new SqliteReadOnly
      error variant) instead of silently creating a file.
    • sqlite_busy_timeout wraps sqlite3_busy_timeout so SQLITE_BUSY
      blocks-and-retries under concurrent access rather than failing
      immediately.
    • % Drop auto-close. Database and Statement implement the Drop
      trait; a scope-local handle — including one unwrapped from a
      ! Database E / ! Statement E result 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_transaction that COMMITs on Ok and ROLLBACKs on Err
      (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). Added sqlite_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-based sqlite_set_authorizer /
      sqlite_clear_authorizer that installs a sandbox callback with the
      exact C ABI libsqlite expects (the closure's compiled function +
      captured env are passed as xAuth + pUserData, the same mechanism
      thread_spawn uses for pthread_create — no C bridge); and PRAGMA
      helpers sqlite_journal_wal / sqlite_foreign_keys /
      sqlite_synchronous. Verified under ASan + UBSan
      (compiler/tests/sqlite_tier34.nu).

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 at spec/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
    -lcurl
    sentinel-gated linking, and the nurlc_lastgood.nu refresh
    lifecycle (documented via --refresh-bootstrap). Added an explicit
    "What's actually left" summary to the Status section (HTTP/2+WebSocket
    client-side; mobile/no_std targets; SQLite BLOB/double; reverse-proxy
    binary bodies; blake3; MCP SSE/sessions/auth; the runtime.c file-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 / §N references out of code comments.
    After docs/GOTCHAS.md lost its numbered list, ~44 source comments (in
    compiler/nurlc.nu, the nurlc_lastgood.nu snapshot mirror, nine
    compiler/tests/*.nu, and stdlib/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. The nurlc_lastgood.nu edits are
    comment-only — verified to produce byte-identical IR, so the committed
    bootstrap nurlc_lastgood.ll is unchanged; the build still reaches its
    fixed point and the full test suite passes.

  • **`doc...

Read more

v0.9.3

31 May 13:02

Choose a tag to compare

[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 typesVec ?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 Blargg cpu_instrs 11/11, instr_timing and
    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 shared core.nu with gb.nu (CLI)
    and gb_wasm*.nu (wasm32-wasi → canvas) front-ends; the browser demo
    at /gameboydemo auto-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 -O leaks 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, ??T as 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_optopt for the fused ??T token;
    (2) capture_type_arg_src + nurl_src_to_llvm + an opt_
    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 branch feature/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 0global i8* null,
    : String g 0zeroinitializer, inttoptr for a nonzero address),
    and hex-literal normalisation in match (int-patterns ?? op { 0xCB → … } and enum field-constraints Code 0xFF → … are rewritten to
    decimal before the icmp, since LLVM reads 0x… as a hex float).
    Regression test compiler/tests/hex_literals.nu.

  • Production-grade PostgreSQL client + psql CLI.
    stdlib/ext/postgres.nu reaches production grade: a PgParams builder
    (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/getters pg_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 \conninfo meta-commands, -c "SQL"
    one-shot) and examples/pg_optional.nu. Verified live against
    PostgreSQL 16 under ASan (PRs #20 / #22).

  • Audio output in the WASM playground. An env.audio_out_push host
    shim streams packed-stereo i64 samples to 48 kHz Web Audio, letting
    WASM programs emit sound; demonstrated by examples/audio_tone.nu and
    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 concrete impl through 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 {} as Trait##<llvm> in
    g_trait_syms; gen_generic_fn_store records per-tparam bounds; and
    check_generic_bounds (called from gen_call at 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 in gen_fn_decl extended to recognise a
    colon anywhere in the […] (a slice param's type never contains one).
    This removes the need to pass Ord/Hash/eq closures into generic
    helpers when an impl exists. Tests compiler/tests/trait_bounds.nu
    (positive, i + String) and should_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 to gen_match:

    • GuardsPattern 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 via nurl_lex_set_pos at 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-patternsA | B | C → body: several tag-only named
      variants share one body (emit_or_chain lowers 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).

  • Compile-time const folding for integer globals. A top-level
    integer const (: i NAME …, or u / sized ints — not b) 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_int in gen_const_decl folds 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 1 directly
    (int_min_val retained, delegating to it). Transparent (computes a
    value, hides no control flow); fits the parse-directed architecture.
    Test compiler/tests/const_eval.nu. Bootstrap fixed point holds.

  • select over 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. bind is the
    ?T the receive yields (None ⇒ closed). Arms are heterogeneous (each
    channel may carry a different element type) and tried in source order.
    Implemented in gen_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 shared SelectWaiter armed on every
    channel, fired by senders/closers under the channel mutex) lives in
    stdlib/std/channel.nu via the type-erased chan_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...

Read more

v0.9.2

28 May 04:16
557dc12

Choose a tag to compare

[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 playgroundnurlapi/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/list in 65 ms wall on a single host).
  • Multi-stage Windows build fixclang -c then
    x86_64-w64-mingw32-gcc link. Closes the last "structured error
    only, no real .exe" gap; /build_windows now produces a 1.18 MB
    PE32+ executable end-to-end.
  • http_router HEAD + 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_poke slot-index overrun — four call sites
    in stdlib/ext/http_server.nu, stdlib/ext/postgres.nu and
    compiler/tests/thread_basic.nu. ASan-verified regression test
    nurl_poke_slot_index.nu added; the http_router boxed-handle
    workaround comment block was rewritten to credit the real cause.
  • Playground init no longer blocks on a CDN@bjorn3/browser_ wasi_shim moved from a top-level ES-module import to a lazy
    import() inside run(). 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 (uses prepare_ir_for_wasi IR shim;
    wasm32 ABI rename + libc shims for malloc / puts / write etc.)
  • /build_windows → mingw-w64 PE32+ via two-stage clang -c then
    x86_64-w64-mingw32-gcc link (static libcurl chain when the
    runtime.win.curl marker 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_cross helper
  • GET /examples, GET /examples/*name — bundled example listing +
    individual source
  • GET /stdlib, GET /stdlib/*path — recursive .nu listing (84
    entries) + per-file {name, source, bytes} JSON
  • GET /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 route
  • GET /health → toolchain status (60+ stdlib modules + per-tool
    liveness)
  • GET /mcp-info → MCP server probe (tools / resources / prompts
    catalogue + a client_config_example built from the request's
    Host header or NURL_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 (protocolVersion 2025-03-26, FastMCP capabilities
    shape, instructions blurb verbatim, serverInfo nurl-playground 0.9.2)
  • ping, tools/list (15 tools), tools/call (dispatch by name)
  • resources/list (7 nurl:// URIs), resources/read
  • prompts/list (1 prompt: nurl_coding_assistant), prompts/get
  • notifications/* (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:

  1. Session persistence across container restarts. Python
    validated Mcp-Session-Id against 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
    in Mcp-Session-Id response header; subsequent requests with sid
    → echoed verbatim, no validation. Pod restart → client sid still
    accepted, no reconnect required.
  2. Burst handling. Python's asyncio event loop serialised
    request handling. NURL piggybacks on server_run_pool's
    16-pthread worker pool — every POST /mcp gets its own worker
    thread, no serialisation. Measured: 50 concurrent tools/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-Length to 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 assembled Allow: header. Root-level catc...
Read more

v0.9.1

26 May 14:14
5e92af9

Choose a tag to compare

[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-borrowck is 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_parse micro-benchmark.
  • First peer benchmarks shippedbench/ 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 specdocs/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:

  1. stdlib/std/udp.nu — pure-NURL wrapper over runtime §18b. Dual-
    stack IPv4/IPv6 by default (wildcard udp_bind("", 0) creates an
    AF_INET6 socket with IPV6_V6ONLY=0 so a single fd serves both
    v4 and v6 peers; literal udp_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_to park 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 after udp_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. iface arg is
      intentionally minimal (IPv4 IP literal or numeric ifindex; no
      if_nametoindex so Win32 doesn't need -liphlpapi).
  2. 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 ) NetErr of 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 direct tcp_connect / udp_connect.
    • dns_reverse ip? String (Some only when there's a real PTR
      record — NI_NAMEREQD).
  3. Runtime §18b / §18c (stdlib/runtime.c) — implementation:

    • NurlUdp { fd, err_kind, family, peer } opaque handle (~16 % the
      size of NurlTcp — UDP has no TLS state to track).
    • 22 new nurl_udp_* and 3 new nurl_dns_* exports; WASI stub
      row at the bottom degrades every call to NetOther /
      strdup("") so wasm32-wasi builds still link.
    • Reuses §18's NURL_NET_ERR_* error space + nurl__net_map_errno / _wsa mapping helpers; multicast group-family branching uses a
      new nurl__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 matches correct.txt baseline.
  • compiler/tests/dns_basic.nu — always-on (uses literals + the
    /etc/hosts localhost mapping 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":

  1. 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 regex
    

    PATH is one or more files or directories — directories recurse,
    dotfiles are skipped, and with no PATH the tool reads stdin. Output
    is path:line:contents per 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_lines is mode-agnostic; walk_dir returns -1 on
    not-a-directory so the dispatcher falls back to the file scanner
    cleanly. Built on top of stdlib/std/fs.nu (read_file,
    dir_list), stdlib/ext/regex.nu (regex_compile / _test), and
    the existing nurl_str_* helpers. Pure CLI I/O — runs on the
    public playground.

  2. examples/README.md refresh — 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 on play.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 full run_tests.sh
    corpus). 15-min timeout.
  • sanitizers — same setup, run...
Read more

v0.9.0

24 May 20:35
196fa7e

Choose a tag to compare

[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_str ported to pure
    NURL in stdlib/std/random.nu. Only nurl_rand_fill stays C —
    the getrandom / arc4random_buf / BCryptGenRandom platform
    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_kind moved to pure NURL. The g_last_bytes_len sideband is
    gone — fread / fwrite write directly into the Vec[u] data
    buffer and vec_set_len records the count. EACCES / EPERM /
    EEXIST added to nurl_native_constant; errno_kind now lives in
    stdlib/core/posix.nu.
  • PURIFY §22 gzip. nurl_gzip_compress / _decompress moved to
    pure-NURL FFI in stdlib/ext/compress.nu over deflateInit2_ /
    deflate / deflateEnd + inflateInit2_ / inflate /
    inflateEnd. Two tiny C accessors (nurl_z_setup /
    nurl_z_total_out) bridge the platform-varying z_stream field
    layout (LP64 vs LLP64 uLong width).
  • PURIFY §14 HTTP response accessors. The 7 accessor C functions
    (status / err_kind / body / body_len / header_count /
    header_name / header_value) deleted from runtime.c. Pure-NURL
    equivalents in stdlib/ext/http.nu read the NurlHttpResponse
    heap struct via nurl_peek(p, slot) over its 6-i64 slot layout.
    Static asserts in runtime.c pin the layout at compile time so a
    future field reorder breaks the native build instead of silently
    miscompiling NURL reads. nurl_http_response_free stays C because
    it walks headers[] deallocating each name / value pair plus the
    body.
  • PURIFY §14b HTTP libcurl backend + multi-stream orchestration
    driven from pure NURL. Sync nurl_http_perform_full_to and
    multi-stream _open_to / _next / _pump_headers plus the 5
    stream accessors live in stdlib/ext/http.nu; 22 monomorphic
    trampolines stay C (nurl_curl_* setopt / multi /
    stream-state) because libcurl's variadic curl_easy_setopt and the
    raw-fn-pointer callbacks (nurl__http_write_body /
    _write_header) can't cross the FFI directly. NurlHttpStream's
    three historical int fields widened to long long for a clean
    14×i64 slot layout; static_assert pins 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 over nurl_peek of a heap-side byte
    window.
  • runtime.c prose cleanup (commits d558844, 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

  • json ext goes production-ready. stdlib/ext/json.nu:
    • Typed JsonError replaces the bare ParseErr — carries
      kind (BadFormat / Empty / TrailingGarbage / Overflow),
      pos (0-based byte offset), line (1-based) and col
      (1-based). Location is computed once per failure and travels
      with the error value — no global state, so nested
      json_parse calls and multi-threaded use are both safe.
      json_format_error renders the standard message; build your
      own from the fields if you need a custom shape.
    • RFC 8259 strict mode. Non-conforming numbers (leading zeros
      like 01, +5, lone .5, 1.) are now BadFormat instead of
      parsing to the prefix — json_stringify ∘ json_parse is
      guaranteed-valid JSON.
    • New constructorsjson_int n / json_float x from
      primitives (no i8* roundtrip), json_arr_new / json_obj_new
      for empty containers.
    • Duplicate-key behavior documented. Parser preserves
      duplicate keys as-is; json_obj_get returns the first match in
      source order; json_obj_set replaces 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.

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.py and
    compiler/src/*.py are gone. Stage 0 now links the committed
    compiler/nurlc_lastgood.ll snapshot directly via clang. The
    only build-time dependency is clang/LLVM 14+. Refresh the
    snapshot with ./build.sh --refresh-bootstrap when a
    grammar/runtime-ABI change leaves the current snapshot unable
    to compile current nurlc.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. % Drop auto-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 in nurlc.nu, −71 C)
    • Phase 9b §6b symbol table (3 parallel grow-by-2× arrays,
      inner loops via direct *s / *i pointer 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.nu HashMap[s i] is the one-true map for
      every consumer; the migration also fixed hash_string from
      O(n²) → O(n) by switching from per-byte nurl_str_get to a
      direct *u byte 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 + nanosleep FFI (stdlib/std/time.nu,
      −38 C; macOS uses CLOCK_MONOTONIC = 6 vs 1 elsewhere, read
      at runtime via nurl_native_constant)
    • §13 batch 2/3 — stdin + dir_list POSIX FFI (−80 C)
    • §11 strtod sideband eliminated with an endptr buffer (−20 C)
  • || 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;
    runs nurlc against 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-site error: 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_resultmcp_client's 1-arg result
    extractor renamed for consistency with the rest of the surface.

Fixed — `refactor/pure-n...

Read more

v0.8.0

20 May 20:10

Choose a tag to compare

[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 LLVM xor: bitwise XOR on integer operands,
    logical XOR on b operands. Float operands are a compile error
    (LLVM has no float xor). Replaces the old (a | b) - (a & b)
    identity workaround. ^ alone remains the return operator.
    Grammar (spec/grammar.ebnf) and nurlfmt updated; regression
    tests xor_op.nu + should_fail_xor_float.nu.

  • inout and sink parameter conventions (BORROW.md Phase 4,
    Option B — mutable value semantics).
    in / inout / sink are
    contextual keywords recognised only as a parameter's leading token
    (no lexer change); in is the default.
    A parameter marked inout is an exclusive mutable borrow: the
    callee mutates the caller's binding in place. inout T lowers 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; an inout function must be defined before it is
    called. Exclusive-access check (BORROW.md Phase 5): a binding
    passed inout must be the only argument path to its value at that
    call — passing it again, as a second inout or a plain by-value
    argument, is a warning:.
    A parameter marked sink consumes (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. sink v1 applies to Vec and other
    manually-managed handles; passing a compiler-auto-dropped value
    (owned string / slice / Drop value / struct with owned fields)
    to a sink parameter 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 with warning: diagnostics: use-after-move (a binding
    read after its ownership moved), alias double-free (: T b a of an
    owned heap value moves a), 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 new docs/MEMORY.md.

  • Tail-call optimisation in the @-fn dispatch path. gen_ret
    now flags the upcoming return-value expression as
    tail-position; gen_call snapshots + 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 LLVM call becomes tail call
    when (a) the flag was set, (b) rlt == fn_ret_ty so LLVM
    accepts the marker, (c) the callee is not variadic, and (d)
    gen_ret saw 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 tail over musttail: tail is 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. musttail is
    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.nu runs 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 plain call (different shapes; not
    the deep-recursion targets TCO exists for).

    Coexists with --g DWARF emission: tools/dwarf_test.sh still
    passes all five phases.

  • DWARF Phase 6 composite-type rendering. User structs and
    generic-instantiation handles (%Vec__u8, %String, %FmtTok,
    user % Point, …) now resolve under nurlc --g to a
    !DICompositeType(tag: DW_TAG_structure_type, …) carrying one
    !DIDerivedType(tag: DW_TAG_member, …) per field — instead of
    the previous i64 placeholder. gdb ptype Point lists the fields
    with their NURL names + base types; print p renders the value
    as {x = 3, y = 7}; print p.x evaluates a single field.

    Field roster lives in the existing symbol table next to the
    per-field __idx_N__type entries — gen_struct_decl and the
    generic-instantiation emitter now also record
    <sname>__field_count and <sname>__idx_N__name. New helpers
    dbg_size_bits / dbg_align_bits / dbg_align_up compute
    LLVM-natural cumulative field offsets so the emitted
    !DIDerivedType member 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
    in g_dbg_type_syms before the per-field recursion descends
    through dbg_type_id_for, so a back-edge returns the cached id
    instead of looping.

    Regression: compiler/tests/dwarf_struct.nu exercises 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.