Repository navigation
Releases: ZigoteProject/zsharp
Release list
Z# 0.7.0
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 b...
Z# 0.6.2
The first public release, for microcontrollers: firmware for a real board, the ST Nucleo-F401RE,
written as tasks on the runtime's loop, with interrupt handlers, registers as [<Bits>] records,
code checked not to touch the heap, and its log over RTT (examples/nucleo). Beside it: package
versions and the orphan rule for impls, Concurrent ML's events for tasks, lldb, doc tests,
zs doctor, allocation hints in the editor, and the guide's page for F# programmers.
Archives for Linux, macOS and Windows on x86-64 and arm64, each cross-built from Linux x86-64.
Linux x86-64's suite passes natively, Linux arm64's under QEMU (its compiler builds itself to
the same bytes), Windows x64's under Wine; the macOS builds and arm64 Windows are untested on
a real machine. The flagship runs under QEMU's STM32F405, not yet on the board itself.
Language
- What an F# programmer writes in the first hour parses:
let y = x + 1 in y * 2; a condition
continued on a line starting with||or&&(and|||,&&&,^^^); a record on the line
after its constructor (Somethen{ x = 1 }indented);match a, b withwithout
parentheses.handleis a keyword only where a handler starts, solet handle x = …is a
function.[<CLayout>]and the other attributes apply to aprivatetype. [<Unsafe>]on a function: onlyunsafecode may call it (Bytes.writer, which writes
wherever its pointer points, is one).- A top-level
Bufferof one type (let private seen: Buffer<int> = Buffer.empty, or a record
holding one) is one Buffer, computed once at startup, so a push to it is seen at every use; it
was a new Buffer at each use, and the push was lost. A generic one,Buffer.emptyitself, is
still a new Buffer at each use. - The orphan rule (K9): an impl written lives in its trait's package or its type's, so two
packages can't both write one that a program using both would have twice. To rewrite: an
impl of the library's trait for the library's type, or for a dependency's: wrap the type in a
one-case union of your own. A hand-written impl used before it's checked (further on, or in a
later module) is an error saying to write it before its first use; it was a wrong
"does not implement" or "already implemented". - Patterns at the top level:
let (width, height) = (800, 600)declares both names. A tuple of
constants stays constants; anything else is computed once at startup. A locallet's pattern
takes an annotation (let (a, b): int * string = …), and aletwhose pattern can fail says
so:a let's pattern must match every value, and this one misses None. - Layout facts:
sizeof<T>,alignof<T>andoffsetof<T> "field"areintconstants for the
target, a[<CLayout>]type's as C lays it out. A top-levelasserton constants is checked
when compiling (assert (sizeof<Packet> = 16)), naming what it read when it fails; any other
top-levelassertruns at startup. The headerzs build --libwrites checks each struct's size
with_Static_assert. - C unions: a
[<CLayout>]union has no tag, each case a view of the same bytes. It's built by
case (the bytes after the case's field zero) and read by case name insideunsafe
(unsafe d.U32); it can't be matched, compared or printed. It crosses to C by value as C
passes a union on every target, sits in[<CLayout>]records, and is declared in--lib
headers; debuggers show it as C's. Hovering a[<CLayout>]type shows its size and alignment. - The
cyclewarning (0021): a type whose objects can form a reference cycle through aref,
Buffer,MutexorChannel(two ways back, or two types reaching each other), which
counting never frees, is warned where it's declared, with the path and notes at the other
types' fields, and the fixes: aweakback-pointer,[<Cyclic>]on a type whose cycles the
program breaks, or[<NoWarn "cycle">]. - A public constant that uses a private one of its module works from other modules (it was
'x' is private to module Mat the use). - A
type privaterecord's fields are private too: outside its module, code can't read, set,
build or match them, even on a value it reached through a public record's field. - A task cancelled at a wait no longer leaks a value it took apart with a tuple pattern
(let! (x, reply, nack) = …) before that wait.
Platforms
- The flagship,
examples/nucleo: firmware for the Nucleo-F401RE as a project (zs buildthere
builds it for the board,zs testtests its shell on the computer), three tasks on the
runtime's loop: the LED's, the button's (Irq.wait) and a serial shell fed by USART2's
handler through anSpscand aSignal, its registers[<Bits>]records, its log over RTT.
./test.shruns it on QEMU's STM32F405, typing at its USART2. - Tasks on a microcontroller (0016): the same
task { },Task.run,sleep,both,all,
race,timeoutandscopeas on a computer, on the embedded runtime's event loop: timers on
SysTick, the core asleep (wfi) when nothing runs.Irq.wait nis a task that goes on once
linenfires;Signal.setfrom a program's own handler wakes the task inSignal.wait.
Runtime.setClock hzafter changing the core's clock.Taskmoved from Std to Core (the same
namespace, so nothing changes on a computer); on a chip, reachingTask.spawn,
Task.blockingorTask.waitForis a compile error naming the chain.examples/mcu/uart.zs
receives a line in a task waiting for the UART's interrupt.Task.fromCallbackmakes a task
of a callback, for a driver's own waits. - A real board, the ST Nucleo-F401RE (
--target nucleo-f401, 0027): linked for its STM32F401RE
(512 KB of flash at0x08000000, 96 KB of RAM), with its interrupt lines by name
([<Interrupt "USART2">]), and output over RTT, a ring in RAM the debug probe reads while the
chip runs.zs run --target nucleo-f401flashes it with probe-rs, which shows the output and
returns the exit status. With no probe attached, nothing waits: output that doesn't fit is
dropped, and an exit leaves the core asleep (semihosting's breakpoint would fault there).
Targets are found by name, so a board shares its core's triple:[target.nucleo-f401]and
[c.nucleo-f401]are its own. - A chip's image keeps its symbol table (
zig ccdropped it at-Os) and its entry point (it
was 0, where a debugger'sloadjumps). - 32-bit Arm Linux (
--target arm-linux-musleabihf) builds with 32-bit words, as its runtime
has: it built with 64-bit ones, linking against a runtime that read them as 32. - Interrupt handlers (0012):
[<Interrupt 38>],[<Interrupt "UARTRX0">](the board's names,
runtime/boards/<board>.irq) or[<Interrupt "SysTick">]/"HardFault"/… on a
unit -> unitfunction make it the line's handler over the runtime's weak defaults. A handler
may reach only what'sShare(interrupt handler 'tick' uses the top-level value 'count' : ref<int>, …);Atomicis in Core now (masking interrupts on a Cortex-M0), andSpsc<'a>is a
queue of one producer and one consumer that neither waits nor locks.Irq.enable,disable
andpend. Cpu(0012):wfi,wfe,sev,dmb,dsb,isb,nop,enableInterrupts,
disableInterrupts, one instruction each, andCpu.critical (fun () -> …), its body run with
interrupts masked (it nests). On a computer the barriers are fences and the rest nothing; masking
is an error there.[<Bits>]records (0013): a record that is one unsigned integer, its fields ranges of its
bits (bool, enums,u8tou64,[<Width n>]and[<Bit n>], the first field
attributes):{ ue = true; te = true }is one constant, a read a shift and a mask,
{ r with f = v }a masked write keeping the gaps; compared as the integer, printed as the
record.Bits.toU32,ofU32and the other widths,rawandofRaw.[<NoAlloc>]and--no-heap(0012): a function that, or anything it calls, may allocate or
free is an error naming the chain (reply is [<NoAlloc>], but reply -> describe builds a string); interrupt handlers andCpu.critical's bodies are checked the same way.
--no-heapchecks the whole program, startup values included, and builds a chip's image with
no heap.- A string built with
+(an interpolation is one) is one allocation: its pieces are joined at
once, where each+made a string of its own ($"{name}: {qty}\n"made five, and makes one
besideqty's text). An empty literal piece is left out, and a piece alone is the string
itself, so$"{s}"allocates nothing and passes[<NoAlloc>]. [<NoAlloc>]passes code that frees nothing: dropping a value matched to a case without
fields (a static cell), an option'sSome skeeping its string, andstringof a string (its
own text). Its errors name an operator as written (Ledger.<+, which showed asLedger.) and
a dropped local without the compiler's numbering ('o', not'o.8').- A function that makes a span over a parameter (
Array.fold f 0 xsover an array) borrows it. Runtime.tickson a chip counts a SysTick wrap its handler hasn't run for yet, and never goes
back (it could, by a wrap, right at one).- An
[<Export>]function returningunitcompiles (the compiler panicked in its C wrapper). - C interop on a microcontroller, both ways: structs by value follow AAPCS (they were passed
with x86-64's rules), hard and soft float;[<Link>]C sources are compiled for the chip; C
callbacks work there.zs build --lib --target cortex-m4makes an archive and its header for a
C firmware, which takes the library's output, stop and ticks by definingzs_write,
zs_stopandzs_ticks. - WebAssembly:
[<Link>]C sources are linked in, and--lib -o lib.amakes an archive for a C
program built for WebAssembly. import cbrings C unions in, a member a case named through its union (epoll_data.fd 3,
unsafe ev.data.fd), and a str...
Z# 0.6.1
The first release for more than Linux: the compiler for Linux x86-64, macOS arm64 and
Windows x64, each built by the macOS one (src/release.sh).
Platforms
zs build --targettakesaarch64-macos,x86_64-macos,x86_64-windows-gnu,
aarch64-windows-gnu,aarch64-linux-gnuandwasm32(freestanding, with a Node host):
clang compiles the IR, zig cc the runtime and the link, no SDK.- The compiler runs on macOS and Windows. On Windows, a build without
--targetgoes through
mingw-w64; file names, arguments and the environment are UTF-8; paths work across drives. - The runtime's loop on epoll, kqueue and IOCP (wepoll); sockets, processes and pipes on Windows.
- Cross builds are the same bytes each time; ThinLTO caches on macOS.
Compiler
- Every mistake of a declaration is reported, not only its first.
- The back half of the compiler runs on all cores; it builds itself faster than the F# compiler did.
- Optimized builds drop what nothing reaches: a hello world is 17 KB, not 89.
import cworks from a fresh clone, and for a target finds its system headers.- A project's modules are found from its main's folder, in any subfolder.
Editor
- LSP: a function's signature at the end of its line; hover, references and definition of
type names.
Library
Task.runwaits for the spawned tasks it cancelled.
Fixes
- A dependency's name can't reach outside
.zs-cache/deps; a rev starting with-isn't
passed to git as an option; MCP tools refuse symbolic links. - A file with a top-level expression reports a parse error instead of panicking.
- The Windows runtime builds with newer mingw-w64 headers, which declare
mkdtemp.
Other
- MIT license;
examples/logstat, a Z# tour for F# developers.
Z# 0.6
The compiler in Z#, beside the F# one (the last version with both). Stage 0 for building src/: ./dev fetches zs-0.6-linux-x86_64.tar.gz from here.