| what it affects | what it means for you | |
|---|---|---|
| Language | anyone who wrote Wantzel code | a program that compiled yesterday may not today, or may mean something else |
| Compiler | anyone who compiles | the same program, the same meaning — but different messages, different code, different limits |
| Library | anyone who imports a library module | a routine, its behaviour or text it emits changed; your own code may need to follow |
Before 1.0, a Language entry can appear in any release. A construct that costs more than
it gives is removed rather than kept — see language.md §9. Read that row
first on every upgrade.
Newest first. Dates are the day the change landed.
Unreleased — part of Wantzel Studio
Library
Compiler
- Every source file carries
// (C) Copyright 2026 Floris Knolas its first line (3 October 2026), and the banner ofwantzelandwantzel --helpsays(C) Copyright 2026 Floris Knol. A line added at the top moves every line number by one: the error and runtime messages of the library and of the tests name the next line now. The code itself is unchanged -- 88 of the 90 reference programs compile byte for byte as before; the two debug builds differ only in the line numbers their debug information carries. - A slice bound may hold a call that takes an array (6 October 2026):
f(b[g(b)..9])was refused with "a slice can only be passed as an array argument" -- the inner argument reset the permission of the slice around it. It compiles now; a slice inside an index or a bound (f(b[c[1..2]..9])) is still refused with that message.
Library
lib/oauthbinds a token to the resource it is for (RFC 8707) (6 October 2026). Aresourceat/authorizegoes with the code; at/tokenit must be the same one (elseinvalid_target), or binds the token on its own; a refresh keeps it.oauth.verifyleaves the resource of the accepted token inoauth.res/oauth.resn(0: not bound) for the application to judge. The bindings live in a table of their own (oauth_resources), so codes and tokens already on disk keep loading; a binding goes when its code or token goes, never on a clock of its own.
httpreads a chunked request body (6 October 2026): a request withTransfer-Encoding: chunked(what node'shttp.requestsends afterwrite()andend(), and curl with a body from a pipe) reachesapp.requestwith the body decoded inhttp.bodyat/http.bodylen. Before, it had no Content-Length, so its body was empty and the chunks were read as the next request. Chunk extensions and trailers are skipped; chunked wins over a Content-Length; a chunked body has to fit in the connection's buffer (256 kB) and gets 413 when it does not; a malformed chunk size closes the connection.
0.6.0 — 2 October 2026
A library for everyday work. Four new modules for what business programs do all day:
csv reads and writes spreadsheets' export format, money keeps amounts as whole cents and
reads them the way people write them, search finds rows by the start of any word, pg
reads from a PostgreSQL database, and time learns weeks, months, Dutch dates and running at
a fixed time of day. net and http can listen on one address, for a server behind a
reverse proxy.
What may need your attention if your program compiles today: nothing. Every change here adds; the only change in behaviour is a clearer message when a variable follows a routine.
Library
sha384: SHA-512 as a second entry point of the same core --sha384.init512,sha384.final512,sha384.hash512.csv: reading and writing CSV (RFC 4180) -- quoted fields, a separator or a line end inside quotes,,;or a tab, picked from the first line.money: amounts as whole cents --money.parsereads1.234,56,1,234.56,€ 12,30,12,30-;money.putwrites them back with either decimal mark;money.percent.time:time.weekdayof,time.isoweek(withtime.isoyear),time.adddays,time.addmonths,time.putdmy,time.parsedmy,time.cet(Central European Time),time.nextatandtime.sleepuntilfor running at a fixed time of day ().search: a small search index for listings -- every query word as the start of a word, no case, no accents.pg: a PostgreSQL client, read-only -- SCRAM-SHA-256, the simple query protocol, rows as text, the session set read-only right after signing in.
Compiler
- A declaration the compiler cannot read between the declarations and the main program is now reported at its own line. A variable after a procedure or function (whose
varblock the routine ended) says "a variable here needs its own 'var'"; before, the only output was "forward declared routine ... is never defined" for routines that were in fact defined further down. - A program may declare up to 1024 schemas (was 512); 1025 still stops with "too many schemas". A program with a large tool surface reached the old limit. The tables that grow with them grew along: 2048 record types (was 1024) and 4096 source files (was 1024), because every schema is a record type and a source of its own.
Library
net.listenat(a, b, c, d, port, backlog, reuseport)listens on one IPv4 address instead of every address;net.listenisnet.listenat(0, 0, 0, 0, ...), unchanged. An address the machine does not have gives EADDRNOTAVAIL (-99).http.bindto(a, b, c, d)passes it to every listener ofhttp.serve, so a server behind a reverse proxy can listen on 127.0.0.1 only.
0.5.0 — 25 September 2026
One file. The compiler now carries its standard library: download
wantzel-linux-x86_64 and it compiles anything, with no lib/ directory beside it and no
archive. A library module is imported with a new statement, import io;; include is for
files of your own and nothing else. And a program no longer carries the
routines it never calls: a hello world is about 2 kB.
What may need your attention if your program compiles today:
include "io.wz";for a library module becomesimport io;— no quotes, no.wz. The old form is refused, and the message gives the line to write. For a directory of sources:for m in $(wantzel --lib); do sed -i "s/^\( *\)include \"$m\.wz\";/\1import $m;/" *.wz; done. Rename a file of your own that has a module's name first: its include must stay one.importis a keyword now; a nameimportin your code must change.- A
lib/directory beside the compiler is no longer read, and nothing replaces a module: to try a changed copy, include it as a file (include "./tls.wz";). - The release is one file,
wantzel-linux-x86_64(no version in the name, so.../releases/latest/download/wantzel-linux-x86_64always points at the newest), withSHA256SUMS. There is no.tar.gzany more.
Language
import <module>;reads a module of the standard library inside the running compiler — a bare name; nothing on disk, no variable, no flag.include "path.wz";is always a file, relative to the file that contains it (or absolute). A bare name is no longer looked up in the library first.
Compiler
- The standard library is a packed trailer after the compiler's ELF image;
wantzel --liblists its modules andwantzel --lib <module>prints one;--versionreports the modules, their size and a sha256 over their source. - A library module is named
wantzel/lib/<module>.wzin messages, runtime checks and debug information, wherever the compiler is installed: the same source gives the same executable from a checkout and from a download (it used to carry the install directory's name). - Every wrong spelling of an import or an include is refused with the right one:
import "io";,import io.wz;,include io;,include "io.wz";without such a file, and an unknown module (with the nearest name). - The hint "there is an include further down this file" looks for an include or an import at the start of a line; it fired on the word in comments and strings.
- A routine your program never calls, directly or indirectly, no longer reaches the
executable: the compiler builds the call graph from its own relocations, then compacts
the code it keeps. Always on, no flag -- except under
--debug, which skips this pass because the sidecar's addresses are written as the code is generated, before compaction would know what moved. A--debugbuild is therefore larger than a plain one, and no longer byte-identical to it. Measured against 0.4.0:hello.wzwith an unusedimport tls;drops from 348 KB to 56 KB,examples/serve.wzfrom 500 KB to 257 KB, andexamples/mcpfiles.wz, which uses most of what it imports, from 847 KB to 707 KB.
Library
lz.wz, new:lz.pack,lz.unpack,lz.bound— the compression the compiler stores the library with; damaged input gives-1, never a read past a buffer.- The examples
autocert.wzandjsonschema.wzare nowautocertd.wzandjsoncheck.wz, so that no file outsidelib/has a module's name.
lib/oauth.wz(security): aredirect_uriorstatecould carry a percent-encoded CR LF into theLocationheader of the redirect (header injection), and a registeredredirect_uriorclient_namecould put markup on the sign-in page./registernow refuses aredirect_urithat is nothttp://orhttps://or holds a control character, space, quote, backslash, backtick or angle bracket;/authorize(GET and POST) refuses such aredirect_uriorstate, and astatewith&; the client name is HTML-escaped. A client that registered such aredirect_uribefore can no longer use it. (25 September)
0.4.0 — 25 September 2026
What may need your attention if your program compiles today:
--target=is no longer an option, andwinapi(...)/winproc(...)no longer exist. A program or build script that uses either stops compiling. There is no replacement on Linux, wheresys1..sys6reach the kernel directly.lib/websocket.wz: insideapp.wsframeandapp.wsclose, the base intows.inisws.slot * WS.INMAX, no longerfd * WS.INMAX.lib/fs.wz: a directory opened withoutfs.opendirand closed withoutfs.closemust callfs.forget(fd); code that readfs.dbuf/fs.dlen/fs.dposdirectly no longer compiles (they are per-directory now, and internal).- A
toolshandler that fillstool.vbufto its 1 MB limit is now refused with an error instead of answering with truncated text.
Compiler
- A
toolshandler that fillstool.vbuf(1 MB, atext/jsonview field in the output schema) to its limit is now refused loudly, naming the tool and the limit, instead of the generated writer silently going on with truncated text. The check is generated once as a shared routine rather than copied into every tool's arm, so a program with many tools no longer pays for it per tool. - The compiler targets Linux only: a static x86-64 ELF. Windows output (
--target=windows, the PE writer and the runtime that translated syscalls to the Windows API) and thewinapi/winprocbuiltins are gone, and--target=is no longer an option.
Library
lib/net.wzgains IPv6 sockets:net.connect6,net.listen6,net.socket6, on top ofAF_INET6and a 28-bytesockaddr_in6.net.listen6setsIPV6_V6ONLY, so it can share a port number with anet.listenon the same machine instead of colliding with it.lib/dns.wzresolves AAAA records:dns.resolve6,dns.start6,dns.poll6, alongside the existing A-onlydns.resolve/dns.start/dns.poll, sharing the same lookup machinery, cache (now keyed on name and record type) and CNAME-chain following.dns.connectnow resolves both A and AAAA for a name and tries every address -- IPv6 first, IPv4 as the fallback -- so a host with only AAAA records, previously unreachable from Wantzel at all, can be connected to by name.lib/fs.wz: directory iteration is re-entrant.fs.opendir/fs.nextused to share one read buffer and position for the whole process, so opening a subdirectory while its parent was still being read silently reset the parent's place in its own listing -- fewer entries, no error. Each open directory now gets its own slot (a small table, up to 16 at once; a 17thfs.opendirfails withfs.errno = 24,EMFILE), so a recursive walk works. New:fs.forget(fd), for a caller that opens a directory its own way (not throughfs.opendir) and closes it its own way too -- call it before that close, or a reused fd number can inherit another directory's old read position.lib/dns.wzwaits as long as the C library does: 5 s an attempt, 12 s a lookup (was 2 s and 5 s). A local caching resolver that has to ask upstream the slow way -- systemd-resolved while it re-probes its upstream server -- answers seconds later, and the old bounds turned such an answer into "no nameserver answered in time".lib/http.wzrefuses (413, 431, 503) by half-closing the connection and draining what the client still sends, instead of closing it outright: closing a socket with unread input still in its receive buffer answers with a reset, so a client mid-upload could lose the status that told it why. A header block that never ends now answers 431, and one that never arrives in time answers 408, both of which used to be a silent close.net.wzgainsnet.shutdown.- New module
lib/sha1.wz(RFC 3174), moved out oflib/websocket.wz, which now includes it. A program that needs SHA-1 for something other than the WebSocket handshake no longer has to copy the routine out of a module that also pulls inlib/http.wz. lib/websocket.wzconnections now live inWS.MAXCONNslots, not at their fd -- the same shapelib/http.wzalready uses.ws.takeused to refuse any fd of 256 or more outright; now it refuses only once 256 connections are already open, whatever the fd. Insideapp.wsframe/app.wsclose, the base intows.incomes from the newws.slot, not fromfd.p256.signand the arithmetic it uses on the secret nonce and private key -- scalar multiplication and modular inversion -- are constant-time: no branch or table/array index keyed on a secret bit. Scalar multiplication is now a Montgomery ladder with constant-time conditional swaps; modular inversion is Fermat exponentiation with a constant-time select instead of the previous branchy binary GCD.lib/tls.wzsigns every TLS server handshake with this routine, so its timing is now exposed to any network client, not just a local ACME operator -- measured (a quiet machine, best of several): signing went from about 4 ms to about 14 ms, the cost of removing that timing channel.p256.sign/p256.genkey's k*G now uses a precomputed fixed-base comb table (p256.mulbase) instead of the general Montgomery ladder, while staying constant-time: the table lookup scans every entry and selects with a constant-time fold, never reads at a secret index, and the additions never take a secret-dependent shortcut (a fixed public blinding point keeps the running total away from the point at infinity for the whole multiply, so there is no secret-conditional case to branch on). Measured (a quiet machine, median of ten): signing dropped from about 14 ms back down to about 8.5 ms.
0.3.2 — 24 September 2026
A patch release for servers that face the internet. A program that includes lib/http.wz
now terminates TLS in its own event loop, obtains and renews its Let's Encrypt certificate
from that loop, resolves hostnames, and carries requests and replies of many megabytes over
thousands of connections at once — with no reverse proxy, no certbot, no second process.
The language is unchanged.
What may need your attention if your program compiles today:
lib/http.wzincludeslib/autocert.wz, and with it the TLS stack andlib/dns.wz. A server that never asks for HTTPS pays only in binary size.http.osendandhttp.oendare gone: the finished reply ishttp.outbuf[http.rstart..http.rend).tls.connectandtls.acceptswitch the socket to non-blocking and keep a wall-clock deadline (tls.deadline, 30 s). A caller that read the socket directly afterwards has to expectEAGAIN.ch.verifyno longer tries every root in the store when none is named as the issuer: a chain whose issuer is not in the store is refused at once.- The reply of
mcp.handleis atview(mcp.obase, n); it is still inmcp.bufwhenever it fits there. - Every Windows executable imports
iphlpapi.dll, for the nameservers.
Library
lib/http.wzkeeps connections in slots instead of at their fd: 4096 at once per worker (was 256, and any fd from 256 up was closed on sight), any fd up to 65536. Static memory drops from 192 MB to 82 MB: each connection owns 16 kB of input, a larger request borrows one of 64 shared 256 kB areas, and a reply waits in memory of its own only while the socket cannot take it. Largest request and reply are unchanged.http.serveraises the soft open-file limit to 65536 (at most the hard limit) on Linux.- New deadlines close stalled connections: headers 10 s from the first byte (never
extended), body 30 s plus 1 s per 16 kB, idle keep-alive 60 s, sending 30 s without
progress.
http.timeouts(idle, header, body, send)changes them. - When every slot is busy, an idle connection is closed to make room; if there is none, the
new connection gets
503withRetry-After: 1.http.maxconn(n)sets a lower limit. Counters:http.nopen,http.refused,http.evicted,http.timedout,http.queued. - Fixed: pipelined requests are answered in order. Bytes after the first request of a read were dropped, and the next request could overwrite a reply still being sent.
- New:
http.listen(port, workers)andhttp.poll(timeout)for a program with its own loop,http.addport(port)for more listening ports,http.lisandhttp.slisfor the listener a connection came in on. The finished reply ishttp.outbuf[http.rstart..http.rend);http.osend/http.oendare gone.http.accept,http.readable,http.flush,http.dropandhttp.open[fd]still work. lib/dns.wz: hostnames to IPv4 addresses, over UDP to the nameservers in/etc/resolv.confand TCP when an answer is truncated; CNAME chains, a TTL cache, and distinct failures (DNS.NXDOMAIN,DNS.SERVFAIL,DNS.TIMEOUT, ...). Blocking (dns.resolve,dns.connect, at most 5 s) or from an epoll loop (dns.start,dns.poll).examples/host.wz: ahost-like lookup tool.examples/wget.wztakes a hostname in the URL, looks it up, and checks the certificate against it.
- New
lib/autocert.wz: a server gets and renews its own Let's Encrypt certificate from inside its event loop — one HTTPS request per tick, the HTTP-01 answer from memory, verified chain, atomic store, backoff that survives a restart, and the new certificate handed totls.setcertwithout a restart.examples/autocert.wzis a complete server. ch.verifybuilds the path by name only: the roots whose subject is the certificate's issuer are tried, and a chain whose issuer is no root in the store is refused at once. It used to fall back to trying the signature against every root, which cost seconds of CPU per handshake with a system bundle (28 s for one certificate nobody issued) -- in the caller's thread, which for a server is a way to be stalled by anyone who connects.lib/tls.wzserves many TLS connections from one event loop:tls.open,tls.feed,tls.seal,tls.shut,tls.freeandtls.statedrive a session per connection without touching the socket, so a slow client no longer holds up the others. Up to 4096 sessions.- The server answers KeyUpdate, drops the compatibility change_cipher_spec, and fails only the session that sent malformed input, with an alert.
tls.setcerttakes a chain (DER certificates back to back, leaf first) and refuses a key that does not belong to the leaf, keeping the pair it had. It is safe while sessions are open.tls.acceptruns on the same session code; its callers are unchanged.- The blocking calls (
tls.connect,tls.accept,tls.read,tls.write,tls.close) each end withintls.deadlinemilliseconds, 30 s by default, instead of retrying a stalled peer without a time limit. They put the socket in non-blocking mode. examples/serve.wzuses the new server, gives each connection a deadline, and picks up a new certificate from disk while running.
- MCP messages of many megabytes: a request up to
mcp.maxin(64 MB), a tool result up totool.maxout(64 MB) and a reply up tomcp.maxout(160 MB). A message larger than the static buffers moves into a mapping that is given back afterwards; static memory is unchanged. Past a limit the answer is a JSON-RPC error that names it (data.limit). - Fixed: a reply larger than 4 MB was cut short or stopped the server, and a stdio line
longer than 4 MB earned two errors. A tool result past its limit is now a JSON-RPC error
(
-32603), no longer a tool result withisError. - The reply of
mcp.handleis atview(mcp.obase, n)— inmcp.bufwhenever it fits there, as before. A transport of its own callsmcp.doneafter sending. - Fixed:
mcp.httpandtool.restread a body larger than the HTTP input buffer (thehttp.maxbodyregion) from the wrong buffer. An MCP reply larger than the HTTP layer can send is a JSON-RPC error instead of a plain-text 500.
lib/http.wzserves HTTPS itself, in the same loop: `http.https(host, email, store, staging)gets and renews a Let's Encrypt certificate throughlib/autocert.wz`, andhttp.httpsfiles(cert, key)takes one from PEM files (reloadable while serving). The port given tohttp.servethen speaks TLS;app.requestis unchanged (http.istls).- In HTTPS mode a plain listener (port 80,
http.redirect) answers the ACME challenge, redirects tohttps://once there is a certificate (301,308for other methods than GET and HEAD) and answers503before — never the application. - The header deadline covers the TLS handshake; a refused TLS request lingers briefly so the
client reads the
413/503; countershttp.tlsrefused,http.tlsfailed,http.tlskept. lib/http.wznow includeslib/autocert.wz(and with it the TLS stack andlib/dns.wz): every program on it is about 0.5 MB larger.lib/autocert.wzinstalls the whole chain instead of only the leaf, which browsers refused; resolves the authority throughlib/dns.wz(autocert.pinstill overrides); bounds its TLS calls withtls.deadline; reads PKCS #8 (PRIVATE KEY) keys too.ws.takerefuses a TLS connection instead of writing to it in the clear.examples/autocert.wzishttp.httpsplus options;--pinis no longer needed.
- Large requests:
http.maxbody(cap)now reads each body pastINBUFinto a region of its own connection's, freed once answered, so several arrive at once (was: one region per worker, a second big body got413).http.maxbodymem(total)bounds them together (256 MB); past it,503withRetry-After.http.bigowner,http.biglenandhttp.bigtotalare gone;http.bodyinbigandhttp.bigbasework as before. - Large replies:
http.maxreply(cap)allows replies pastOUTBUF(512 kB); the reply moves to a region that grows as needed and, in the clear, becomes the queued output itself. Past the limit, still a clean500. Expect: 100-continueis answered once a body is accepted; curl no longer waits a second before sending a body over 1 MB.- An MCP reply over HTTP is limited by
http.maxreply, no longer by 512 kB (mcp.httproomaskshttp.replyroom).
Compiler
- A Windows executable opens UDP sockets (
socketwithSOCK_DGRAM), a non-blockingconnectreportsEINPROGRESSinstead of a refusal, and an open of/etc/resolv.confis answered with the DNS servers Windows uses, fromGetNetworkParams— solib/dns.wzruns unchanged there. Every.exenow importsiphlpapi.dll. - Fixed: the C bootstrap (
bin/wantzel0) compilesforloops. It refused every one, so it could not build any program that includeslib/http.wz,lib/sha256.wzor another module with afor. test.shno longer reports "identical output" when a compiler refused the source (it compared the previous source's files); both targets are checked, and a refusal is shown.
0.3.1 — 24 September 2026
A patch release for Windows. A Windows executable now starts a server that writes its process id and checks another process, connects out over TCP, and sleeps as long as it is asked. Nothing that compiles today changes meaning.
Compiler
- A Windows executable now translates
getpid,killandconnect:proc.self,proc.alive,proc.killandnet.connectwork there instead of stopping the program. nanosleepon Windows sleeps the time asked for; it slept 1 ms whatever it was given.
0.3.0 — 24 September 2026
The standard library now lives on disk, beside the compiler. It used to be embedded in
the binary; now lib/ sits next to bin/, and wantzel --version says which library it
uses and whether it is there. A release archive keeps the two together. Copying only the
binary somewhere else is the one thing that no longer works.
What can break code that compiles today:
| if your source | then |
|---|---|
includes ui.wz or uicap.wz | they are removed; nothing in the library used them |
| relies on a schema refusing an unknown JSON key | the key is now skipped and recorded instead |
names a tool like a generated or library tool.* routine | it is reported at your line |
Also in this release: TLS certificates that Chromium accepts (DER lengths in their shortest
form), a WebSocket server, reporting instead of silent failures across lib/, tool names up
to 126 characters, clearer errors for a keyword used as a name, and --debug. The VS Code
extension and the two window examples are gone; winmessage.wz remains as the Win32 example.
Library
- Fixed:
lib/der.wznow writes every length in its shortest form, as DER requires.der.closeshrinks the header it opened, andder.put,der.putbitsandder.putstruse0x81for lengths 128-255. The old fixed0x82form was accepted by OpenSSL and curl but rejected by BoringSSL, so Chromium-based browsers failed every certificate made bycsr.selfsignwithERR_SSL_PROTOCOL_ERROR. New:der.putlen. - **Removed: the VS Code extension (
editors/vscode/) and the exampleswinwindow.wzandx11window.wz**, together with their tests and the two window recipes inhowto.md.winmessage.wzstays as the Win32 example;winprockeeps its own test. - Removed:
lib/ui.wzandlib/uicap.wz, the component layer for native windows, and their tests. Nothing else used them. - **
examples/mcpfiles.wzandexamples/mcpserver.wznow declare their tools in atoolsblock**, likeexamples/mcptools.wz, instead of registering each one by hand withmcp.add. Same tools, same behaviour;mcpserver.wz'scountis nowwordcountbecause atoolsblock turns a tool's name directly into its handlertool.<name>, andtool.countis already generated by the compiler. lib/p384.wz:p384.signandp384.genkeyno longer read past their nonce buffer. It held 32 bytes where a P-384 scalar needs 48, so the first real call stopped with an index out of range. The test now signs with a fresh key and verifies the result.- **
lib/store.wz,lib/fs.wz,lib/proc.wz: failures that returned a silentfalsenow say why.**store.apply/store.writefillstore.err/store.deton an out-of-range table or row and on a write that does not fit the buffer;store.snapshotfills them on every write, fsync or truncate that fails, matching the sibling branches that already did.fs.statandfs.nextgainedfs.err/fs.errno, left empty for the common case (a missing path, an exhausted directory) and filled for a real failure, so the two no longer read the same.proc.alive(pid)now callsio.fatalforpid <= 0instead of answeringfalse: 0 and a negative pid address a process group tokill(2), not one process, so that was never a real answer to ask for. - **
lib/tls.wz,lib/x509.wz,lib/p256.wz,lib/p384.wz: malformed input now leaves a reason.** A CertificateVerify signature with impossible DER, an unloadable leaf key, or a hostname refused because more names existed than the parser kept, used to come back as a barefalse. They now settls.err/tls.errmsg,x509.err, or the newp256.err/p384.err. Whether a signature itself verifies stays a bare boolean, unchanged. - **
lib/websocket.wz: a WebSocket server (RFC 6455) that takes over a connection fromlib/http.wz, on the SAME port.** Handshake (SHA-1 + base64 accept key), text/binary frames both ways, ping/pong, a close that carries a code, and masking of client frames checked and enforced.app.requestsetshttp.detach := trueand callsws.takeon anUpgrade: websocketrequest;lib/http.wzthen lets go of the fd (closes nothing) instead of replying, and this module answers the 101 and reads frames from its own epoll set viaws.poll, called from the application's own loop. No new mandatory hook onlib/http.wz— a program that never setshttp.detachis unchanged. Seedocs/library.md. Linux and--target=windows, throughlib/net.wzonly. lib/http.wz:app.requestcan hand a connection off instead of replying. Sethttp.detach := truebefore returning;lib/http.wzremoves the fd from its own bookkeeping and epoll set without closing it. Added forlib/websocket.wz's takeover, but the mechanism itself has no WebSocket in it — any module could use it the same way. A program that never sets it sees no change.lib/toolsmcp.wz: an MCP server over stdio no longer has to defineapp.request.lib/tools.wzis now split in two:lib/toolsmcp.wzholdsapp.toolsandapp.calland brings onlylib/mcp.wz;lib/tools.wzincludes it and addstool.restandlib/http.wz. A program that includeslib/tools.wzis unchanged. A stdio-only server with atoolsblock includeslib/toolsmcp.wzand drops its emptyapp.request.- **
kv.bytes/kv.sameandhash.put/hash.del/hash.putpad/hash.delpadnow stop the program withio.fatalon a range or key that violates the caller's contract** (a negative or out-of-bounds position, a key of the wrong length, a negative value), instead of returning the same plainfalseas an ordinary "these differ" or "the table is full".json.wellformedkeeps its return value but addsjson.toolarge, so a caller can tell "too large to answer" apart from "not valid JSON". - **
lib/http.wz:http.param/http.formreturningfalseno longer hides a malformed value.** A key whose value has bad percent-encoding, or is too long forhttp.pval, used to answer exactly like a key that is absent.http.lookupnow sets the newhttp.paramerrflag in that case, so a caller can tell "missing" from "malformed". - **
lib/acme.wz:acme.find/acme.copynow report why a lookup failed, throughacme.err.** A genuine parse error (a body that is not JSON, a truncated object, a malformed member) used to answer exactly like an ordinary well-formed object that simply lacks the key; a value too long for the destination used to answer exactly like the key being absent. Both now setacme.errwith a specific reason; a plain missing key stays silent, unchanged. - **
lib/oauth.wz:oauth.take,oauth.readcookieandoauth.newsessionnow report why through the newoauth.err.**oauth.take's failure used to be thrown away at every call site;oauth.readcookieanswered the samefalsefor "no session cookie" and "a session cookie too large to be real";oauth.newsessioncollapsed a full session table, a failed write and a full index into onefalse, so its caller always reported 503 "no room for more sessions" even when that was not the reason.oauth.errfollows the same first-reason-wins convention asacme.errandstore.err.
Compiler
- Windows: each
epollset is its own set. The runtime kept one interest table for the whole process, so everyepoll_create1returned the same handle, an fd added to one set was reported by all of them, andEPOLL_CTL_DELon one removed it from the others. Sets are now independent, as on Linux; a set can be watched inside another (ready when one of its fds is; a cycle isELOOP, a set inside itselfEINVAL), closing a set takes it out of every set, and there are at most 16 at once (one more isEMFILE). - *A tool name that clashes with a generated or library `tool.` name is now reported at
its own line**, with a message naming the clash: `tool name "count" is reserved: the
tools block generates tool.count; choose another name`. It used to point at a line of
the compiler-generated
<tools>text (`<tools>:19: name already used by a variable or constant), or, for a namelib/tools.wz/lib/toolsmcp.wz` declares, at a line deep inside that library. A duplicate tool name in the same block is reported the same way, naming the earlier line instead of<tools>:N: duplicate declaration. - The compiler finds its own
lib/when started with a backslash path. On Windows, where there is no/proc/self/exe, the search anchor always fell back toargv[0], and that fallback only split on/. Started asC:\tools\wantzel.exe, that left no separator at all, so the search collapsed to a relativelib/andinclude "io.wz"failed with "looked in the library first: lib/io.wz" from any working directory without its own copy of the library. The fallback now splits on\as well as/. - A tool name may be up to 126 characters (was 62). The name of a tool is also its name over MCP and REST, so a server that mirrors an existing service cannot shorten it.
- A keyword used as a name is named.
var: int;in a record gives `var is a keyword and cannot name a record field`, on that line; the same for a schema field and a routine name. It used to surface lines later, for example asmissing end of the schema. undeclared identifierpoints at the include order when an include further down the file might declare the name: a name is only visible after the include that declares it.--debugwrites a debug sidecar.wantzel prog.wz prog --debugwritesprog.wzdbgbeside the executable: a line table (address to file and line, with the statement starts a debugger may stop at), every routine with its frame layout, parameters and locals with theirrbpoffsets and types, every global with its address and type, and the record layouts. Plain text, one record per line, for both targets. The executable itself is byte-identical with and without the flag. Format:docs/design.md.- Windows: a peer resetting or closing a socket no longer crashes the program. The
runtime's
recvandsendtookSOCKET_ERROR-- a 32-bit -1 the register does not sign-extend -- for 4294967295 bytes transferred, so an HTTP server onlib/http.wzdied inhttp.parseas soon as a browser or curl closed an idle keep-alive connection. They now recognise the error by its low half and askWSAGetLastError: only a would-block isEAGAIN; a reset isECONNRESET, a broken pipeEPIPE, as on Linux, so the server drops the connection. A failedbindorlistenis reported the same way instead of being taken for success. - A keyword used as a name is reported as that.
to: int;in avarblock, a schema field calledtype, a tool, a constant, a type, a routine or a parameter named after a keyword now givesto is a keyword and cannot name a variableon its own line, instead ofmissing begin of the main program,missing end of the schemaor whatever the parser failed to find next. For a schema field the message adds the repair: rename the field and keep the JSON key withkind "type".
Library
lib/store.wz: a layout version per table.store.layout(t, v)declares it (0 when not called); it is written into the snapshot and into every log record. A store of an older layout opens only with a registered step:store.upgrade(t, oldv, oldsize)copies the old bytes and zeroes the rest (fields added at the end),store.carrymoves byte ranges when fields move or disappear, and steps chain. Otherwise, and for a newer layout,store.opendirrefuses andstore.err/store.detname the table and both versions.store.upgraded(t)says which layout was converted. Snapshots are now written asPHS2; aPHS1snapshot, and a log from before versions, reads as layout 0. An older binary refuses aPHS2snapshot rather than misread it.
Read this row first: the standard library is no longer inside the compiler binary. It is
read from lib/ beside the compiler's own executable. **Download the archive, not the bare
binary** — the archive contains lib/; a lone binary cannot resolve include "io.wz" and
now says so, naming the path it looked for.
Compiler
--versionnow says where the library is, and whether it is actually there:library /opt/wantzel/lib/— or the same path followed byNOT FOUND -- copy lib/ next to the compiler. Since the standard library is read from disk, a compiler copied away from itslib/cannot resolveinclude "io.wz", and that is the one thing about an installation you cannot otherwise look up.
Compiler
- A line with several runtime checks stores its place once.
a[i] := b[j] + c[k]is three bounds checks and onefile.wz:line, and each stored copy costs its text plus an eight-byte length and padding. Measured on the compiler itself: 1016 place strings, 662 of them distinct — 298817 bytes down to 287697, 3.7%, with every message still naming its own line.
winapi()checks the argument count for the imports the compiler carries itself:winapi(): CreateFileA takes 7 arguments, not 2. Win32 passes the first four arguments in registers, so a call with too few used to compile and then read a register nobody set — wrong behaviour rather than a crash. A named import (winapi("user32.dll", ...)) is left alone: its arity lives in a DLL on another machine, and refusing there would be guessing.
- A hex escape with no two digits now gives one message instead of two: the version with
the example (
as in x41 or xff) is what both paths report. Which of the two fired used to depend on whether the source happened to end first, so the help you got depended on where your file ended.
Language
- A schema now ignores a key it does not declare, and records it.
parseskips the value and succeeds instead of returning-1;json.ignoredncounts the skipped keys andb[json.ignored0..json.ignored1)is the first. A caller may therefore send a superset of what a schema declares. It stays safe because the keys are recorded: a renamed optional field would otherwise be answered with the default in its place and nothing would say so. A renamed required field still fails, a declared field given a value it cannot hold still gives-1, andjson.badkey0..json.badkey1now names that declared field. **If you relied on a schema refusing an undeclared key, checkjson.ignorednafterparse.**
- A schema field can name its JSON key:
rdxcap "rdX": int;. Identifiers are case-insensitive, sordXandrdxare one name in Wantzel and two on the wire — and a key likecontent-typeis not an identifier at all. A string literal before the colon, which needs no new keyword.
language.md§7b documents three limits of atoolsblock. An input field must betext[N], not a view: the handler never receives the buffer a view points into. A view in the result is copied in literally, so escape it withjson.escslice. Over REST only a schema mismatch gives 422;tool.failgives 400, a result too large for the buffer 500.
Conventions
- Keep the levels apart: a helper file carries the vocabulary. Code reads like
pseudocode because the words live in one file and the decisions in another — with the test
for when a routine is mixing them, and when a wrapper is not worth having. See
conventions.md§6.
- An error names the fact first and the repair after a dash — the shape the compiler's
messages already mostly had, now written down so a new message does not invent a third
form. See
conventions.md§5.
- **
agent-permissions:in the first 512 bytes of a file says how it is meant to be treated** —read(do not change),none(do not open), or absent for ordinary. It is a hint rather than a guard, like.gitignore, and an unrecognised value restricts nothing. Seeconventions.md§4.
Library
- **
lib/http.wz: a request or reply larger than its buffer gets a clean HTTP error instead of a dropped connection.** A request bigger thanINBUF(256 kB) used to fill the buffer and then reset with no status code, indistinguishable from a crash; it now answers413 Payload Too Large. A reply bigger thanOUTBUF(512 kB) used to write past the buffer and trap the worker, taking down every other connection it was serving at that moment; it now answers500instead.http.maxbody(cap), called once beforehttp.serve, reserves one shared region withmmapfor the body of whichever connection is currently reading pastINBUF— one region, not one per connection, so it does not multiplyMAXCONNstatic buffers bycap. lib/uicap.wz: a component rendered as a list of draw calls instead of pixels.lib/ui.wzdeclares its four drawing routinesforwardand a target supplies them; this supplies a target that records them. A program that includes it renders a component to text — one line per call, with its geometry, corner radius, colour and string — which compares withdiff, runs without a display, and names what differs rather than showing that something does.cap.begin,cap.dump, and queries (cap.find,cap.countop,cap.colat) for checking a component without a window. Truncation is counted and printed, so a capture that overflowed cannot compare equal to one that did not.UI_SURFACEwas the wrong colour:0x25273C, a blue-tinted slate, where the value beside it read0x252526. It is the panel background, so it affected every surface drawn at rest. Found by writing the tokens out in hex — every test compared against the symbol and so agreed with the typo.- **
io.argnandio.argstr: a command-line argument, as a number or copied into a buffer.** Five examples had written the same loop overargchby hand (serve.wz,getcert.wz,tcpproxy.wz,x11window.wz,wget.wz) — one of them said so in a comment, naming the threshold this crosses.argc()andargch()remain the builtins; this is the copy loop around them, in one place. - **
examples/localhttps.wz: HTTPS on loopback, with a certificate the program makes itself.**csr.selfsign,tls.setcertandtls.acceptin about eighty lines — the "1. Localhost" case fromdocs/howto.md, working end to end rather than only described. No VPS and no authority: ACME cannot issue forlocalhost, so the certificate is signed by the program and a client has to be told to trust it, which the example and the test both show by usingcurl --cacert/-k. - **
lib/ui.wz: the start of a component layer, so that a program with a window does not begin by writing a button.** Immediate mode — `if ui.button(panel, "Save", x, y, w, h) then save;every frame, with theif` as the callback. No widget objects, nothing to construct and nothing to free: what persists between frames is a hover, an active and a focus id in fixed arrays. This first entry carries identity (an FNV-1a hash of parent and label, so two panels can each hold a Close), the press/cancel/focus machinery,ui.labeland the button in its four states.
It draws through four routines declared forward — ui.rect, ui.rrect, ui.text
and ui.textw — which the program supplies for the target it is built for. So the
component layer names no platform, and the half that decides anything is testable
without opening a window.
lib/tools.wztells the caller which fields it ignored. A tool call carrying a key its input schema does not declare is answered rather than refused, and the reply says what was dropped:_meta.ignoredFields(firstandcount) in an MCP result, and theX-Ignored-FieldandX-Ignored-Countheaders in a REST reply. It goes there rather than in the body becausestructuredContenthas to keep the shape the output schema declares, and a REST list tool's body is a bare array with nowhere to put a note. The key is reported as it stands in the request, with anything outside printable ASCII replaced by?so a caller cannot forge a response header.
lib/is ordinary Wantzel source on disk and nowhere else. You can read it, step into it in a debugger, and change it: editlib/io.wzbeside the compiler and the next compile uses your version, with nothing to rebuild.
lib/chacha20.wzandlib/poly1305.wz: the AEAD that TLS 1.3 uses, verified against the test vectors in RFC 8439 rather than against our own output.
net.connect: an outgoing TCP connection, which the library did not have —net.wzcould listen and accept but never call out.
examples/tcpproxy.wz: forward a port, in one static binary of about 20 kB. The plumbing a TLS terminator needs — accept, connect, forward, event loop — without the encryption, which is the part that has to be right before a handshake is worth writing.
examples/jsonget.wz: one field out of a JSON document, for a shell script.jsonget a.b.2.id < reply.json, and--validto check a document parses. **1.04 ms per call** against 37.8 ms for the scripting one-liner it replaces: the cost of a one-liner is starting an interpreter, not the parsing, and it only shows up once the helper runs in a loop.
examples/winmessage.wzandexamples/winwindow.wz: calling Win32 from Wantzel. A message box (three lines, and proof thatwinapi()reaches a function the compiler has never heard of) and a window with a realWndProchanded over bywinproc. Both build only for Windows, whichexamples_compile.shnow states rather than assumes.
examples/x11window.wz: a window on X11 with no library at all. X11 is a binary protocol over a Unix socket, so a program that can open a socket can open a window — about two hundred lines, nothing linked, nothing installed.
fs.mapandfs.unmap: one call for "map this file". A stat, an open, an mmap and a close, which every caller used to write out again — including the close, which is the part that gets forgotten. A mapping keeps its own reference, so a caller who does not know that leaks a descriptor per file.
json.prettylays a JSON document out over lines, two spaces per level, one key per line, no trailing comma. It copies values rather than reparsing them, so a number stays exactly as it was written. Invalid input is refused instead of repaired.examples/jsonpretty.wzis the whole program around it: read, call, print.
- TLS 1.3, client and server, in
lib/.lib/tls.wzspeaks TLS 1.3 with ChaCha20-Poly1305 over X25519 (lib/x25519.wz,lib/hkdf.wz), andtls.acceptserves it. Certificates:lib/der.wzandlib/x509.wzread them,lib/csr.wzwrites a request, andcsr.selfsignmakes one forlocalhost. tls.verifiedmeans the chain reaches a trusted root.lib/chain.wzwalks the chain against a trust store — the system bundle, or two built-in roots on a machine without one. Signatures:lib/p256.wz(ECDSA P-256, sign and verify),lib/p384.wzwithlib/sha384.wz, andlib/rsa.wz(RSA-2048/4096, PKCS#1 v1.5 and PSS).- Fixed:
tls.connectbsent an empty SNI extension, so public servers refused the handshake withunrecognized_name. - ACME:
lib/acme.wz,lib/jws.wzandlib/certstore.wzrequest a certificate from an authority and keep it beside the binary, so a restart does not ask again and run into the authority's weekly limit. examples/serve.wzserves HTTP, HTTPS and the ACME challenge from one event loop;examples/getcert.wzfetches a certificate,examples/wget.wzfetches a URL. Which of the three situations you are in:docs/howto.md.p256.signis 2.3× faster (9.2 ms → 4.0 ms): the modular inverse by binary GCD.lib/jsonschema.wz: a JSON document checked against a JSON Schema, naming the place that failed. Calledlib/schema.wzfor a day; renamed because a common name inlib/shadowed a program's ownschema.wz.json.wellformed, and the schema check uses it:{"a":},{"a" 1}and{"a":1,}were reported valid against{"type":"object"}.- Fixed:
json.prettyaccepted malformed input ({"a":},[1,,2], a bare word) and printed something that looked formatted. It now refuses it. io.put64, the 64-bit writer besideio.get64.
Language
localkeeps a name inside its file. Put it in front of a top-levelvar,const,procedureorfunctionand nothing outside that file can see it — two files can now both declarehidden.nwithout colliding. Public is still the default, so existing source is unchanged. The unit is the file: no module system, no separate compilation.localis now a keyword, so a routine or variable that was called exactlylocalneeds a different name (a dotted name likedpy.localis unaffected).
- A schema is declared like every other type:
type X = schema ... end;, where it used to beschema X = record ... end;. The old form is refused by name, so an older source gets the new spelling rather than a puzzle. Nothing else changes: the fields, the optional?, the descriptions and the generatedparse/write/jsonschemaare the same.
The word record in the old form said nothing — a schema body could never be anything
else, and a schema shares none of a record's machinery (it has eleven tables of its own
and touches the record tables not at all). What it did do is offer two shapes for what
looks like one thing, type X = record beside schema X = record, where the difference
is not in the form but only in the meaning. That is the kind of distinction a generator
gets wrong.
Library
- An oversized input line no longer ends an MCP server silently.
mcp.stdioused toreturnwhen a single line filled the input buffer, so the client waited for a reply that never came — the worst failure this transport has, because nothing says anything is wrong. It now answers with a JSON-RPC parse error, drops the line and carries on. MCPBUFis 4 MB, up from 1 MB. A reply carries text as a JSON string, and escaping can multiply a byte by six (\u00XXfor a control byte), so a 256 kB file answered verbatim needs 1.5 MB of room. Measured: 200 kB of control bytes becomes 1.2 MB of valid JSON, which the old buffer could not hold. It is a static array, so the cost is address space and not work.
Compiler
- **"
io.putcis declared inio.wz; add:include "io.wz";" is no longer said about a name that is not there.** The compiler checked only thatlib/io.wzEXISTS, never that it declares the name — so any misspelling whose prefix happens to be a library got a confident pointer to a file that does not have it, and often to anincludethat was already three lines up. It now reads that one file and asks; when the name is not in it, the message is the plainundeclared identifier: io.putc. The hint is unchanged where it was right.
- A name that is one typo from a real one now says which.
io.putcanswersundeclared identifier: io.putc -- did you mean io.puts?. Only one edit, and only within the module the prefix names: measured against the errors that writers really make, a name that was invented rather than mistyped has its nearest neighbour three to five edits away, and a suggestion at that distance is worse than none.
An error still exits, from 234 places in 45 mutually recursive routines; threading a
return value through those is a rewrite of the parser's control flow, not a refactor. A
host that must survive a failed compile calls compile in a fork: the child may exit,
the parent reads its code. Note that the language has one global namespace, so a host
prefixes its own names — the compiler owns short ones like i, out and line.
- An include that cannot be opened names the library path it tried. For a bare name
like
io.wzthe compiler looks inlib/beside its own executable first, and when that file is absent the message now says so on a second line. It used to print only the name written on the line — which the reader can already see, and which sent them looking in their own directory for a file the compiler expected elsewhere. This is the case a download withoutlib/produces. An include containing a/is unchanged: one path was tried, one path is named. lib/is found when the compiler is started throughPATH. The search path is anchored on the compiler's own executable, read from/proc/self/exeinstead of taken fromargv[0]. Started by its bare name — which is what an editor or a script does —argv[0]iswantzelwith no directory in it, and the anchor used to collapse to a relativelib/:include "io.wz"then depended on the working directory and failed in a correct installation. Naming the compiler by an absolute path is no longer necessary.- A constant array index outside the array's bounds is now a compile error rather than a
runtime one:
a[9]on anarray[0..3]is refused while the line is being read. The runtime check stays and is still the only possible one for anarray of Tparameter, whose length only the caller knows. A literal and a named constant are both covered; a runtime variable is unchanged. - The compiler no longer carries a copy of the standard library. One search path,
lib/next to the executable; a bare name is looked for there and then beside the source, and a name containing/is only ever a path. Before, a third place could answer an include and nothing said which one had — so behaviour depended on the working directory. - The binary is 48% smaller: 536561 → 280313 bytes.
- A runtime message names its file relative to the project, at most two directory segments
deep (
src/win32/main.wz:412). The same source therefore produces the same executable whether you name it relatively or absolutely, and no binary carries the directory layout of the machine that built it. Measured before the fix on a program with 1288 checks: 513024 bytes against 581632, from identical source, with the build machine's home directory embedded 1288 times.
Editor
- The VS Code extension no longer ships snippets. It keeps highlighting, compile errors in
the Problems panel, and build and run — and it stays at those three on purpose. The
complete working programs live in
docs/writing-wantzel.md, where a test compiles them on every run and anyone can find them, not only a VS Code user.
Tests
- The certificate and signature tests (
base64,jws,p256,sha384,selfsign,serve) no longer call Python;openssl,base64andxxdare the independent side. english_only.shcounts distinct Dutch words per line, no longer listsover, and checks string literals in every.wzand.shfile; it found test output that was not English.no_foreign_code.shaccepts addresses at the reservedexample.com/.net/.orgdomains and judges each address rather than the whole line.examples/jsonpretty.wz: the error routine is calledfail.
0.2.1 — 15 September 2026
Three keywords are gone: program, repeat ... until, case. A source using them is
refused with a message naming the replacement.
| if your source has | write instead |
|---|---|
program name; at the top | delete that line |
case x of ... | an if-chain (else binds to the nearest unclosed if) |
repeat ... until done | while true do begin ... if done then break; end |
an output name ending in .exe | add --target=windows; the name alone no longer selects the target |
Forty-three keywords became thirty-nine.
Language
- The
programheader is gone; a file starts with its first declaration.programis an ordinary name again. repeat ... untilis gone; writewhile true do begin ... if done then break; end.caseis gone; write an if-chain instead.\xHHis a byte escape in acharorstrliteral, with exactly two hex digits ("\x41BC"is three characters).- The target comes from
--target=only; a.exename with no--targetis refused rather than quietly built for Linux. - A dotted name part may start with a digit (
Reading.level.1); only the first character of a whole identifier must be a letter or_.
Compiler
- Naming a DLL function the compiler already imports reuses its import rather than adding a second one.
- The fixed part of a runtime message is stored once instead of once per check: the compiler itself went from 554,041 to 534,169 bytes, GUI programs 9-10% smaller.
- Imports from two DLLs, interleaved, now get the right IAT slot (a bug could jump to address zero).
undeclared identifiernames the identifier even when it belongs to no known library.winproc(name)gives a Windows API callback the compiler can call: an adapter that moves arguments from the Windows convention (rcx, rdx, r8, r9) to this language's (rdi, rsi, rdx, rcx), adds the 32-byte shadow space, and preservesrdi/rsi/rbx. Notaddr()on a routine — this language has no function pointers. Windows targets only.winapi()takes a DLL and a function by name: `winapi("user32.dll", "MessageBoxA", 0, text, title, 0)` compiles and resolves at link time, whether or not the compiler has ever heard of the function. An empty name, or a DLL name without.dll, is refused at compile time.winapi()refuses an import slot that does not exist, at compile time (a literal slot only; a non-literal slot still cannot be checked).schar()on astrthat was never assigned refuses (`runtime error: string index out of range) instead of segfaulting.slen()on the same still answers0`.- A generated
<Schema>.parserefuses an unknown key instead of silently filling in the default (json.badkey0/json.badkey1name it). Declare optional protocol members asjson?if you need to accept a superset. - A missing
includenames the library that declares the identifier, when one does. - A name colliding only by capitalisation (
STORE.SETvsstore.set) says so explicitly. - A
strwherearray of charis expected gets its own message: only a string literal converts; useio.pushinto a buffer and slice it. - An undefined tool handler is reported at the writer's own
toolsentry, not at a generated line nobody can open. - The VS Code extension's Run menu finds the compiler by walking up from the file, not only at the workspace root.
Library
lib/proc.wzcan start another program:proc.arg0,proc.arg,proc.execwrapexecve. The environment is empty by default, for repeatable tests.mcp.name/mcp.versiondefault towantzel-mcp/0instead of crashing an application that never set them.storerefuses a log record that does not fit its declared size instead of dropping it silently;store.opendirthen returnsfalsewithstore.err/store.detnaming the table and both sizes.store.applyis now a function returningbool(was a procedure).store.closeis valid after a failedstore.opendir.kv.isobject,kv.count,kv.matchno longer move the cursorkv.vat/kv.vend.- New
kv.text(b, dst, at): decoded text of the value the cursor stands on (-1if not a JSON string or it does not fit). mcpandoauthdeclare the protocol members they receive (capabilities,clientInfo,_meta, RFC 7591 fields) — an undeclared member used to make the stricter parser refuse a genuine handshake.- The OpenAPI tags are English (
read/write, were Dutch). - A request header name is case-insensitive on both sides (
http.header("Accept")used to need exact case).
Documentation
docs/syntax.md,docs/flags.mdrewritten as standalone references, checked against the compiler by script.docs/design.md,docs/howto.md,docs/design.mdadded or restructured (later folded intodesign.md/howto.md/library.md; see the top of this changelog for the current shape).docs/language.mddocumentswinprocand the DLL import model.docs/writing-wantzel.mdgains argument order for the most-used routines, failure return values, the buffer-and-length pattern, and two complete example programs.
Tests
no_python.shrefuses a file that IS Python, not only one that mentions it.- The Windows checks that need no emulator (PE header,
.exebyte comparison) run in every--toolchainrun instead of only under Wine. - The VS Code extension's problem matcher, build task and nine snippets are tested on
every
--toolchainrun. lib/openapi.wzcompiles standalone and has its own test.english_only.shalso checks string literals that ship.--windowsno longer runs unconditionally; a surviving Wine process now fails the test.
0.2.0 — 15 September 2026
One language change, and it breaks existing sources: { } is no longer a comment.
Language
- The
{ }block comment is gone;//is the only comment form. A{outside a string is now refused; write a multi-line comment as several//lines.
Compiler
- Name lookup uses a hash index instead of a linear walk: 16,384 globals, 1,296 ms → 16 ms, growth now linear.
- Keyword recognition dispatches on the first character first: the compiler compiling itself, 12.8 ms → 10.3 ms (~590,000 lines/s).
0.1.2 — 14 September 2026
One theme: a failure that could still pass quietly no longer can.
Language
net.watchandnet.nonblockare now functions, not procedures — a call that ignored the result no longer compiles.kv.matchanswers differently for a stored list: a filter list now means containment.
Compiler
- A generated
<Schema>.writerefuses a buffer it does not fit in, instead of writing past the end of it. - A required schema field given as JSON
nullis rejected. - A source that fills the data segment gets a compile error, not a crash.
- The capacity limits are four times larger.
Library
io.nowandio.realtimestop the program when the clock cannot be read.net.watchandnet.nonblockreturn aboolinstead of the raw syscall result.kv.matchdoes containment when the stored value is a list.json.putraw,json.putstr,json.escslice,io.push,io.pushnum,json.putrealno longer write past the end of their buffer.- A negative position travels through the appenders unchanged instead of being treated as 0.
- New:
json.putb(dst, at, c)— writes one byte of JSON structure. - The standard library travels inside the compiler, so a downloaded binary resolves
include "io.wz"withoutlib/beside it.
Tooling
test-win.shno longer fails on its own cleanup.- A
.wztest runs in its own temporary directory.
0.1.0 — 13 September 2026
First public release.
Compiler
- An unresolved forward declaration names the routine, and every one is reported.
Library
- The session cookie is called
session. - The login, failure and logout pages are in English.