Repository navigation
The second public release: what the flagship and the first users asked for, from 0.7's plan
(ROADMAP), without that plan's first item, the checks on machines (below). The compiler is fuzzed in
every run of the suite (the same 300 programs each time) and builds programs as long and as deep as
generated code writes them; a stack overflow is a panic, a throw disposes the uses it passes over,
every value has debug info, and -g keeps the lines of an optimized build. Two memory errors 0.6.2
had are fixed: a value lent to a read or a call that another of its arguments freed first, and a
weak reference taking a later object for the one it pointed to. For the chip: constants in flash,
values placed in named sections (.noinit, a DMA region), fixed-point numbers, weak references and
arenas, and a file's tests run on it. Beside them: type providers (JSON, CSV, SQL, SVD, C headers
and providers of your own), properties with shrinking and zs fuzz, zs bench and zs test --coverage, workspaces, zs vendor and a lock that hashes its dependencies, and track S's small
items (Int's checked, saturating and bit operations, ISO 8601, UDP and name lookup), with ten
Advent of Code puzzles and three small tools as its exit tests.
Archives for Linux, macOS and Windows on x86-64 and arm64, each built on Linux x86-64 by
src/release.sh, whose compiler builds itself to the same bytes. Linux x86-64's suite passes in
the dev container (Fedora 44, LLVM 23), the chips' programs under QEMU and wasm32's under Node
among it, tests/pgo.sh and bench/host too. On Ubuntu 24.04 those two fail on that machine's
tools: tests/pgo.sh on LLVM 20's profile runtime, and bench/host's fib at 5.3% over its
instruction baseline (2,464,794 against 2,339,907, past the 5% the check allows). CI's Linux x86-64 job, in the dev container, failed two more on 2026-11-05, both fixed
here: tests/debug/kinds.zs, whose DWARF LLVM 23's llvm-dwarfdump --verify rejected for a
.debug_str_offsets entry inside a string GNU ld had merged by its tail (the one error the check
now lets by; lld's output has none), and the tree-sitter grammar, which didn't parse a typed _
parameter (let check () (_: Source)). What 0.7's plan put first is undone (ROADMAP, 0.7's item 1):
the flagship isn't flashed to a Nucleo-F401RE yet (it runs under QEMU's STM32F405, which models none
of the board's GPIO), the suite hasn't passed on the five CI runners (at fbf956d, on 2026-11-05,
Linux x86-64's passed and the others' failed: 23 and 24 checks on the Macs, the same 7 as before on
Linux arm64, and the Windows suites printed nothing; what the Macs and Linux arm64 failed on is
addressed after it, not yet confirmed in CI), and no archive has been tried on a clean machine, so
no macOS or Windows build has run on a real machine. Linux arm64's suite under QEMU and Windows
x64's under Wine, which 0.6.2 ran, haven't been run for 0.7, nor has the stress run over the pool,
tasks and Alt under ThreadSanitizer (the suite's four programs under ThreadSanitizer pass). Of the
plan's other items, the fuzzer's seed isn't rotated per run yet (it's 1 unless ZS_FUZZ_SEED says),
and [<Align>] on a type, Seq pipelines as one loop (track S, item 9) and fixed point's vector
forms are left.
Upgrading from 0.6.2
What a 0.6.2 program, project or script may have to change, and what behaves differently:
- A negative integer literal is a signed integer:
let a : u8 = -1, a-1passed where au8is
wanted and a pattern| -1at an unsigned type are errors (expected u8, got int;a negative pattern is a signed integer; u8 has no -1), where 0.6.2 negated the positive literal at run
time, which overflowed, and matched a pattern's value wrapped to the type with nothing said
(| -1atu8matched 255). To rewrite: the value itself (let a : u8 = 255,| 255). - A one-case union over plain values has no tag (decision 66):
type UserId = UserId of intis
anint, 8 bytes on a 64-bit target, where 0.6.2 put a 4-byte tag before it (16), sosizeof
andalignofgive its field's, and the records and arrays holding it are smaller. To
rewrite: anasserton such a size. - A throw, or a handler clause that stops without resuming, runs
Dispose.disposeon what the
uses it passes over bound, newest first, before the clause runs; 0.6.2 freed them without it. - A stack overflow on Linux and macOS (in a program, not under a sanitizer) is a panic,
panic: stack overflow (recursion too deep)with exit status 134, where it was a SIGSEGV (139
in a shell). - A dependency's module under a directory named
internal(at any depth, in either case) is the
package's own: naming it from another package is an error that lists the package's public
modules. To rewrite: the import, to one of those. zs.lockgains a hash of each git dependency's files at the next build or fetch (a 0.6.2 lock
is read as it is, and rewritten then). From then on a checkout edited under.zs-cache/deps/is
restored from its commit at the next build, and one that still differs is an error. To
rewrite: a dependency patched in place, as a path dependency (zs add ../json).Testing.Testhas two more cases,BenchandProperty. To rewrite: a match over a
Test(a runner of your own), with their arms or| _ ->. A test binary's--listgives a
fifth field, the entry's kind (test,benchorproperty).- With
ZS_LEAKCHECK=1, a test that leaks at its exit fails underzs test; 0.6.2 reported the
leak and passed it. zs build -g --release, and-gwith any-Olevel, is an optimized build that keeps the
lines; 0.6.2 built it at-O0.-galone is still-O0but on the Cortex-M0+ and wasm32,
where it's now the optimized build with lines.
Language
- A negative integer literal is one constant, a signed integer:
i32 -2147483648on a 32-bit
inttarget (a chip) was the positive literal, past the type's range, negated, and panicked
with an overflow (tests/mcu/ints.zs). Solet a : u8 = -1is an error (it overflowed at run
time), and so is a pattern| -1atu8, which says so;-0x8000_0000_0000_0000is i64's
least, as a value and as a pattern (tests/err_neg_unsigned.zs,tests/err_neg_pattern.zs,
tests/fold_edges.zs). - One-case unions without a tag (decision 66, built): a plain union with one case that has
fields is the field itself (type UserId = UserId of intis anintin registers, arrays and
records;sizeof<q15>is 2), or a struct of its fields; matching, equality, show, hash and
the debug info follow (tests/onecase.zs). One over a heap value (Wrap of string) stays
boxed, its count in the object. A[<CLayout>]record can't hold one yet (C's view of a
one-case union is left). - F#'s list and array ranges:
[ 1 .. 10 ],[ 1 .. 2 .. 9 ](a step, negative too) and
[| 0 .. n |], the list expression over the range, so one loop and the list built once, empty
when the range is (tests/ranges.zs). The README called them not there yet. - Typed tuple parameters as F# writes them:
let f (a: int, b: string) = …is one parameter of
typeint * string, calledf (1, "x"); each part takes its type (the error says so when one
is missing). The other not-there-yet of the README. useunder effects (track S, item 10): a throw, or any handler clause that stops without
resuming, disposes what theuses it passes over bound, newest first, before the clause runs,
as a cancelled task does (tests/effects/use.zs). It freed them without theirDispose.dispose.
Code without effects compiles exactly as before. A resumption a clause keeps and never calls,
and a stop inside atask { }body, still dispose nothing; a consumer that leaves a generator
early (Seq.take,Seq.tryFind) stops it, and that disposes.[<Section ".noinit">]and[<Align 32>]on a top-level value (0014 item 12, decision 69):
a constant table,Buffer.create n xorref xwith constant arguments has its storage in
the image, in the named section and so aligned (a table's data at a multiple of it after its
header), with no startup code and nothing allocated..noinitis neither copied nor zeroed by
the chip's reset handler (runtime/boards/cortex-m.ld), so a boot counter or a crash log
survives a reset; a push past a placed Buffer's room moves it to the heap and leaves the
storage. On a function, a computed value or an alignment that isn't a power of two from 8 to
4096, an error says what the attribute takes. A section named.noinit…(as the boards'
scripts'*(.noinit*)takes it) holds a ref or Buffer of plain values only (a string or array
there would point into memory a reset left), and not a constant (never loaded); a section's
name is letters, digits, '.', '_' and '$'. The element may be any constant (a fixed-point
number, a tuple, an option, a table), its own objects constants outside the section. On a
chip, a ref's or Buffer's storage in a section the board's script doesn't name (a DMA region)
is copied from flash at startup; a constant table alone in such a section is read-only and
stays in flash, but one sharing it with a ref or Buffer (RAM, never loaded) or in.bss…
(zeroed) is an error, as is a ref or Buffer in flash (.text…,.rodata…) or one in.bss…
that doesn't start at zero (a plain union's first case does), and anything in the board's own
.vectorsor.ARM…. A chip's library has no startup, so its placed values mustn't need that
copy or a.noinitref's header (a host's or wasm32's loads its sections like any other). A
placed Buffer's slots are dropped at exit whatever it did since (written, grown into the heap,
shortened). A program with placed values builds as one module, so they're placed wherever
they're declared (tests/placement.zs,tests/placement_drops.zs,tests/err_placement.zs,
tests/mcu/placement_errors.sh; on both chips, a boot counter across SYSRESETREQ resets under
QEMU,tests/mcu/noinit.zs,tests/mcu/sections.zs, and with--no-heap,
tests/mcu/noheap_placed.zs). On macOS, where Mach-O names a section
segment,section, the name is dropped and the alignment kept, as on Windows (it was emitted,
and stopped clang's backend).[<Align>]on a type is left.- Writing a Buffer's element is no allocation for
[<NoAlloc>]and--no-heap(it was taken for
an array's copy-on-write), and the write borrows the Buffer as a read does, so writing a
parameter's, a field's, an element's or a top-level Buffer frees nothing; the element it
overwrites is a drop when its type is counted, and so is a Buffer a call gave or a mutable
moved into the write, or one taken out of a value the write frees: a write is accepted only
where the Buffer is provably held elsewhere (a variable's, a top-level value's, a field's or an
element's), on every path of a match or an if (tests/noalloc.zs,
tests/err_noalloc_buffer.zs,tests/err_noalloc_buffer_call.zs,
tests/err_noalloc_buffer_taken.zs,tests/err_noalloc_buffer_arms.zs,
tests/err_noalloc_buffer_alias.zs). - A library (
--lib) may hold top-level constants: a table, a record, a tree of union cases
are static objects with no startup code, which a library doesn't have; only a value really
computed at startup is an error, from the code generator, and so is a startup assert
wherever it is among the values. Whether a value is a constant doesn't depend on the build:
-gand--coveragekeep the code as written, but a value whose initializer folds to a
constant ([| 1 + 2 |],[| Q15.ofFloat 0.5 |]) is that constant there too
(tests/libstatic.zs, from C, with-gand--coverage). module Weather = import json "weather.json"and the bareimport csv ("stocks.csv", separator = ";")
(0001): the provider's module, written to.zs-cache/providers/<importing module>/<Name>.zs
and loaded as a dependency of the file, soWeather.Root,Weather.parseand hover, definition
and completion on them are a module's; the bare form opens a hidden module named after the file
and the provider (StocksCsv), soRow,parseandprintare in scope, the module's own
names before the prelude's. Options are constants after the string, in a member call's syntax.
An import of a provider zs doesn't have, a file that isn't beside the importing file, or an
option the provider doesn't take is an error at the import. In the editor the types follow
the sample: the language server watches the files providers read and re-checks the documents
made from one that changes, and a rename of a provided name says where it comes from
(tests/providers/import_use.zs,tests/lsp/providers.sh).import sql "schema.sql": a record perCREATE TABLE(INTEGERint,REALf64,TEXT
string,BLOBarray<u8>,BOOLEANbool; a column an option unlessNOT NULLor in the
primary key) withtableandcolumnsin a module of its name.import svd ("chip.svd", only = "GPIOA,RCC"): a module per peripheral, each register a[<Bits>]record whose defaults are
its reset value and a module withread,writeandmodifyas the file allows;
examples/mcu/nucleo-f401-svd.zsis the Nucleo firmware on the STM32F401's SVD, compiled to the
same register accesses as the one with addresses by hand (tests/providers/sql_use.zs,
tests/providers/svd_use.zs,tests/providers/svd_nucleo.sh).- Providers of your own:
zs-provider-<name>on PATH, run in the importing file's directory
with the string and--name=valueoptions, its standard output the module (// provided by,
// input:lines first), its standard error the errors. The output is cached with a hash of
each input, so a machine without the provider builds from the last output, with astale-input
warning at the import naming an input that changed since. A dependency package with a
[provider] main = "Main.zs"section is the provider of its name: the compiler builds its
program into.zs-cache/providers/binwhen its sources change and runs it the same way
(tests/providers/pkg,tests/providers/shout).examples/providersand the guide's chapter
18 show all of it. import con the providers' protocol:module Ray = import c "raylib.h"is the header's
declarations as a module, each with its C signature and comment as its doc, and
zs provide c raylib.hprints that binding as Z# to keep (externs,[<CLayout>]records and
unions, constants from macros; the names left out listed with why). The bareimport ckeeps
its path,[<Link>]and the editor's C hover (tests/providers/cshapes).sql'squeries = "queries.sql": named queries (-- name: GetUser :onebefore each, as sqlc
writes them) as a module each, typed against the schema: the text with?for each parameter,
the parameters' names, aParamsrecord typed from the columns they meet (compared with, or
inserted into by position), and aRowrecord for a select's columns (SELECT *a table's,
a join's by table or alias, an expressionAS name). A column or table not in the schema is an
error naming the query (tests/providers/sql_queries_use.zs).M.fnames the externfdeclared in moduleM(a C function is one global name, and was
reachable only bare):Shapes.rect_areaon amodule Shapes = import c "shapes.h"
(tests/providers/cshapes).svd: a field's enumerated values that are 0 to n-1 are a union of cases, named after the list
(Valueadded when a register's record has the name), shared by the fields with the same
values; sparse values stay a number (tests/providers/svd_enum_use.zs).
Compiler
- Constants folded (0015 step 4): arithmetic, comparisons, bit operations, conversions and
Math.roundandsqrton literals are computed at compile time, as the code would compute
them, and aletof a float literal puts the literal in its uses; an integer result that wouldn't
fit its type stays as written, to overflow at run time (inttaken as 32 bits, a chip's).
Integers fold only when they're values of their types (au64at or above 2^63 isn't folded
with signed arithmetic),f32is rounded to single precision at every step, the sign of zero
is kept (-0.0,Math.round -0.3), a NaN result isn't folded, and a conversion nested in
another wraps as the code does. A static'sf32literal is single precision (so widening it
is exact), and anintorusizepast 32 bits converted to a wider integer or a float is
left to the code, as a chip wraps it. A match with a pattern literal, at any depth, that isn't
a value of its type (| Some 256atoption<u8>) is neither decided at compile time nor made
a decision tree, so the code's comparison at the type's width decides it, in order. So
Q15.ofFloat 0.5is a constant once inlined, and a table of them a static object
(tests/fold.zs,tests/fold_edges.zs,tests/static.zs). - Constants in flash, the rest of item 3 (0014): a top-level record, tuple or plain union of
constants is written into its global, a boxed union case (a tree's node) is a static cell, and
they nest any way (an array of records, a list of shapes, a record holding a table). A boxed
case of constants was a constant built at each use and is a value now, so it's static too.
Nothing of these runs at startup (tests/static.zs,tests/mcu/static.zs). - Constants in flash (0014, item 1, for arrays and lists): a top-level array or list literal of
numbers, bools or strings is a static object in the image (count 0, never unique: a write
copies it), the value's global pointing at it, with no startup code and no heap; on a chip its
bytes are in flash (tests/static.zs,tests/mcu/static.zs: three tables on a Cortex-M4, 64
bytes of heap and three allocations gone, and the image 496 bytes smaller without their
startup code).--no-heaptakes such a value. - A union with a case holding a list of itself (a tree) builds for the Cortex-M0+: its drop
function's last call is a plain call there, where Thumb1 can't tail-call another function (a
loop still, as a list's drop is a call to itself); LLVM refused the image before
(tests/mcu/tree.zs). - A record field's default resolves its names in the record's declaring module, not where the
record is built: a provided register'smode0: ModeValue = Alternateholds for a literal in
another module (it was "unknown name 'Alternate'" there;tests/providers/svd_enum_use.zs). - Long and deep programs, as generated code writes them, build: a flat list literal of 20,000
elements, 10,000 statements orlets in one function, anelse ifchain or nested parentheses
10,000 deep each crashed the compiler (its stack overflowed); its threads now have 256 MB of
stack where the system's hard limit allows, and the pool's threads the softulimit -swhen
that's over 8 MB (tests/stress.sh). - Nothing quadratic in what was: an array literal of 100,000 elements checked in over two minutes
(each element's type variable was bound to the one before, and unification walked the whole
chain: variables are now bound straight to the end of a chain once it's walked, trailed so a
trial undoes that too), 2.4 s now; a chain of 1,000 small functions, each calling the one
before, took over a minute to inline (each copy carried every one before it, and whether a
function could reach itself was a walk per function), 1 s now. A copy inlined carries at most
four times a small body, so twelve of the test programs compile to up to 38% less IR, running
the same. - A type with a member declared twice, or a member named as one of its fields, and a call to one
of its members crashed the compiler (panic: Map.find: key not found;tests/fuzz.sh's seed 1
found it): the member that can't be is reported and left out, and the type's other members
stand, so the call checks. A call to a member whose function never got defined is an error
naming it, never a crash (tests/err_member_twice.zs). - A UTF-8 byte order mark at the start of a file is skipped, as
\r\nis read as\n; it was
"expected 'let'" (tests/stress.sh).
Runtime
- A panic's backtrace names the Z# functions on the way (0011, debugging 5), the runtime's own
frames left out and the program'smainlast: on Linux the runtime reads the executable's
own symbol table at the panic (a build keeps it; the functions are internal, so nothing else
names them), in release builds too, and names the function of each call (not the one after
it). On macOS, and in a library built into a C program, it's the C library's symbolizer, as
in 0.6.2.ZS_BACKTRACE=0keeps the panic to its message, as before (tests/backtrace/run.sh).
Lines need-g's DWARF and are left. - Weak references and arenas on a chip (track M, item 8, known gap 3): the embedded runtime
has the host's table of weak cells and its arenas, over the chip's heap. An arena's blocks are
an eighth of the heap each (4 KB at most), bumped through, and go back at once when the
arena's last object has died; an object's arena is found by its address among the blocks. The
table and the blocks are heap memory, not objects, so the exit report'slivestays the
program's. Released blocks are kept for arenas (on the heap's list of large blocks any object
over 256 bytes would take a whole one), but a larger freed block an arena took goes back to
the heap when it's released, and the heap takes the arenas' blocks back before it runs out;
a block is a multiple of 8, so a heap size that isn't a multiple of 64 keeps the heap aligned
(tests/mcu/weak.zs,tests/mcu/arena.zs,tests/mcu/arena_rounds.zs,
tests/mcu/arena_big.zs,tests/mcu/odd_heap.sh, both chips). - Panics on several threads at once give one report: the others wait for its stop, and a panic
inside a panic stops at once (tests/backtrace/run.sh: four threads at once). A panic's
message is printed by its length, so a NUL in it doesn't cut it short. - A stack overflow is a panic:
panic: stack overflow (recursion too deep), exit status 134, what
was printed before it flushed, on whichever thread overflows (the pool's, a task's loop's;
two at once say so once). It was a SIGSEGV with nothing said. On Linux and macOS, in programs
(a library leaves its C host's signals alone), not under a sanitizer, which reports one itself
(tests/panic_stack_overflow.zs,tests/panic_stack_overflow_thread.zs).
Security
- A value reached through a pattern variable is lent to a read, to a call that borrows it or to
a closure's application only when no other argument takes the value it was matched from: one
that did (handing it to a function that frees it) freed it before the read or the call used
it, in 0.6.2 too. And alet mutablepassed to a function that borrows it, which another
argument reassigns, is read where it stands, the arguments bound left to right, and kept alive
through the call; the assignment freed it first (tests/buffer_lend.zs, clean under valgrind). - A weak reference to an object the runtime frees itself (a weak cell, an
Spsc, aChannel)
sees it die: those frees go through the checked path, so a later object at the same address
isn't taken for it (tests/weak_runtime.zs; on both chips,tests/mcu/weak_runtime.zs).
Library
- Properties (0010, track D item 5):
testProperty "name" (fun (xs: list<int>) -> …)runs
the property on 100 cases drawn withArbitraryat the parameter's type (a tuple for
several), growing from size 0 to 100;unit,trueorOkpasses a case,falseor an
Errorfails it, and the failing case is shrunk to a smallest one (its bytes shorter, then
smaller, as Hypothesis shrinks) and shown with the seed that replays the run through
testPropertyWith { seed = Some s };{ runs; maxSize; maxDiscardRatio; shrink }are the
other settings. A generator (Gen<'a>) reads a stream of bytes, so every one shrinks:
Gen.int (), the sized integers,float,bool,charandstring(each a function of
unit),range,elements,frequency,oneOf,map,bind,filter,list,array,
listOfLength,option,sized,scale,constant, thegen { }builder, and
Gen.sample seed n gto look at one.Arbitraryhas impls for Core's types, lists, arrays,
options, results and tuples, and[<Derive(Arbitrary)>]for records and unions: a union's
cases are ranked by how readily a value ends (0 when no field can hold the type, 1 when only
lists, arrays and options do, which are empty at size 0, 2 when only a type parameter might, 3
when a field is or reaches the type), drawn in that order, and at size 0 or past a replayed
stream's end the lowest rank's with the fewest fields are the only choice. So a tree whose
leaves carry values, one with a way out through a list, and a generic one (Expr<'v>'s
Lit of 'v, aLabeled<'a>holding the type) end; a type whose every case holds it outright
(through aresult, say) may not past a replayed stream's end, and a generic union's
type-parameter cases come after those that hold nothing and before those that hold the type
(the derives gainShape.cases, each case'srankamong its facts).Gen.optionisNone
at size 0;Gen.rangespans all ofintwithout overflowing and
Gen.frequencysums its weights in u64; andGen.chardraws any scalar value but a
surrogate, the controls last, so a case shrinks to a printable one
(tests/property/recursive.zs).prop { let! x = Gen.int () … do! Prop.assume (y <> 0) … return … }draws as it goes, discards a case (Prop.assume) or labels it (Prop.label,
classify,collect): a passing property reports how its cases were labelled, a failure
shows the failing case's labels.Prop.model commands { init; step }checks a system
against a model with a derived sequence of commands, which shrinks to the fewest with the
smallest arguments. Underzs test, a case that panics is recorded as drawn (each case from
its own seed), shrunk by replays of the test binary (--replay) to a smallest case that
still panics, shown, and saved beside the file in.zs-cache/failures/<file>/, keyed by
the test file and the property's path; the next run replays it first, and once it passes
it's deleted. A panic the case doesn't reproduce alone, and a run that gave up on its
discards, are reported as they happened, nothing shrunk or saved; a shrink keeps the failure
it started with (its panic or sanitizer report with its values left out, an assert's place and
the function it panicked in kept; a program's own case names count as the message's words, as
Some,None,OkandErrordon't), so a candidate that trips another bug, another assert,
or ends in a stack overflow, isn't taken (tests/property/basic.zs,tests/property/labels.zs,
tests/property/model.zs,tests/ztest/property.sh). Numerics.Fixed(0015, track M item 10):q7,q15andq31, each a one-case union over
its integer (its size, by decision 66), with saturating+,-,*(rounded half up),/,
%(computed wider, somin % -stepis 0) and negation,mulAdd,abs,clamp,
ofFloat/toFloat/toF32,ofInt,raw/ofRaw, the conversions between the formats, and
Showas the decimal with the digits the format needs (0.99997forQ15.max), in integer
arithmetic.List.sumover them saturates like+.Q7andQ15
compute in 32 bits, the chips' word, their products and quotients[<Wrapping>](they can't
overflow):bench/mcu/firq15.zs, a 32-tapq15filter, executes 1.05 times the instructions
of C in CMSIS-DSP's style on the Cortex-M4 and 1.01 times on the Cortex-M0+, at-O2.ofFloaton a literal folds to a constant
(Compiler, above); the vector forms are left (tests/fixedpoint.zs,tests/panic_fixed_div.zs,
tests/mcu/fixed.zs).Net.Udp(track S, item 8):Udp.bind host port(0 for a port the system picks,Udp.port
says which),Udp.sendandsendTextto a host and port, andUdp.receive, which gives the
bytes (a longer datagram cut to the buffer) and the sender's address and port, each waiting on
the event loop asTcpdoes;usecloses the socket.Dns.lookup host: a host's addresses,
looked up off the loop's thread withTask.blocking, an empty list for a name that has none
(tests/task/udp.zs).DateTime.parseIso(track S, item 6): the moment ISO 8601 writes,2026-11-04T08:30:00Z, with
a fraction (.25, to nanoseconds), an offset (+02:00, taken off) or no zone (UTC), without
the seconds, or a date alone, a space for theTtoo;Nonefor anything else, a day its
month hasn't and a year outside 1678 to 2262 included; a moment after
2262-04-11T23:47:16.854775807Z, past what ani64of nanoseconds holds, panics with an
overflow instead. AndDateTime.ofParts year month day hour minute second nanos,Noneout
of range, with the same exception (tests/datetime.zs).Int's checked, saturating and bit operations (track S, item 3):Int.checkedAdd,checkedSub
andcheckedMulgive anoption;saturatingAdd,saturatingSubandsaturatingMulstop at
the type's bounds;popCount,leadingZeros,trailingZeros,rotateLeft,rotateRightand
byteSwapare the bit operations, onintand every sized integer, an instruction each where
the target has one (CLZ, ARM's saturatingQADD), on the chips too (tests/int_ops.zs; on
both chips,tests/mcu/ints.zs).
Packages
- On a file system that ignores case (macOS's), a package's module under
internal/is found: the
resolver triedInternalfirst, which exists there forinternal/too, and then refused it, so
a path dependency whose modules use its own internal ones didn't build. - Workspaces (0018, item 8):
[workspace] members = ["app", "libs/*"]in a rootzs.toml(a
pattern ending in/*takes the directories under it that have azs.toml). Its members
resolve together into onezs.lock, one.zs-cache/and onevendor/at the root; a lock
inside a member is an error naming the root.zs build,zs test,zs fetch,zs update,
zs treeandzs vendorat the root run over every member (a library member isn't built on
its own);zs run,zs addandzs removethere say to run them in a member
(tests/project/workspace.sh). zs.lockhashes each git dependency's files (0018, item 6): a checkout that was edited is
restored from its commit at the next build or fetch, and one whose files still differ is an error
naming it (zs updateaccepts the new files).zs vendorcopies the dependencies into
vendor/, where the project reads them from and never fetches, their hashes checked;zs build --offline(any command's--offline, orZS_OFFLINE=1) is an error instead of a
fetch.zs newputs.zs-cache/in.gitignore. The hash is the compiler's xxhash until
stage 0's library hasSha256(tests/project/vendor.sh).zs add ../text,zs add https://…/json.git@1.2(a release) or@v1(a rev),--rev main:
the dependency written intozs.toml's[dependencies](the name from the package's
zs.toml, or the URL's last part) and fetched;zs remove jsontakes one out, the lock
rewritten, naming the ones there when it isn't;zs treeshows the dependencies as resolved,
each with its version, where it comes from and its locked commit, its own under it (0018,
item 7;zs add json@1.2by name waits for the index;tests/project/pkgs.sh).- Internal modules (0018, item 3): a file under a package's
internal/directory (any depth)
is for the package's own files; from another package its name is an error naming the package
and its public modules. A module's directory may start in lower case now (internal/for
Internal), as a file could. - A microcontroller build names a dependency that uses Std when it starts (0018, item 5):
package 'logger' uses File, part of Std, which needs an operating system; a build for cortex-m4 has none, so it can't depend on this package, instead of an error deep in its code.
Tooling
- Completion in the language server lists what a program declares, not only its functions and
types: effects and their operations, traits, type aliases, union cases and modules, with a
declaration's///comment as its documentation; a library module's private names are left
out. After a module's name and a dot (Weather.,List.,Collections.List.) it lists that
module's members, a provided module's among them (parse,print,Rootfor a JSON sample),
where it listed nothing (tests/lsp/providers.sh). - On macOS,
zs fuzzlinks libFuzzer's runtime for macOS (libclang_rt.fuzzer_no_main_osx.a) and
libc++, andzs build --lib -o x.soleaves a function the program defines (anexternof the
library's) to the loader, as ELF's linkers do (-undefined dynamic_lookup): the one found
nothing, the other didn't link. zs --versionprints the compiler's release, and the language and MCP servers report it in
serverInfo(they said0.4);test.shchecks it againstsrc/zs.toml's.zs build -g --release(and-gwith any-Olevel; 0011, debugging 3): the optimizer runs
and the DWARF lines stay, so gdb and lldb break on a line of an optimized build and show its
backtrace with Z# files and lines; a value the optimizer keeps is shown, an inlined call's
lines are its caller's frame's.-galone is-O0with the code as written, for stepping,
on every target (a chip's-O2default included) but the Cortex-M0+ and wasm32, whose-O0
can't lower Z#'s tail calls: there-galone stays the optimized build with lines. Code as
written is what[<NoAlloc>]checks then, so a function passing a lambda to a library's
higher-order function may need-O2(tests/debug/opt.sh,tests/mcu/stepping.zs).- Debug info for every value (track D, item 1, known gap 4): a list, array, boxed union,
ref
orBufferhas its type's name over a pointer to its object, so gdb'sprint *xsshows a
cell's count, head and tail, an array's count, length and elements, a node's case (where the
cells carry one) and each case's fields over the cell, a ref's value; an option over a string
is the string's pointer, a closure or one of the runtime's cells an address, and afixedan
array. Nothing is left out ofinfo localsfor its type now, and a type that holds itself
through a list, a ref or an array is described once; a SIMD vector has no DWARF type of its
own, but its size lays out the fields after it, as each target aligns it (tests/debug/gdb_heap.sh,
tests/debug/kinds.zs, whose DWARFllvm-dwarfdump --verifychecks). zs test --coverage(track D, item 10): the test binary is built with a counter per line of
the file's directory's sources (--coverageon any build: the program writes the counts to
<program>.zscovat exit, a panic's included), and the command reports the lines run and
not run per file, with the runs of lines not run, and writescoverage.lcov, whichgenhtml
renders and coverage services read. Match arms, branches and loop bodies count by their
lines; the counters are atomic, so threads count right. A generic function's instance counts
its definition's line, so one defined in another module and run isn't listed as never run;
code nested on a line already counted isn't counted again;coverage.lcovis this run's,
each test file of a workspace adding its own; and--coverageon a chip is an error, since
the chip has no file to write the counts to. The counts are written once, whichever of a
panic,Environment.exitand the end ofmaincomes first, on whichever thread
(tests/ztest/coverage.sh).zs bench file.zs [filters] [--runs n](track D, item 9):bench "name" (fun () -> …)is a
Testvalue among a file's[<Tests>], timed in a process of its own after three warm-up
runs: the median of 30 runs (or--runs) and its 95% confidence interval from the order
statistics. The results are saved beside the file (.zs-cache/bench/<file>.tsv), and a
benchmark whose interval doesn't overlap the saved one's, its median 10% or more away, is
reported slower or faster;zs testruns a benchmark once, as a test of its body.
testFaster "name" fast slowis a test that passes whenfastwins 19 of 25 paired rounds (a
sign test at 1%), so a performance claim can't silently regress (tests/ztest/bench.sh).zs fuzz file.zs path [--for n] [-- flags](track D, item 6) runs one property under
libFuzzer (the tests built with its instrumentation and the sanitizers; a panic aborts, so
it's a crash), each input the property's bytes (Gen.bytes ()takes an input whole, so a
property over bytes fuzzes a parser as it is), the corpus kept beside the file in
.zs-cache/fuzz/<file>/, keyed by the property's path, so a run resumes (for--for's
seconds, or as long as libFuzzer's own flags say), the flags after--libFuzzer's but for
those that start other processes (-jobs,-fork: it runs in one), its output read apart from
the property's so neither stream blocks the other; a crash is shrunk by replays, shown, and
saved as the failurezs testreplays first (tests/fuzz/fuzz.sh).zs test file.zs --target cortex-m4(track D, item 11): the file's[<Tests>]built into one
image and run under the chip's QEMU board (or a real board through probe-rs, aszs rundoes),
every test in one process in order: Core'sTesting.runAllprintstest pathbefore each,
ok pathafter it andskip pathfor a pending one, and a failing test's panic ends the run,
zs testnaming it with what it printed, every line of the panic kept (blank ones, a
property's case and replay seed); the counts as on the host. A property runs its cases there
too, nothing recorded or shrunk, and a test returning a task runs on the chip's event loop
(tests/mcu/ztest.zs,tests/mcu/ztest_fail.zs, both chips; neither has a task's test yet,
so that one was checked by hand on both).zs mcphasdefinition(where the name at a position is defined:file:line:columnand the
defining line, in the program's files or the library's) andexplain(its type with
constraints, its effects in words, where it's defined, and its doc comment, a built-in's
included) besidecheck,type_at,docs,build,run,emit_llvmandformat(track E,
item 3;tests/mcp/session.sh).zs test --stress 10m(track D, item 4): the tests at random on every core, over and over for
that long (30s,10m,1h), each run its own process as always; a failure one run hides (a
race, a leak on a rare path, a timeout under load) shows, each failing test shown once with its
first failure. WithZS_LEAKCHECK=1, a leak at a test's exit (leak check: n object(s) still allocated) fails the test, in one run too; it was reported and the test passed
(tests/ztest/stress.sh).zs provide json <file>andzs provide csv <file>print a module from a data source, to keep
beside the program (proposal 0001, track P's first
step):jsongives a document's records with theJsonderive andparseandprint, an
object a record named after its key (two with the same fields share one), an array a list,
nullor a key some objects lack an option, a numberintwhen every occurrence is
integral, a key that isn't a name a``backtick``field, with--root=Weatherand
--types=temp:f64,readings.value:f32;csvgives aRowrecord from the header and the rows
(int,f64,bool, elsestring; an empty cell makes a column an option) withparseand
printthat read and write quoted cells, with--separator=;,--hasHeaders=falseand
--types=volume:f64.zs provide sql,svdandcprint the other providers' modules the
same way, andzs provide <name>a provider of your own on PATH;zs provide --listnames the
five and those on PATH (tests/providers/provide.sh).- The tree-sitter grammar, and so Zed, parses the providers' imports (named, bare, with
options), list and array ranges and typed tuple parameters; the Zed extension pins the
grammar's commit with them.
Performance
x <- f … x …hands the call x's own reference when the other arguments only read x, so
counts <- HashMap.add w (Option.defaultValue 0 (HashMap.tryFind w counts) + 1) counts
updates the map in place instead of copying it on every update (a word count of 1M words: 84 s,
now 0.23 s); the same for arrays (a <- Array.set a i (a.[i] + 1)). An argument that assigns
x, or writes its array in place, keeps the copy (tests/cow_args.zs,tests/cow_split.zs).- Only a program that can put a SIMD vector in a heap object has its objects 64-byte aligned by
the system allocator; one with vectors in locals only keeps the pool. Calling
Bytes.appendJsonStringno longer costs a whole program the pool (a JSON round trip of 200k
records: 3.5 s and 935 MB, now 1.4 s and 368 MB;tests/simd/pool.zs,
tests/simd/pool_locals.zs,tests/simd/pool_wide.zs,tests/simd/pool_wide_indirect.zs).
Examples
examples/aoc: ten Advent of Code puzzles (2015 to 2021's first days), each solved on the
puzzle's example input with only the prelude, Core and Std, 203 lines in all: track S's exit
test that the library is enough for a day's puzzle (tests/examples_aoc.shchecks the
answers).examples/tools: grep (38 lines), a JSON pretty-printer (16) andtail -f(49), each with the
prelude, Core and Std alone (track S's exit test), built and run bytests/examples_tools.sh.
Benchmarks
bench/hosthas five small real programs beside its kernels, in Z#, C and Rust:wordfreq,
json,interp,pathfindandsha256(the report).
Tests
- CI's macOS runners get coreutils'
timeout, which./test.shand its scripts call and macOS
doesn't have: most of each Mac's 130 or so failures on 2026-11-04, every guide example among
them, were its absence.test.shbuildstests/placement.zsfor macOS and Windows from any
host and checks no section name is left, and expects the ELF names in a native build on Linux
alone.tests/debug/kinds.zs's DWARF check fails ifllvm-dwarfdumpcrashes. - The scripts run with BSD's tools, as macOS has them (a
wc -lcount's padding,sed -i,stat -cand\|in asedpattern were GNU's), and two resolve their temporary directory as the
compiler names it (macOS's/varis/private/var). A check that needs QEMU, Node or valgrind
skips without it, as the others did;tests/simd/arm.zsruns natively on aarch64 Linux;
bench/host's baseline counts x86-64 Linux's instructions and is checked only there;
tests/debug/gdb.shandlldb.shcompare a frame's locals in name order, which debuggers list
differently by machine. - CI: the Intel Mac gets LLVM 23 from conda-forge (Homebrew's newest for Intel is 22, and LLVM's
releases have no build for it), both MacsSDKROOT, so clang and libclang find the SDK's
headers, and each native runner builds and runs one program first, each step bounded, and
stops after three hours, so a hang shows where it is. tests/lsp/providers.shedits its sample only once the server has answered the requests before
it (a pipe the script writes as the server answers), so what the first hover and completion
see no longer depends on who was faster; it failed one run in four under load../test.sh --sanitizebuilds every program with the address and undefined-behaviour sanitizers
(zs build --sanitize) and runs it, a report failing the test as valgrind's does, beside
--valgrindand--release. A.plain-onlyfile beside a test gives the reason it runs only as
built (it measures the pool, or the runtime's stack guard), skipped under both../test.sh --releasebuilds every program optimized (zs build --release), so what LLVM may
assume of the IR the compiler writes (no overflow where it saysnsw, no aliasing where it says
noalias) is tested too: each program must print the same.tests/stress.sh: long literals, long runs of statements and lets, deep nesting and a byte
order mark build in time and run; a statement failing after a long literal is one error, the
next statements checked; deeper than any stack is the panic above, not a crash.tests/fuzz.sh: 300 programs broken at random (seeded:ZS_FUZZ_SEEDpicks them, 1 unless
it says,ZS_FUZZ_COUNThow many) throughzs checkandzs fmt --stdin: errors, never a
crash, a panic or a hang.