v0.9.4
Added
-
Keyword arguments — default parameter values + named call arguments.
A trailing parameter may carry a default:@ f s a s b =xi n = 3 → R
(the default is a single source token — literal / const / atom). A call
may then omit defaulted trailing arguments —( f val )— and/or pass
arguments by name in any order, mixed with leading positional ones:
( f a: 1 b: 2 ),( f val n: 5 ),( greet greeting:Hiname:Bob).
Implemented as a call-site desugaring to an ordinary positional call:
scan_fn_sigsrecords each function's parameter names + default sources;
gen_callfills omitted trailing defaults inline, and routes a call that
usesname:labels throughgen_call_kwargs, which evaluates arguments
in source order and assembles them in parameter order. Existing positional
calls take the unchanged path (byte-identical IR — bootstrap fixed point
holds). Regression:compiler/tests/kwargs.nu. Current limits (documented
in the grammar): not on generic functions, FFI/variadic, or parameters
with theinout/sinkconvention;**kwargs-style collection is not
provided (pass aJson/struct). -
BLAKE3 hash (pure NURL) — completes the hash family. New
stdlib/std/hash_blake3.nuimplements full BLAKE3 (the ChaCha-derived
compression function, 1024-byte chunks split into 64-byte blocks with
CHUNK_START/CHUNK_END flags, the binary Merkle tree of chaining values,
and the ROOT-flagged final node), exposed viablake3_bytes/
blake3_hexinstdlib/std/hash.nu(unkeyed, 32-byte output). All-NURL
u32 wrapping arithmetic, little-endian, binary-clean over( Vec u )—
no C at all (compiler and runtime untouched). Verified
digest-for-digest against the official BLAKE3 reference across every
structural path (empty, sub-block, the 1024-byte single chunk, the
1025-byte two-chunk boundary, balanced multi-chunk trees up to 5000
bytes); regressioncompiler/tests/blake3.nu; clean under ASan/UBSan/LSan.
Closes the ROADMAP "Extended Hash Family" item — SHA-1/256/512, MD5,
HMAC, and BLAKE3 are all shipped. -
volatile_load/volatile_storecompiler intrinsics for MMIO. Emit
load volatile/store volatileas pure IR (no runtime call, so they
work on a freestanding target). The optimizer can no longer hoist an MMIO
read out of a polling loop (LICM), reorder accesses, or coalesce repeated
reads/writes — the missing piece for spinning on a device status register
at-O2. The access width comes from the typed pointer argument (*T),
so one pair covers i8/i16/i32/i64.stdlib/hal/mmio.nu
(mmio_read32/write32/set32/clear32) now uses them, so the ESP32
UART/GPIO drivers no longer need the-O0workaround. Regression:
compiler/tests/volatile_mmio.nu; verified at-O2the volatile load
stays inside the loop body. -
ESP32 bare-metal register HAL (
stdlib/hal/esp32.nu). Pure-NURL GPIO
and UART0 over the chip's memory-mapped registers (built on
stdlib/hal/mmio.nu) — no ESP-IDF, no FFI. GPIO output enable / set /
clear, and a blocking UART console (esp32_uart_putc/getc/puts
with FIFO-count helpers), with register addresses taken from the ESP32 TRM
and cross-checked against ESP-IDF'ssoc/*_reg.h. Demonstrated by the new
fully-NURL UART echo example (examples/esp32/idf-uart). -
C64 emulator example (
examples/c64). A MOS 6510 / Commodore 64
emulator in pure NURL — a singlecore.nuengine shared by a native CLI
and a WebAssembly browser front-end. The CPU core passes Klaus Dormann's
6502_functional_test(the canonical 6502 correctness oracle, validated
headlessly), and with stock KERNAL/BASIC/CHARGEN ROMs the machine boots
through the full power-on sequence — PLA banking, CIA1 jiffy IRQ — to the
BASICREADY.prompt.
Fixed
-
nurlfmt split hex/binary/octal integer literals. The tokenizer's
numeric scanner stopped at the first non-decimal digit, so0x3FF44008
became two tokens (0+ identifierx3FF44008) and the reformatted
source miscompiled — silently, because--checkis idempotent on its own
broken output.tools/nurlfmt/tokenize.nunow scans a0x/0b/0o
prefix and its body as one token. Verified by the
nurlfmt_idempotent.shgate (450 files, IR-transparent) and by restoring
the hex literals in theexamples/esp32/*register maps that had been
worked around with decimal constants. -
SQLite production hardening (Tier 1 + Tier 2).
stdlib/ext/sqlite.nu
is now binary-safe and resource-safe:- NUL-safe text I/O.
sqlite_column_textreads the column's exact
byte length viasqlite3_column_bytes(wasstrlen, which truncated
at the first embedded NUL), andsqlite_bind_textnow takes aString
and passes an explicit byte length tosqlite3_bind_textinstead of
-1— strings with embedded NULs round-trip intact. - BLOB support. New
sqlite_bind_blob(Vec u→sqlite3_bind_blobSQLITE_TRANSIENT) andsqlite_column_blob(sqlite3_column_blob+
_bytes→ ownedVec u) — the binary-safe write/read path.
sqlite_open_v2with open flags.SQLITE_OPEN_READONLY/
READWRITE/CREATE/URI/NOMUTEX/FULLMUTEX/NOFOLLOW
constants exposed;sqlite_openis nowREADWRITE|CREATEover
open_v2. A read-only connection refuses writes (newSqliteReadOnly
error variant) instead of silently creating a file.sqlite_busy_timeoutwrapssqlite3_busy_timeoutsoSQLITE_BUSY
blocks-and-retries under concurrent access rather than failing
immediately.% Dropauto-close.DatabaseandStatementimplement the Drop
trait; a scope-local handle — including one unwrapped from a
! Database E/! Statement Eresult in a match arm — closes itself
on every path (Ok, Err, early return) with no manual
sqlite_close/sqlite_finalize. Teardown zeroes the handle slot after
closing, so a stale internal re-entry is a no-op. Verified leak-free
and double-free-free under ASan + UBSan (compiler/tests/sqlite_hardening.nu).- Tier 3 — datatypes & transactions.
sqlite_bind_double/
sqlite_column_double(REAL columns),sqlite_column_is_null,
sqlite_begin/commit/rollback, and a closure-based
with_transactionthat COMMITs onOkand ROLLBACKs onErr
(propagating the original error). - Tier 4 — hardening for untrusted SQL/DB. Extended result codes are
enabled on every open, so constraint failures now map to distinct
variants (SqliteConstraintUnique/…ForeignKey/…NotNull/
…PrimaryKey/…Check). Addedsqlite_last_insert_rowid;
sqlite_set_defensive/sqlite_enable_load_extension/
sqlite_harden(DEFENSIVE on + extension-loading off — blocks
corruption/RCE from a hostile DB);sqlite_limit(bound query
complexity); a closure-basedsqlite_set_authorizer/
sqlite_clear_authorizerthat installs a sandbox callback with the
exact C ABI libsqlite expects (the closure's compiled function +
captured env are passed asxAuth+pUserData, the same mechanism
thread_spawnuses forpthread_create— no C bridge); and PRAGMA
helperssqlite_journal_wal/sqlite_foreign_keys/
sqlite_synchronous. Verified under ASan + UBSan
(compiler/tests/sqlite_tier34.nu).
- NUL-safe text I/O.
Changed
- Match-arm payload bindings now participate in auto-drop. A
% Drop
type bound as a??match-arm payload (e.g.?? r { T db → … }) — or a
:let inside a match arm — is now dropped at arm scope exit, on the same
void-arm-only rule used for owned strings/structs. Previously such
bindings were never dropped (a latent leak); this is what lets the SQLite
handles above close automatically in the idiomatic result-unwrap flow.
Documentation
-
ROADMAP brought up to date. The Status header now reads Grammar v2.1
(was v2.0) and points atspec/grammar.ebnf. Items that were marked pending
but are in fact shipped are now[x]: the async runtime (stackful M:N
fibers — the Coroutines-vs-async/await decision is settled), HTTP server
Phase 8 (production hardening) and Phase 9 server-side (TLS+SNI+ALPN+
mTLS+reload, HTTP/2, WebSocket — client-side remains), the optional
-lcurlsentinel-gated linking, and thenurlc_lastgood.nurefresh
lifecycle (documented via--refresh-bootstrap). Added an explicit
"What's actually left" summary to the Status section (HTTP/2+WebSocket
client-side; mobile/no_stdtargets; SQLite BLOB/double; reverse-proxy
binary bodies; blake3; MCP SSE/sessions/auth; theruntime.cfile-split;
a compiler-embedded LLM; bench peers). Stale build-size figures left only
in dated historical "shipped" entries (records, not current claims). -
Removed hard-coded build-artifact sizes from the reference docs. The
~480 KB nurlc.wasm(docs/PLAYGROUND.md) and~1.6 MB
nurlc_lastgood.ll(docs/BUILDING.md) figures drift every build and
mislead when the real artifact differs. Build sizes belong in the
changelog/release notes (tied to a specific version), not in
instructional docs. -
Cleaned stale
GOTCHAS.md item N/§Nreferences out of code comments.
Afterdocs/GOTCHAS.mdlost its numbered list, ~44 source comments (in
compiler/nurlc.nu, thenurlc_lastgood.nusnapshot mirror, nine
compiler/tests/*.nu, andstdlib/ext/{http_middleware}.nu) still pointed
at item/section numbers that no longer exist. Each now points at the real
home (escape/lifetime →docs/MEMORY.md§2.3, grammar →docs/LIMITATIONS.md)
or simply describes the behaviour inline. Thenurlc_lastgood.nuedits are
comment-only — verified to produce byte-identical IR, so the committed
bootstrapnurlc_lastgood.llis unchanged; the build still reaches its
fixed point and the full test suite passes. -
docs/GOTCHAS.mdreduced to "Currently no known gotchas." Every
source-level trap is now a compiler diagnostic (error:/warning:with a
caret + cure), so the page no longer lists a museum of resolved issues.
The real content that lived there was relocated to its proper home: the
fiber-runtime operational caveats (non-blocking handle flipping,
runtime_runblocking, stack-borrow capture, plus runtime-maintainer
notes on TLS-under-LTO and the reactor park/unpark ordering) moved to
docs/ASYNC.md→ Operational caveats, and the
: ~-capture lifetime rule now points atdocs/MEMORY.md
§2.3. Updated every back-reference (docs/spec.md,docs/LIMITATIONS.md,
ROADMAP.md's "5 active quirks" status line, the VS Code extension
README, and staleGOTCHAS.md item Ncomments instdlib/ext/toml.nu,
mcp_http.nu,http_multipart.nu). All internal links verified. -
docs/LIMITATIONS.mdscoped to actual language/compiler limitations.
Removed the standard-library capability tables (PostgreSQL, SQLite,
panic/recover) that were never language limitations — that information
lives with each module (stdlib headers,ROADMAP.md,TODO.md). Moved
the HTTPS/TLS table todocs/NETWORKING.mdwhere it
belongs. Removed two entries that were stale (the behaviour already
works, verified empirically): "no tail-call optimisation" (self-recursive
tail calls emittail call→ LLVM sibcall-opt; 50M-deep tail recursion
runs without overflow) and "enum forward references unsupported"
(scan_type_namesregisters type names before codegen, so a struct
payload can be declared after its enum). The page now lists only
language/compiler constraints (Type system, Functions/calls, Enums,
Imports, Grammar). -
Playground now renders linked docs instead of 404ing. Clicking a
relative link inside a rendered doc (e.g.docs/LIMITATIONS.mdfrom the
README, or../spec/grammar.ebnffrom adocs/page) used to hit "not
found".nurlapinow serves the repo doc tree by its natural path —
/docs/*,/spec/*,/bench/*, and the capitalised top-level
/README.md·/ROADMAP.md·/CHANGELOG.md·/CONTRIBUTING.md—
rendering.mdto HTML (__serve_repo_doc, path-traversal-guarded) and
serving other files as text;examples/*.mdrenders too (.nustays
JSON for the editor). Because the route hierarchy mirrors the repo, the
browser's own relative-link resolution chains correctly between docs. The
container image now copies the wholedocs/tree (was only
GOTCHAS.md) plusCHANGELOG.md,CONTRIBUTING.md, andbench/. -
README refactored into a slim overview + topic docs. The 991-line
kitchen-sink README is now a ~230-line overview (why/principles,
architecture, quick start, syntax-at-a-glance, a documentation index, and
project layout) that links out to focused pages underdocs/. New:
docs/BUILDING.md,docs/TOOLING.md,docs/PLATFORMS.md,
docs/PLAYGROUND.md(HTTP API + playground + MCP),docs/NETWORKING.md
(sockets + MQTT),docs/LIMITATIONS.md. Syntax/type/memory sections now
point to the existing authoritative homes (spec/grammar.ebnf,
docs/spec.md,docs/MEMORY.md) instead of duplicating them. -
Removed stale / frequently-changing content. The README no longer
hard-codes a grammar version, benchmark tables (point tobench/), the
example file list, the MCP tool count, or the.vsixversion. The
PostgreSQL "Known Limitations" (claimed no binary protocol / async /
LISTEN-NOTIFY / COPY — all shipped) and the MQTT section (TLS-only +
verify-on-by-default + exactly-once QoS 2 +subscribe_many) are now
accurate. Dropped references to non-existentspec/types.md/ir.md/
bootstrapping.md, fixedCONTRIBUTING.md'sapi/→nurlapi/, a dead
HTTP_SERVER_PLAN.mdlink inROADMAP.md, the compiler's prefix-arity
diagnostic (pointed at the moved README section →docs/LIMITATIONS.md),
anddocs/GOTCHAS.md's cross-reference. All internal doc links verified.
Added
- MQTT: multi-topic SUBSCRIBE (
mqtt_subscribe_many) sends one
SUBSCRIBE for N filters at a shared max QoS and validates every
per-filter SUBACK reason code (a new__mqtt_check_subackthat parses
the property block instead of assuming a single trailing byte —
mqtt_subscribe_qosnow uses it too). - PostgreSQL advanced protocol features — binary, async, LISTEN/NOTIFY,
COPY (stdlib/ext/postgres.nu). Closes the last Tier-5 Postgres gap;
all four are pure-NURL libpq FFI (noruntime.cbridge) and are
exercised end to end by the newexamples/pg_advanced.nu, live-verified
against PostgreSQL 16.14.- Binary result protocol:
pg_exec_params_binaryrequests
resultFormat = 1;pg_get_i16_bin/_i32_bin/_i64_bin/
_bool_bin/_f64_bindecode network-byte-order cells (float8
reinterpreted from its IEEE-754 bit pattern, not a numeric cast), with
pg_get_length/pg_field_format/pg_binary_tuples. - Asynchronous queries:
pg_send/pg_send_paramsdispatch without
blocking;pg_get_result(→?PgResult,Nonewhen finished) and the
blocking conveniencepg_awaitcollect results;pg_consume_input/
pg_is_busy/pg_socket/pg_flush/pg_set_nonblockinghook into
an event loop. - LISTEN/NOTIFY:
pg_listen,pg_notify_send,pg_notifies
(→?PgNotify { relname, be_pid, extra }, read after
pg_consume_input) andpg_notify_free. - COPY:
pg_copy_start(accepts thePGRES_COPY_IN/COPY_OUT
handshake that plainpg_execrejects),pg_put_copy_data/
pg_put_copy_str/pg_put_copy_endforCOPY … FROM STDIN, and
pg_get_copy_data(→?String) forCOPY … TO STDOUT.
- Binary result protocol:
Fixed
- Compiler:
??-match on a result/option-typed parameter dropped its
payload.@ f !S E r → S { ?? r { T x → ^ x } }emittedret i64
against the%Sreturn type (an LLVM "value doesn't match function
result type" error), and the same gap mishandled a( Vec u )handle
payload and dropped the unsigned flag on a?uparameter (sign-extending
a byte ≥0x80).gen_fn_paramnow records the
<param>__res_nurl_T/__res_t_llvm/__res_e_llvm/__opt_nurl_T
metadata thatgen_let_or_structalready records for let-bound result
vars, sogen_matchreconstructs struct / pointer / unsigned payloads
for a parameter scrutinee exactly as it does for a let binding. Bootstrap
fixed point held; regressioncompiler/tests/match_param_payload.nu. - Compiler: an empty block
{}returned from a void function emitted
invalid IR. An empty block is the unit/void value, butgen_blockleft
the "last type" at whatever preceded it (i64 by default, i1 inside a
conditional), so^ {}in a void function producedret i64 undef—
rejected by LLVM. The block now types asvoidwhen it has no trailing
statement. - Compiler: undefined identifier in value position no longer emits an
undefined SSA value with exit status 0 (PR #25 /Fixes).gen_ident's
bare%<name>fallback fired for any name lacking a__ptr/__global
binding — so: i x ^ a b cemittedret i64 %athat nurlc accepted and
only clang rejected. The fallback now requires a by-value parameter and
otherwise dies with "use of undefined identifier". This was critic.md §4's
headline contradiction of "every trap is a compiler diagnostic".
Regressioncompiler/tests/should_fail_undef_ident.nu. - Compiler: a within-statement prefix-arity cascade that swallowed the
next^silently returned early (PR #25 /Fixes).: i x + 1/
^ aparsed as+ 1 (^ a)→ret %aplus a deadadd, exiting 0. A new
g_ret_forbiddenflag (armed by agen_operandwrapper around every
value-operand parse, reset bygen_stmtand the?/??arm bodies)
makesgen_retrefuse to emit aretin operand position. Regression
compiler/tests/should_fail_cascade_caret.nu. - Compiler:
??-match on a direct-call scrutinee dropped pointer/handle
payloads and option signedness (PR #25 /Fixes).: ( Vec u ) x ?? ( f … ) { … }/: s x ?? ( f … ) { … }left the bindingundef(no T-arm
reconstruction, no result phi) for handle/pointer payloads, and?? ( vec_get [u] … ) { T b → # i b }sign-extended an unsigned byte. The
callee's Ok/Err-payload LLVM types and option-inner token are now recorded
per function and surfaced togen_match's direct-call synthesis (with a
bare-i8*inttoptr path for both arms). Regressions
compiler/tests/match_bind_call_handle.nu,match_call_opt_unsigned.nu. - Compiler: option/result construction from a sized-int literal didn't
truncate (PR #25 /Fixes).@ ?u { T 0x86 }emittedinsertvalue { i1, i8 } …, i64 134, 1, which clang rejected.gen_agg_lit's opt/res
payload coercion now truncs/sexts/zexts the literal to the payload width
(option = T's real width, result = i64). Regression
compiler/tests/opt_lit_payload_width.nu. - MQTT inbound QoS 2 is now exactly-once. A retransmitted (DUP)
QoS 2 PUBLISH was acknowledged but re-delivered to the application. The
client now tracks inbound packet ids across their PUBREC…PUBCOMP window
(MqttClient.qos2_rx, bounded, oldest evicted past 256), acknowledges a
duplicate but delivers it only once.__mqtt_parse_publishreturns
?MqttMessage(None on a de-duplicated retransmit) and the dedup policy
is unit-tested incompiler/tests/mqtt_qos2_dedup.nu. The doc-drift
comment on__mqtt_do_publish(claimed a fixed packet id) was corrected.
Security
- MQTT TLS certificate verification is now configurable and on by
default.mqtt_connect_cfg/mqtt_reconnectpreviously hard-coded
verify = F, so every TLS connection was effectively--insecure
(MITM-able).MqttConfiggained atls_verifyfield, threaded through to
tcp_connect_tls;mqtt_configdefaults it to T (peer-cert chain + host
name verified against the system trust store). Set it F only for a
self-signed broker in a trusted environment. pg_listenSQL injection (critical) — fixed. A channel name is a SQL
identifier and cannot be a bound parameter, so it now goes through
pg_escape_identifier(PQescapeIdentifier) before interpolation; raw
concatenation previously letpg_listen c "x; DROP TABLE …; --"execute
the injected statement. (pg_notify_sendwas already safe — it binds the
channel as a value topg_notify($1, $2).)- Out-of-bounds read in the binary accessors (medium) — fixed. A new
PQgetlength-checked__pg_bin_ptrguards everypg_get_*_bin:
reading anint4cell with the 8-bytepg_get_i64_bin, or any accessor
on a binary SQLNULL(0 bytes), now returns0instead of reading past
the cell into adjacent libpq buffer memory. - TLS is not verified by default — documented.
pg_connectand the
file header now carry a prominent warning that libpq's default
sslmode=preferneither prevents a silent plaintext fallback nor
verifies the server certificate (MITM-able), recommending
sslmode=verify-full sslrootcert=…for non-local connections. Not
force-defaulted, as that would break legitimate unix-socket / trusted-LAN
connections. Minor:pg_get_boolgained a NULL-pointer guard, and the
empty-on-NULL behaviour ofpg_escape_literal/pg_escape_identifier
is now documented.