sigildocs

Sigil Releases

0.22.13 (2026-10-06)

A bug-fix release. The headline: since 0.22.10, a non-ASCII character in any manifest string (an em-dash in a description:, say) broke sigil deps for every project that read it. That's fixed, along with a build hang, a few native string miscompiles and --dry-run.

Existing projects build without changes. Crash The Stack, Harkfell and Phantom Burn build and pass their own checks on this release.

write and read agree on non-ASCII text

write escaped each byte of a non-ASCII character with no closing ;, so read couldn't read its own output back. That's what broke manifests. write now prints printable non-ASCII text as itself ((write "café — λ") prints "café — λ"), escapes control characters as valid \x<hex>; escapes, and everything it prints reads back unchanged.

A lock file written by 0.22.13 with non-ASCII text in it won't pass 0.22.12's identity check, so upgrade every machine that shares one.

Fixes

  • sigil build could hang forever when a compiler or linker wrote a lot of warnings to stderr. Builds now read both output streams together.
  • Native and WebAssembly builds could compile some string constants wrong: "a\x0;12" came out as "a\n", and "\x1B;7" didn't compile. String constants now match the bytecode VM byte for byte.
  • sigil build --dry-run actually built everything. It now only reports what each step would do.
  • The store couldn't detect a changed item on macOS and BSD (it relied on GNU find), so a modified item could be served as verified.
  • --no-bundle native builds left out native package initializers, so for example sha256 from (sigil crypto) was unbound.
  • A --no-bundle web build of an app with a native dependency that has no WebAssembly code (sigil-audio, for one) failed with "Failed to link dev runtime". Web builds now link only what was built for them.
  • Managed Zig picked up C++ include paths when compiling C, which broke builds inside Guix and Nix shells.
  • Web builds could write invalid JSON or HTML for names containing control characters or quotes.
  • do now accepts a variable with no step expression, as R7RS allows.
  • The bootstrap CLI's sigil --version printed garbage.

Known issues

  • syntax-rules doesn't match nested ellipses yet: a pattern such as (_ (name value ...) ...) never matches.

0.22.12 (2026-10-01)

A correctness release. Sigil ignored a program's own binding of a builtin's name, such as a local not or an internal (define (car x) ...), on every backend, and several older bugs mixed up local names with macro keywords. Both are fixed. Release configs also compile optimized C by default now.

Existing projects build without changes. Code that doesn't rebind a builtin compiles exactly as before: the release and web builds of Crash The Stack, Harkfell and Phantom Burn are byte-identical to 0.22.11's.

A binding of a builtin's name is used, not the builtin

Sigil compiled a call to car, not, +, list, raise, call/cc and about sixty other builtins straight to the builtin whenever the operator was spelled that way, whatever the program had bound to the name. So (let ((not (lambda (x) 42))) (not #f)) gave #t, an internal (define (car x) 99) was ignored, and ((lambda (car) (car 5)) (lambda (x) 77)) raised car: expected pair. The same happened with a library's own definitions, with names imported from a library other than (sigil core), and in the macro expander, where a parameter named like a macro was expanded as the macro: (define (g twice) (twice 3)) expanded a twice macro instead of calling its argument. The bytecode VM, native builds and WebAssembly all gave the same wrong answers. Now a binding the program introduces wins, apart from two cases involving a macro defined inside a procedure body (see Known issues), and code that doesn't rebind a builtin compiles exactly as before: every compiled module of the standard library and the CLI is byte-identical to 0.22.11's apart from the two below.

If your code relied on the old behavior, SIGIL_DEBUG_PRIMITIVE_SHADOW=1 prints each call to a builtin's name that now goes to your own binding, with the reason. Two places in Sigil itself did: the MCP server's project overview used its own keyword? from before keywords were their own type, so it never showed a project's name, description, dependencies or packages (it does now), and (sigil test) had its own list? that never ran.

Local bindings named like macro keywords

Three older bugs in the same area are fixed. A macro whose expansion uses when failed with syntax-rules: no matching pattern when the caller had a local named when, as in (let ((when 1)) (m)): syntax-rules now ignores the keyword position of a pattern, as R7RS says. After (let () (define foo ...) ...), a later foo in the same procedure read the let's variable and failed with call: not a procedure; it now refers to whatever foo means outside the let. And with-exception-handler passed as a value (through apply, or bound to another name) now installs a handler that raise and raise-continuable reach, exactly like a direct call.

Release builds compile optimized C by default

A config that leaves out optimize: now builds at level 2 when it is a release config: one named release, release-static, prerelease, prerelease-static or web, or one that says debug?: #f. Before, it compiled its C without -O and skipped the optimizer: a web app's runtime came out 6.6 MB and about four times slower than with optimize: 2. Any other config that leaves it out still builds at 0, and an explicit optimize: always wins. Every release config in the projects we checked already declares optimize:, so their builds don't change.

write prints compiled procedures as #<procedure>

0.22.11 made format print a compiled procedure as #<procedure>; write and display still printed #<object 0x...>. All three agree now. To tell compiled code from bytecode, use the new (procedure-native? p): #t for a procedure compiled by the native backend or a builtin written in C, #f for a bytecode closure or anything that isn't a procedure.

sgx appears on the first upgrade

Upgrading from 0.22.10 or earlier no longer needs a second sigil cli use to create sgx: the first command the new version runs writes $SIGIL_HOME/bin/sgx (and bin/sigil, if that is missing) and says so once on stderr.

Fixes

  • Importing a macro through only, rename or prefix, or through a renamed re-export, left it unbound under its new name: after (import (rename (only (demo source) allowed) (allowed selected))), (selected ...) failed with unbound variable 'selected'. Imported macros now follow the same selectors as values.

Known issues

  • A macro that defines a builtin's name inside a top-level (let () ...) and calls it still gets the builtin, and the definition leaks into the module (0.22.11 too).
  • In a built app (sigil build, native or WebAssembly), a macro defined inside a procedure body whose template names one of that body's own defines fails when it runs, whatever the name: macro template refers to local 'helper', which a compiled app can't capture yet (a macro defined inside a body). 0.22.11 failed the same way, as unbound variable 'str'. sigil run, eval and the REPL call the local.
  • A macro defined inside a procedure body whose template names a let-bound variable or a parameter with a builtin's name still gets the builtin: in (let ((car (lambda (x) 'mine))) (define-syntax m (syntax-rules () ((_ x) (car x)))) (m '(1 2))), (m '(1 2)) gives 1.
  • A body holds at most 64 local names for the purpose of telling a local from a macro: the 65th and later parameter or definition named like a macro is still expanded as the macro.
  • (do ((x 1) (i 0 (+ i 1))) ...), a do variable with no step, fails with syntax-rules: no matching pattern.

0.22.11 (2026-09-29)

A release in two parts. sigil task gives a project and its dependencies one way to share scripts, such as a game engine's publish tools. And a long list of memory-safety and correctness fixes, found by reading and testing the runtime closely: several were silent wrong answers or memory corruption that had been there for a long time.

Existing projects build without changes. Run sigil deps install once in each project after upgrading, so it writes the new dependency index.

sigil task runs a project's scripts and its dependencies'

sigil task <name> [args...] runs one of the project's tasks: a step task (a tasks: build pipeline, which takes no arguments) or a script task, a Sigil shell script that receives every argument after its name unchanged. A package declares script tasks as quoted data under provides::

provides: (list
  script-tasks: '((publish-web file: "tasks/publish-web.sgl"
                               description: "Publish a checked site tree")))

A dependency's script tasks run only with its package prefix, as in sigil task substratic:publish-web, and only for dependencies listed in dependencies: or dev-dependencies:. A bare name is always the project's own task, so a dependency can't stand in for one, and sigil build, sigil test and sigil deps install never run a script task. Before a script task runs, one line on stderr says where it comes from, including the locked commit. sigil task --list lists every task.

sgx is sigil task under a short name: sigil cli install, use and upgrade write $SIGIL_HOME/bin/sgx, and sigil completions bash|zsh prints a completion script for both. sigil run -t <task> still works, prints Deprecated: use 'sigil task' instead, and goes away before 1.0.

Upgrading from 0.22.10 or earlier, the older CLI does the switch and doesn't know about sgx, so run sigil cli use 0.22.11 once more after upgrading to create it.

sigil run passes your arguments through

Everything after sigil run's own options now goes to the program unchanged: sigil run app --dry-run -t x passes app --dry-run -t x instead of running task x. The program reads them with (command-line), as a built binary does.

Scripts know their project and can keep secrets from child processes

The shell prelude gains (project-root), the root of the project the script was started in. $, $?, the captures, spawn and pipe stages accept env-unset: ("NAME" ...) to keep a variable such as a token out of a child's environment.

Commands start faster, and installed dependencies can be checked

sigil deps install writes .sigil/deps.index, so commands no longer read every dependency's package.sgl each time they start: an empty script starts about 100 ms faster in a project with many dependencies. sigil deps verify re-hashes each installed dependency and reports anything changed since it was installed. Native builds no longer write into a dependency's shared copy; the generated version_info.c goes to the build's own output. A package.sgl whose sigil: floor is newer than the running Sigil is refused before anything in it runs: this package needs sigil ≥ X; this is Y.

Large numbers, web builds and macros no longer corrupt memory

  • number->string on a large integer in a radix below 10 wrote past the end of its buffer, and string->number with a radix above 16 skipped the digits g to z and could abort.
  • On the web, bytevector-copy! and other range checks could wrap around and write out of bounds, and make-vector or make-bytevector with a huge length quietly made a tiny one. They raise an error now.
  • syntax->datum could lose objects to the garbage collector under memory pressure, so macro-heavy code could read freed memory.
  • The garbage collector didn't trace the recent-calls ring behind stack traces, so capturing a stack trace after a collection could copy freed memory into the trace and corrupt the heap later, typically as a crash at exit. The profiler's tables had the same gap under --profile.
  • A path containing a NUL character was cut at the NUL, so the file checked and the file opened could differ. It raises an error now.

let no longer mistakes its body for a named let

A let whose body starts with a list of lists, such as (let ((k 5)) ((f k)) 0), was read as a named let and referred to a global k instead of the binding, failing with unbound variable 'k'. It affected every backend.

Errors read the same on every backend

Generic runtime errors now have the kind runtime-error everywhere (the bytecode VM used vm-error), and index errors say "index out of range". Code that catches vm-error by name should catch runtime-error instead.

Fixes

  • length of an improper list returned -1 in compiled code, and length or apply on a circular list aborted or hung. Both raise a catchable error now, and list? of a circular list is #f.
  • apply accepts continuations and parameter objects, as a direct call does.
  • Re-entering a multi-shot call/cc continuation could stop the bytecode VM with a GET_CELL error, and compile-file on a file with several libraries folded them into the first.
  • (format "~a" proc) printed compiled procedures as #<object>; it prints #<procedure>.
  • format, println and str printed integers of 2^31 and above wrong on the web (4294967297 came out as 1, 4294967295 as -1), and wrote a non-ASCII character as one broken byte on every platform: (format "~a" (integer->char 955)) now prints λ.
  • Web apps print non-ASCII text correctly (it came out as replacement characters), keep stdout and stderr in order, and no longer slow down on long output lines.
  • sigil_make_bytevector now returns zeroed memory, so C code that builds a bytevector, including an embedder's natives, can't expose old heap contents.
  • string-repeat could crash in desktop builds, and a corrupt .sgb file could corrupt memory as it loaded. make-srcloc and make-generated-srcloc raise for positions past 2^31-1.
  • Compiling a library in a process that already had a library of the same name loaded bound its exports in a different order, so the same source could compile to different bytecode. Compiled output no longer depends on what else is loaded.

Desktop apps can compile their dependencies natively (opt-in)

Set SIGIL_NATIVE_DEPS=all when you build, and a desktop app's release build compiles the dependency modules it imports to native code, not just its own modules; the rest stays bytecode. Calls into libraries such as sigil-geometry run 1.5 to 2 times faster and stop allocating per argument. It is off by default in this release while games try it. Binaries grow with the code they carry: 11% for Crash The Stack, 31% for Phantom Burn and 36% for Harkfell.

To try it, raise sigil: to 0.22.11, run sigil deps update --upgrade-sigil, and build with SIGIL_NATIVE_DEPS=all. A native app links the runtime its lock names, so the lock must carry 0.22.11's: it fixes a garbage-collector bug that native dependency code exposed.

Known issues

  • Quoted list constants in compiled code are rebuilt each time they're evaluated, so two evaluations of one quoted list aren't eq? as they are on the bytecode VM.
  • Dependency copies are not yet read-only: older Sigil versions still write into them, so locking them down would break builds with an older CLI.
  • (apply apply k args) with a bytecode continuation k doesn't work; (apply k args) does.
  • write still prints a compiled procedure as #<object …> while format "~a" now prints #<procedure>.

0.22.10 (2026-09-28)

A correctness and build-cache release. Optimized builds could compute wrong answers in two patterns, compiled code skipped argument-count checks, and Sigil's caches could run the wrong compiled code, ship stale native code, or throw away work they had already done. All of that is fixed, and an unchanged rebuild of a program now recompiles almost nothing.

Relock projects that make native or web builds. Code compiled by 0.22.10 needs the 0.22.10 runtime, and linking it against an older one fails with undefined reference to 'sigil_native_requires_sigil_lib_0_22_10_checked_calls'. Run sigil deps update --upgrade-sigil and rebuild. It moves the project's Sigil packages to the newest version its sigil: field allows, and it also re-resolves every other dependency, so expect those to move too. If sigil: pins a version below 0.22.10, raise it first.

Scripts, tests and the REPL could run the wrong compiled library

sigil <file>, sigil eval, sigil test and the REPL cache compiled libraries. That cache found a library by its name and trusted its timestamp, so it could hand back the wrong compiled copy: two projects that lock different versions of one library ran each other's code, and so did a scratch copy with a newer timestamp, an in-place edit that kept the timestamp, or a change to SIGIL_FEATURES. Every compiled module is now stored under what actually shaped it: its source, the compiler, the settings that change output, its dependencies, its path and the host. The new cache lives in $XDG_CACHE_HOME/sigil/compiled/; the old $XDG_CACHE_HOME/sigil/lib/<commit>+sgbN directories are no longer used and can be deleted.

Scripts and tests also follow the project's redirects now: sigil <file>, eval and test take a redirected dependency's sources from the redirect target, and sigil --redirects FILE ... sets redirects for one command.

Optimized builds computed wrong answers in two patterns

Release and web builds use the optimizer (O1 and O2), and so does bytecode compiled at those levels. Two of its passes could treat a value that came from a conditional as if it always came from one branch:

  • Calling a procedure chosen by a conditional could run the wrong one:
  (define (caller choose replacement)
    (let ((target (if choose (lambda (n) (+ n 1)) replacement)))
      (+ (target 7) 1)))

With choose false and a replacement that returns 41, this returned 9 instead of 42.

  • A value chosen by a conditional could be mistaken for one of the branches. With selected set to either an empty list or (cons 1 '()), (null? selected) answered #t for the one-element list.

Both returned the wrong value silently, with no error. The optimizer now checks that a value has a single source before it relies on one. The fixes change no other generated code: the standard library and the packages compile to identical C, and so do both Flux Harmonic games for the second fix.

Builds reuse more and never ship stale code

  • An unchanged rebuild recompiles almost nothing. A package with a custom build step (C sources, an archive, generated version info), or with a source the build can't model (an include, a library condition in cond-expand), used to switch caching off for every module in the package. Now only the files that need it compile fresh. Crash The Stack's unchanged rebuild went from 170 recompiled modules to 3 in its dev configuration, and from 167 to 19 for the web.
  • A build could ship old native code. After an edit that kept the file's size and timestamp, sigil build --no-cache, and a build whose cache failed to start, reused the old compiled object and printed the old behavior. Native objects are now reused only when their inputs match by content.
  • Macros that read files are handled safely. A module whose macros read a file, the environment, the command line or a process while expanding is no longer cached at all: it compiles fresh every build, so a data-file change always reaches the output. --verbose prints an UNCACHED line for it.
  • The web build's link, asyncify and wasm-opt steps are cached across builds, reused when the tool, its arguments, its environment and its inputs all match by content.
  • Package manifests are always evaluated in the top-level environment. On a release sigil, a manifest loaded by a build step was evaluated inside the compiler's own module, which is why release builds of apps that build sigil-lib stopped caching after sigil-lib.
  • A build step that defines or changes a library module in the build process (for example with eval-string) turns caching off for the rest of that build, and --verbose says so: NO-CACHE a task step registered or redefined <module>.
  • A REPL or sigil build --watch session that outlives a rebuild of Sigil itself stops caching, instead of filing new entries under the new compiler.

Fixes

  • Compiled code now checks argument counts. Calling a procedure with too few arguments read garbage in native code and an undefined or stale value on the web, and extra arguments were silently dropped. Both now raise arity-error with the bytecode VM's message, for example "<lambda>: expected 2 argument(s), got 1". Code that caught the old runtime-error from the native bridge should catch arity-error instead. Calling a delimited continuation with the wrong number of values from compiled code now raises too, as on the bytecode VM.
  • apply in tail position is now a real tail call in native builds. A loop that recursed through apply a million times crashed with a segmentation fault; it now runs in constant stack, as on the bytecode VM.

Known issues

  • A build step that re-defines or set!s an existing binding in a library module isn't detected by the new step check.
  • A module that imports (sigil user), or whose macros look up modules outside their own imports, is never cached.
  • Web builds of Crash The Stack still recompile case.sgl, regex.sgl and sigil/audio/sink.sgl on every build (as on 0.22.9).
  • (list? x) on a circular list never returns.
  • The bytecode VM can stop with "GET_CELL: expected upvalue cell" when a multi-shot call/cc continuation is re-entered, and with "SET_CELL: expected upvalue cell" on some module bindings.
  • Release builds of desktop apps still run the standard library and dependency packages as bytecode; only the app's own modules are native.

0.22.9 (2026-09-26)

A size release. Web builds of native-compiled apps are about 70% smaller, and call-heavy native code runs faster, because the compiler no longer spends most of each function's code on bookkeeping. Measured on Crash The Stack, Harkfell and Phantom Burn at the same commits.

Existing projects build without changes. Rebuild to pick up the smaller output.

Web builds are about 70% smaller

A game's wasm is almost all code the compiler emits for its Sigil modules, and two thirds of that was per-function boilerplate: every local variable registered with the garbage collector by its own inlined call, and every call followed by its own copies of the error and exception exits. Locals are now kept in one array that is registered once, and a function's exits share one label.

Game0.22.80.22.9Change
Crash The Stack44.8 MB12.0 MB-73%
Harkfell31.7 MB9.0 MB-72%
Phantom Burn11.4 MB3.7 MB-68%

Gzipped downloads shrink by 35 to 42%. Crash The Stack and Harkfell were over Cloudflare Pages' 25 MiB file limit and are now well under it.

Native code calls faster

Native desktop builds use the same compiler, so a function now makes room for its garbage collector roots once instead of once per local. A call-heavy benchmark (fib, tak) runs in about half the time. In the browser, Crash The Stack's game tick went from 11.7 ms to 9.2 ms a frame, and its whole frame callback from 14.4 ms to 10.6 ms.

Fixes

  • (remainder 7.0 2) returned an undefined value in release and web builds, for every inexact argument. It now returns 1.0, and a zero divisor raises an error, as on the bytecode VM.
  • apply with an improper list as its last argument silently dropped the tail in native code: (apply f 1 '(2 . 3)) called f with (1 2). It now raises the same error as the bytecode VM.
  • integer? was wrong on both backends: (integer? 1.0) was #f on the bytecode VM, and (integer? 3.5) and (integer? +inf.0) were #t in native code. It now follows R7RS everywhere: exact integers and finite integral floats.
  • A -0.0 literal lost its sign in release and web builds, and prints as -0 on every backend now. +inf.0, -inf.0 and NaN literals are also emitted exactly.
  • Forcing a promise whose body yielded inside with-async returned an undefined value on the bytecode VM and cached it, so every later force was wrong too. It now returns the body's value, as native code already did.
  • Release and web builds and the bytecode VM now agree on all of the above, checked by a new test that runs the same cases on both backends.

Known issues

  • Release builds of desktop apps still run the standard library and dependency packages as bytecode; only the app's own modules are native. A proper fix is coming.
  • In web builds, forcing a promise whose body yields inside with-async stops the program, as it did in 0.22.8. The bytecode VM fix above doesn't cover the web path yet.

0.22.8 (2026-09-25)

A correctness release. Release and web builds could drop the guard of a small helper procedure, comparisons with NaN answered true, and sigil mcp serve showed no tools in new Claude Code sessions. It also fixes a web app that doesn't import (sigil core), a from-path dependency that was silently left out, macros that allocated on every use in native code, and modules reused after SIGIL_FEATURES changed. More of R7RS now works without an import.

Existing projects build without changes. Rebuild your release and web builds to pick up the first fix.

Release and web builds keep a helper's guard

Since 0.16.1, release and web builds replaced a small helper with a fast path when its body could end in a struct field read, an index calculation (+ x (* y (cols g))) or a vector lookup (vector-ref (items s) i), even when the helper did something else first. (if (< i 0) 'none (vector-ref (items s) i)) crashed on the guarded branch, a guarded index helper returned the unguarded sum, and a counter bumped before the lookup never moved. Dev builds were right, so tests passed. The fast path now applies only when the whole helper is that one expression.

Comparisons with NaN are false

(<= nan 1.0), (>= nan 1.0) and (= nan nan) answered #t, so a tolerance check like (<= (abs (- a b)) eps) let a NaN through, and zero? said #t for NaN. Every comparison with NaN is now false, on every backend, and min and max return NaN when an argument is NaN.

Sigil's MCP tools appear again in new Claude Code sessions

Since 2026-09-25, Claude Code 2.1.282 negotiates MCP protocol revision 2026-07-28 with any server that offers it, and sigil mcp serve offered it without answering in that revision's shape. Claude Code checks that a tool listing says how long it may be cached (ttlMs and cacheScope). The listing from sigil mcp serve had neither, so the client retried a few times and then registered no tools at all, with no error shown. This release takes sigil-mcp 0.18.1, which adds both fields whenever the client speaks 2026-07-28, and older clients get exactly the answers they got before. Sessions that are already running keep the server they started with, so restart them after upgrading.

A web app that doesn't import (sigil core) starts

An app whose modules never import (sigil core) died in the browser before running anything, with unbound variable '%set-spec!'. The core library is now always set up first.

A from-path dependency is installed and built

A project's (from-path dir: "../lib") without package: was left out: sigil deps install didn't link it and sigil build finished without it, with only a "failed to load import" warning. It's now linked and built like any other dependency, and one that doesn't resolve stops both commands with its directory named. When --redirects points a dependency elsewhere than its .sigil/deps link, the build says which tree it used.

Macros cost nothing at run time in native code

A syntax-rules macro whose expansion calls a procedure or reads a variable of the macro's own library looked the name up, and allocated, on every use in release and web builds: 27 allocations for one vector operation in sigil-geometry. Each use now finds the binding once, and still sees it change after a set!.

Modules are recompiled when SIGIL_FEATURES changes

sigil eval, sigil run and sigil test keep compiled copies of the modules they build from source, and reused a copy compiled without a feature when that feature was set, so a cond-expand kept its old branch. Each feature set now has its own copies.

More of R7RS works without an import

A library that imports only (sigil core), and every test file, now has these (scheme base) procedures:

  • the math procedures floor, ceiling, round, truncate, max, min, quotient, gcd, lcm, expt, square, exact, inexact, exact?, inexact?, exact-integer? and the floor/ and truncate/ families, which needed (sigil math);
  • string<?, string>?, string<=? and string>=?, which needed (sigil string) and now take more than two strings;
  • list-tail, list-set!, list-copy, vector-append, boolean=?, symbol=?, exact-integer-sqrt, input-port-open?, output-port-open?, rational?, real?, complex?, numerator, denominator, rationalize, exact->inexact and inexact->exact, which didn't exist. Sigil has no exact fractions, so numerator, denominator and rationalize return floats for a fractional argument, and rationalize computes in floating point.

(sigil math) and (sigil string) still export the names they had. A library or program that defines one of these names itself keeps its own definition. abs, modulo, remainder and call/cc can now be passed as values: (map abs xs) raised "unbound variable". Importing (sigil string) no longer swaps string-upcase and string-downcase for its own ASCII-only versions; they are core's.

The R7RS compatibility page now says which module each name is in and lists the gaps that remain.

Smaller fixes

  • sigil cli use v0.22.7 works; the leading v of a tag name is accepted.
  • sigil deps install --help lists the options, including --redirects and --upgrade-sigil.
  • string-index and string-index-right take a character as well as a predicate: (string-index "a,b" #\,) is 1.
  • sigil test shows each file's time in its per-file line, and --report lists every file with its outcome, counts and time in milliseconds.
  • A module can export 1024 names, up from 512. The web bridge packages (browser, audio, WebGL, DOM, canvas, network) add their natives to (sigil core), and a web app that linked several of them could run core out of room. A module past the limit now reports each name it can't export instead of dropping it silently.

Known issues

Release builds of desktop apps run the standard library and dependency packages as bytecode; only the app's own modules are native. Web builds run everything native. SIGIL_NATIVE_DEPS=1 makes a release build compile the standard library natively, at the cost of a much larger binary, since every standard library module is included. A proper fix is coming.

string-upcase and string-downcase convert non-ASCII letters with the C library's case tables, so their results for non-ASCII text can differ between systems: the Linux release build turns "λ" into "Λ", and a build against glibc may leave it unchanged. Before 0.22.8 a library that imported (sigil string) got ASCII-only versions whenever it passed these procedures as values.

0.22.7 (2026-09-24)

A game audio and correctness release. Sound effects in the browser now play the moment they're triggered, a game's synth can drop to a lower rate on a weak phone, and sigil is ready to take upgrades from the signed registry. Three defects are fixed along the way: an error lost between nested guards, a -L path that stopped sigil starting, and tests run against dependencies the lock doesn't name.

Existing projects build without changes. The audio calls come with sigil-audio 0.13.0, released alongside.

Sound effects play at once in the browser

A sound effect pushed into the music stream was heard one buffer later, up to a quarter second on a phone, and the delay came and went. Load a sound once with load-audio-cue and play it with play-audio-cue: it's mixed into the very next audio block, about 3 ms, however much music is buffered. audio-cue-lag reports the wait for an on-device readout.

A slow synth can render at a lower rate

open-audio-stream takes rate:, and set-audio-stream-rate! changes it mid-stream. The sink upsamples on the audio thread, so a game can render its music at 24000 on a 48000 device without a resampler of its own.

(sigil audio ladder) decides when to step down. It's the policy Crash The Stack tuned on a phone: it watches the steady cost per frame, ignores the moments next to a sound effect or a tune starting, and never steps back up on its own. rate-ladder-report gives a readout line such as 48000/24000 BY LADDER@20.2S.

audio-stream-underruns-sounding counts only the dropouts a player could hear, apart from a stream with nothing to play running dry.

Upgrades verified end to end

sigil can now take upgrades from the registry's signed release list, checking every step itself with no minisign needed: the key list against the root key built into sigil, the release list against a key from that list, and the binary against the hash, size and file name the list gives for your platform. An older list than one sigil has already seen is refused, so a stale copy can't roll you back.

The registry's signed list goes live with its first signing. Until then, and whenever the registry can't be reached, sigil says Registry unavailable, using Codeberg. and upgrades from Codeberg as before. If the registry answers but what it serves fails a check, sigil installs nothing and says why. SIGIL_REGISTRY points it at a mirror.

An error passed on by an inner guard reaches the outer one

0.22.6's known issue is fixed: when one guard sat in tail position of another's body, an error the inner one passed on skipped the outer handler. The workarounds are no longer needed.

One cost: a loop that calls itself from inside a guard body now keeps each iteration's handler until it returns, about 150 bytes each. For a long-running loop, put the recursive call after the guard.

-L can no longer stop sigil starting

Pointing -L at another project's build output could replace the modules sigil itself is built from, and it failed before running anything. Those modules now always come from sigil's own copy. Separately released packages such as (sigil json) can still be replaced with -L.

Build, test and run stop when dependencies don't match the lock

After switching branches, .sigil/deps can still point at the old branch's dependencies, and tests ran against code the lock doesn't name. sigil build, sigil test and sigil run now stop with exit code 78 and name the package and both commits. Run sigil deps install, or set SIGIL_ALLOW_STALE_DEPS=1 to go ahead anyway.

A clean build from source

make no longer prints Error in macro 'let' while bootstrapping, and the core library it builds is now checked to match a normal build.

0.22.6 (2026-09-24)

A reliability release for scripts, plus the groundwork for sigil env. Two defects let a program lose data or an error and still exit 0, and the command line swallowed -L arguments meant for other programs. All three are fixed, and several errors that named nothing now say what went wrong.

Existing projects build without changes. One behaviour change to know about: a guard that used to return #f for an error none of its clauses matched now passes the error on, so a script that seemed to succeed may now report it.

Strings with a NUL byte are written in full

write-file-string, the shell's write-text and display stopped at the first NUL byte and reported success. A script saving a downloaded archive could end up with a 24-byte file and exit 0. Every byte of a string is now written, to files, pipes and ports alike.

guard passes on an error that no clause matches

A guard with two or more clauses returned #f when none of them matched, so the error vanished and the script exited 0. It now propagates, as R7RS specifies.

Known issue: when the inner guard sits in tail position of an outer guard's body, the error it passes on skips the outer handler and reaches the top level. Until it's fixed, call the inner code in non-tail position, for example (let ((r (thunk))) r), or give the inner guard an else clause.

Arguments after -- reach your program

sigil treated -L as its own load-path flag wherever it appeared, even after --, so sigil env shell -- tmux -L work started tmux on the wrong server. Everything after -- now passes through untouched.

Errors that name what went wrong

Struct, record and multimethod errors printed placeholders like ~a: unknown field ~a; they now read like point: unknown field z. And when a module on a -L path stops sigil itself from starting, the error names the module, the file and the -L directory, and says to remove the -L or rebuild what it holds.

sigil env: one-time setup and a read-only store

sigil env setup offers to create the /sigil link that store programs need outside sigil env shell. It always asks first and refuses to run as root. The environment store now lives in ~/.local/share/sigil/store, apart from the build cache, and its items are read-only: sigil env store rm removes them and sigil env verify --full checks every file.

A lock with nothing to build now takes about 2 seconds instead of 12, and shows its progress as it goes. The first public channel follows once its signing is done.

0.22.5 (2026-09-23)

A regular expressions and web reliability release, plus Windows icons. Sigil now has regular expressions in the standard library. Web builds could also fail in ways nothing on the code path explained: a type error from nowhere, or text that parsed fine natively coming back as garbage. Crash The Stack hit both, including a menu that froze on phones.

Existing projects build without changes. A native or wasm app picks up the fixes by rebuilding with this toolchain. (sigil regex) and (sigil unicode case) are new, and a Windows build config gains two optional keys.

Regular expressions: (sigil regex)

Sigil now has regular expressions in the standard library, using the same syntax as RE2 and Go. regex-match?, regex-search, regex-replace-all, regex-split and the rest take either a compiled pattern or a plain string, so a one-off check is a single call.

Matching always runs in linear time, so no pattern can hang your program, even one you didn't write. The trade-off is that a pattern can't refer back to its own groups (backreferences like \1) or use lookaround, and such patterns are rejected with an error pointing at the position. Replacements can still use captured groups: $1 and ${name} work in regex-replace and regex-replace-all.

The module is plain Sigil, so it behaves the same on bytecode, native and WebAssembly. Shell scripts get it without an import:

(unless (regex-match? "^v[0-9]+\\.[0-9]+\\.[0-9]+$" tag)
  (error "not a release tag" tag))

Tail calls keep their arguments alive

A tail call into a built-in procedure could lose its arguments to the garbage collector while the built-in was still using them. Reading from a fresh string port was the usual victim: a large parse failed partway through with a type error or a reader error on text that was fine.

Only web builds showed it, and only when a collection happened to land mid-call, so the same code could work in one build and fail in the next. Rebuilding with 0.22.5 fixes it.

Deep recursion no longer loses values

Recursion more than 1024 native frames deep could leave the innermost frames unprotected from the garbage collector. Values still in use could be freed, and the next operation to touch them failed with an unrelated type error. The limit is gone: the frame stack now grows as needed.

Catching memory errors in web builds

SIGIL_GC_POISON_FREE=1 fills freed memory with a marker pattern, so a use-after-free fails right away instead of at some random later point. The GC's other environment variables, such as SIGIL_GC_STRESS, now reach a wasm module when its host supplies them.

Case mapping that matches on every platform

char-upcase and char-downcase depend on the C library the runtime was linked against, so é could uppercase in one build and not in another. The new (sigil unicode case) carries the Unicode 15 case tables itself and gives the same answer everywhere. Case-insensitive regex matching uses it.

Windows executables get an icon and version details

A Windows config in your project's package file can now set the exe icon for sigil build with icon:, pointing at a .ico file in your project. The exe also gets its Properties → Details tab filled in from your package's version: and author, and the new optional product-name:.

(config
  name: 'windows-amd64
  toolchain: 'zig
  target: "x86_64-windows-gnu"
  backend: 'native
  icon: "windows/app.ico"
  product-name: "My Game")

A missing or invalid icon fails the build with a clear message instead of quietly shipping without one. Configs that don't use these keys build exactly as before.

0.22.4 (2026-09-22)

A web audio release. A page that is not cross-origin isolated no longer mixes its sound on the main thread, so a long game frame no longer pops the audio. Found by Crash The Stack's live soundtrack on a phone over a plain http host.

Existing projects build without changes. The (sigil audio sink) facade is the same; a project picks the new path up by rebuilding with this toolchain.

The worklet drains a page that cannot share memory

The bridge used an AudioWorkletNode only when the page could share memory with it (cross-origin isolated) and fell back to a ScriptProcessorNode on the main thread otherwise, where any frame that ran long starved the ring. A third mode now sits between them: the worklet is fed the frames by message and drains them on the audio thread, with the depth and underrun readings lagging by at most one report. SigilWasmAudio.sinkMode(id) says which path a sink is on (worklet-sab, worklet-msg or scriptprocessor). A browser without a worklet keeps the old path. test/integration/test-wasm-audio-worklet-msg.mjs drives the processor against the real producer and bounds the lag with the main thread stalled.

0.22.3 (2026-09-21)

A native-codegen correctness release. Two defects found by Crash The Stack in its first week of play, both invisible to the bytecode VM and to every in-process test suite: a release binary that could crash at startup depending on where it was built, and a web build that leaked memory on every frame.

Existing projects build without changes. A declaration such as sigil: "0.22" already admits 0.22.3. Nothing in the language or the build surface changed; a project picks the fixes up by rebuilding with this toolchain.

Quoted literals are safe under the collector

A native binary rebuilds every quoted list or vector at module init, and that reconstruction was not GC-safe: a collection landing in the middle of a large literal could leave later elements dangling, and the crash came much later, in the first major collection. Whether it happened depended on the length of the checkout's path, so the same commit could run in one worktree and segfault in another.

Literals are now rooted and barriered while they are rebuilt, on both the native and the wasm targets. test/integration/test-native-literal-gc.sh forces the timing and is red on 0.22.2, green here.

Tail calls with more than eight arguments no longer leak

A tail call between native-codegen procedures with nine or more arguments carried its arguments in a heap buffer that was never freed. A game's draw path can make thousands of such calls a frame: the published web build grew by about 90 KB per frame and hit FATAL: Out of memory after an hour or two on a board, and the native release binary grew from 283 MB to 907 MB in three and a half minutes.

Both trampoline loops now free the buffer once the callee returns. test/integration/test-native-tramp-args.sh bounces five million nine-argument calls and caps the resident set's growth at 8 MB; on 0.22.2 it grows by 390 MB, here by zero.

Reading a crash in a release binary

Release binaries are stripped. To get a symbolized frame, relink the objects under build/release/native/ without -s and re-append the bundle trailer. To catch a missed barrier directly, run with SIGIL_GC_AUDIT_BARRIERS=1 and force the timing with SIGIL_GC_ADAPTIVE_YOUNG=0 SIGIL_YOUNG_THRESHOLD=<n>; SIGIL_GC_STRESS=1 is blind to this class, since its collections are all major.

0.22.2 (2026-09-18)

A patch for the seams a game project hit in its first two days: between the two backends, between a game and the web loader, between what a docstring says and what a procedure does. It also repairs a 0.22.1 regression that stopped script-style source files from building. Everything here came from Crash The Stack's toolchain notes, each item with the measurement that found it.

Existing projects build without changes. A declaration such as sigil: "0.22" already admits 0.22.2. The one behaviour a page might notice: sigil_wasm_dispatch_event now reports a handler's positive integer instead of 0, so a page that treated any non-zero return as failure should treat only a negative one that way.

Script-style source files build again

0.22.1 rejected any source whose declaration forms the bytecode-input identity extractor could not model, and that included a script-style file under src/: a top-level (import ...) followed by expressions and no define-library, loaded by the wasm runtime at startup. sigil build died with unsupported dependency declaration or feature condition on a repository 0.22.0 built.

A script's top-level imports are now modeled declaration positions: they are dependency edges, so the script compiles after the modules it imports, and its cache identity is complete. A form the extractor still cannot model (an import inside a procedure body, an include, a (library ...) feature test) no longer fails the build: the file compiles fresh, the build prints one UNCACHED line naming the form, and the package declines cache reuse until the form is gone. The contract is in the build cache reference.

The two backends answer alike

(map f a b) with two lists raised map: expected 2 argument(s), got 3 in the bytecode VM and ran under native codegen, which silently dropped the extra list; one source tree gave a green browser build and a native game that drew nothing. Core map and for-each now take any number of lists and stop at the shortest, as R7RS says.

A define inside a cond-expand clause in a library or procedure body is now a define of that body on every backend. Before, the bytecode compiler raised GET_CELL: expected upvalue cell at the call and native codegen resolved the name to an immediate, so a module that differed per target had to cond-expand inside every body.

write prints a symbol as |...| when the reader would not read the bare name back as that symbol: |1| (a keyboard's digit key), |a b|, |foo:|. Written data round-trips through read again; display is unchanged.

A conformance fixture now runs a list of stdlib forms through the differential harness on bytecode, native ELF and native wasm and fails on any disagreement; make test-conformance runs the bytecode and native rungs and is part of make test. A second fixture records a known gap for its own ticket: native codegen does not check call arity, so a two-argument procedure called with three runs as if called with two.

The web loader keeps early dispatches and answers questions

A dispatch made from SigilWasmBridges.setInstance, where a page first learns the wasm instance, reached a runtime with no VM and was dropped with a success-shaped return. The loader now publishes globalThis.SigilWebApp: a dispatch made before sigil_wasm_start is queued and delivered, in order, right after it; SigilWebApp.ready resolves then, and document receives a sigil-web-app-ready event for scripts that ran before the loader.

SigilWebApp.dispatch() returns the handler's integer, so a page can ask the app a question through the return value instead of polling localStorage. A pointerdown on a canvas is forwarded as ("pointerdown", "x,y,mods") in canvas-buffer pixels with a modifier bitmask, unless the page's own listener handled it, so a game page no longer re-implements pointer forwarding.

Structs: update through a procedure, compare by fields

(struct-update s field: proc ...) copies a struct with each named field passed through its procedure, replacing the read-apply-rebuild idiom (session s trace: (trace-hint (session-trace s))). (struct-equal? a b) compares two instances of one type field by field. equal? on instances is still identity in this release.

One package, native and web configs

Three sigil-build gaps met by a package that builds both a native binary and a browser application:

  • A dev-dependency is built only when tests are built (--tests, sigil test). A plain sigil build --config web used to compile the test runner's whole closure for wasm and fail in mbedtls two and a half minutes in.
  • A task's configs: gate is honoured. build can be declared once for the wasm config, linking a web application, while the native configs keep the default task.
  • A dependency library's native-init: is called in a wasm build: the build generates a weak sigil_wasm_app_register that calls each one, so no hand-written app-register.c is needed, and a package that still ships its own keeps winning.

Docstrings say what raises

Procedures in (sigil fs) and (sigil io) that can raise carry a raises: line, and those that answer #f say so; the browser storage facades document that they never raise and what a missing value looks like. read-file-bytes takes missing: like read-file-string, and with-current-directory raises when it cannot enter the directory instead of running its thunk in the old one.

0.22.1 (2026-09-16)

A cache-correctness and test-reliability release. The package cache now proves what it reuses: it hashes the bytes of every input and output it depends on, binds the compiler's own identity into the cache key, and declines to reuse anything it cannot fully account for. The test runner stops counting other checkouts' tests as your own, and its file summary can finally report a shortfall. An opt-in evidence journal records what a test run actually loaded and executed, for qualifying a release rather than trusting a green suite.

Existing projects build without changes. No dependency-wide version bumps are required; a declaration such as sigil: "0.22" already admits 0.22.1. The first build after upgrading rebuilds packages whose manifests predate the new schema, and later builds reuse as before. No store reset is needed.

The package cache proves what it reuses

Package reuse used to trust a manifest whose inputs matched by size and modification time. Sigil now hashes the bytes of every input and every required output before reusing a package, so a same-size edit with a restored timestamp, a broken link, or a missing documentation sidecar rebuilds instead of restoring a stale result. Older manifests miss safely and are replaced after the next complete build.

The compiler's own identity is part of the cache key too: the executable's bytes and its startup libraries, not the version it reports. A rebuilt toolchain that still says 0.22.0 no longer reuses bytecode from the previous one. Full contract in the build cache reference.

Test discovery stays inside your checkout

In a repository without a test/ directory, discovery walks the whole tree and used to descend into other checkouts underneath it. One repository counted 117 of 319 per-file results from .worktrees/ and .claude/worktrees/, including a test that passed in the real tree and failed in a stale copy. Discovery now stops at any directory containing a .git and prints a SKIP line saying so.

The Files: summary line could not report a shortfall: a run in which nothing executed still printed Files: 4 of 4 run. It now reads X of Y loaded, so 0 of 1 loaded, 1 failed to load beside Tests: 1 passed tells you exactly what happened.

Opt-in test execution evidence

Set SIGIL_TEST_EVIDENCE to a new JSONL path and SIGIL_TEST_RUN_ID to a run token before starting Sigil, and the runtime journals every input buffer it acquired (with its SHA-256), every module initialization, and the runner's selection and completion records. scripts/observe-test-evidence.py runs one sigil test --sgl invocation under an external observer and reconciles the journal against the expected files and module bytes; it exits 2 for incomplete evidence, and a missing or truncated journal never qualifies as a pass. Inconsistent evidence is rejected, partial-load fail-fast is honored, and large journals stream during validation. This is POSIX-only and experimental; Windows and browser builds refuse the opt-in with exit 74. See runtime evidence and test execution evidence.

Runtime and process fixes

Async readers and writers are preserved when the opposite direction becomes ready on the same socket or descriptor, so duplex connections no longer freeze under backpressure; the visible symptom was terminal scrollback replay stalling on reconnect. Native closures stay rooted across allocating metadata initialization, closing a collection window that WebAssembly builds could hit. PTY children on macOS and BSD now receive the requested environment, so a TERM override reaches the shell as it already did on Linux. The build store retries a content lock that its previous owner releases mid-contention instead of reporting an unwritable store. Compiler summary export lookups are indexed, development CLI entrypoints read startup library roots the same way the bundled CLI does, and the release Scheme observation timeout is configurable.

0.22.0 (2026-09-07)

A scripting and reliability release. Sigil is more convenient for operational scripts, the bundled client supports TLS 1.3, and builds and sockets recover more reliably from cache and resource problems. Direct file execution now provides the scripting prelude automatically; ordinary evaluation remains available through sigil eval -f.

No dependency-wide version bumps are required. A declaration such as sigil: "0.21" already admits 0.22.0, but sigil deps install preserves a valid locked runtime. Upgrading the CLI does not rewrite project lockfiles. Projects that start using the new runtime or standard-library APIs should raise their minimum to sigil: "0.22" and run sigil deps install. To refresh dependencies and explicitly move the runtime within the existing compatibility window, use sigil deps update --upgrade-sigil; this also updates ordinary dependencies.

TLS 1.3 and connection diagnostics

The bundled TLS client now supports TLS 1.3 alongside TLS 1.2, using Mbed TLS 3.6.7. Certificate and hostname verification remain enabled. New tls-connect/details and tls-upgrade/details procedures expose per-attempt handshake failures and negotiated protocol details; existing connection APIs keep their behavior.

A scripting prelude and reliable command capture

sigil script.sgl now loads (sigil shell prelude), including short text/path names, positional arguments, iteration, polling, and temporary-directory scopes. sigil eval -f file.sgl keeps ordinary explicit imports; sigil eval --quiet suppresses implicit expression results. See the shell scripting guide.

Unix command capture drains both output streams while feeding stdin, fixing stalls when output fills a pipe. Background commands can write directly to log files and explicitly detach from the launching script. Unsupported options now fail before spawning. File/directory scopes clean up when their body raises, and printing to a closed port raises instead of crashing.

Shebang scripts work with formatter verification and strict test classification. Reader errors retain the source filename. Concurrent auto-compilation uses unique staging files, and failed cache refreshes stop execution instead of loading stale bytecode.

Script and eval errors now show concise diagnostics on stderr, with expanded call history available through --error-history. The guide includes a complete HTTP harness with child cleanup and retained failure evidence, and explains how polling deadlines interact with blocking readiness probes.

Shared text and iteration helpers

(sigil string) adds string-truncate, with a configurable truncation marker, and string-contains-any? for literal alternatives. (sigil iteration) adds each for lists and arrays, and (sigil time) adds wait-until for readiness polling. These helpers are also available in the scripting prelude.

read-file-string, also exposed as read-text in scripts, reads complete UTF-8 files without truncating multibyte text. Its new missing: and trim: options cover optional state files and whitespace trimming without hiding permission or other read errors.

Cached builds retain API documentation

Restoring compiled modules from the build store now restores their documentation metadata too, keeping help, editor navigation, and MCP documentation available after a cache hit. When a second checkout reuses that cache, source locations are updated to point into the current checkout. Missing or unusable metadata causes recompilation instead of leaving an incomplete cache result in place.

Socket polling under descriptor exhaustion

On systems using epoll or kqueue, socket polling falls back to poll when the process or system cannot allocate another file descriptor. Applications can continue draining and closing existing sockets, including high-numbered descriptors, so they can recover from that condition.

The async scheduler rounds future fractional-millisecond deadlines up when polling, avoiding a busy loop while waiting for the timer to expire.

WebSocket event notifications for browser hosts

The WebAssembly network bridge accepts an optional onEvent(socketId, eventType) callback when creating imports or opening a WebSocket directly. Hosts can wake their event loop when a connection, message, error, or close event is queued. The existing event queue remains available, and a callback exception does not discard the event.

Updated bundled libraries

The CLI now bundles HTTP 0.18.7, log 0.17.0, nREPL 0.17.2, MCP 0.18.0, SQLite 0.16.3, and LSP 0.16.3 alongside crypto 0.16.6 and TLS 0.16.7. The MCP transition pin is replaced by its published release. The refreshed dependency graph no longer locks the retired standalone socket, docs, and ANSI providers.

Text log fields now quote and escape unsafe values; JSON log output is unchanged. Requesting dependency-subcommand help no longer starts an install or upgrade. The updated HTTP library also includes byte-preserving uploads, streamed responses, and multipart file parsing.

0.21.0 (2026-09-02)

A test-reporting honesty release, and the first one that can turn a previously green suite red. sigil test could report success while running fewer tests than it appeared to, or none at all, and nothing in its output said so. Both cases now fail the run.

It also adds (sigil grant), a standard library type for capability grants and the algebra for narrowing them. Nothing enforces a grant yet; this is the shared vocabulary, not a sandbox.

This is a breaking change. If your test/ directory holds a .sgl file that registers tests and is not named test-*.sgl or *-test.sgl, sigil test now exits 1 and names it. That file has never run. See "If this release turns your build red" below.

It reaches you without any change on your part. The sigil: field is a floor with an implicit 1.0.0 ceiling, so a project declaring 0.19, 0.20, 0.20.3 or ^0.20 admits 0.21.0 and picks it up on its next sigil deps install. There is no pre-1.0 version you can be pinned to that holds this back. The minor bump is a signal, not a barrier.

sigil test always reports how many files it ran

The summary's Files: line used to print only when discovery selected at least one file. A package whose tests are a native binary could lose its entire Sigil suite to a rename and still print Tests: 0 passed (0 total) under a green All tests passed!, with no Files: line at all, and exit 0. Zero tests run and every test passing looked identical. The line now always prints, including Files: 0 of 0 run.

A run that selects no Sigil test files fails

If discovery selects nothing while a .sgl file sits in test/ or tests/, sigil test exits 1 and names the files it skipped. A package with genuinely no Sigil tests has no .sgl under test/ and is unaffected.

Strict discovery is on by default

Discovery selects test-*.sgl and *-test.sgl only, so a file named anything else sits beside the suite and never runs. The summary cannot show this: a file that never matched is upstream of every count it reports, which is how a repository can publish Files: 5 of 5 run with a sixth file of real tests unrun beside the five.

sigil test now fails when a .sgl file that is a direct child of a test directory imports (sigil test), registers at least one test, and was not selected. It reads the file rather than running it, so a file that starts a server or calls exit is examined safely, and a file that mentions (sigil test) only in its comments is not flagged.

Files in subdirectories of test/ are never considered, because that is where fixtures, integration drivers and helper modules live. Across 175 checkouts holding 5193 test .sgl files, 1850 are not selected by discovery and 1647 of those are nested. The rule flags 28 files in four projects, and every one is a real test file.

If this release turns your build red

The message names the file:

✗ STRICT: 1 .sgl file(s) in a test directory register tests and were
  not selected by discovery. They have never run.
    - ./test/parser-regression.sgl

That file has never run, so the question the red is asking you is whether it should have. Both answers are a one line change.

If it should be running, rename it to test-parser-regression.sgl (or parser-regression-test.sgl). It joins the suite on the next run, and you get back however many tests it holds.

If it should not, because it is a live gate or a driver invoked by its own script, add it to .testignore in your project root. Globs work, so one line covers a family:

test/parser-regression.sgl
packages/myapp/test/*-live.sgl

.testignore is the same file discovery already honours, not a new mechanism.

If you cannot deal with it now, sigil test --no-strict restores the previous behaviour and you can come back to it. It is worth knowing that this is the larger of the changes available to you: it means editing your CI config, where the two fixes above are a rename or a single line, and it leaves the file unrun in the meantime.

(sigil grant): a shared vocabulary for capability grants

A new standard library module holding an immutable capability grant and the algebra for comparing and narrowing one. A grant is a set of facets, each naming a kind (fs, exec, net, import, eval, ffi, pty), a mode, and a resource pattern. A facet is a permission, and absence is denial:

(import (sigil grant))

(define g (grant fs: '(rw "/work") exec: '(allowlist "git" "sigil")))
(grant-covers? g (grant-from-specs '("fs:ro:/work/src")))   ; => #t
(grant-covers? g (grant-from-specs '("fs:rw:/etc")))        ; => #f

grant-covers? is the partial order, "has at least as much authority as". attenuate narrows. empty-grant permits nothing and grant-allow-all permits everything. grant-facets returns the facets for a consent prompt or an audit line, and grant=? compares authority rather than spelling, so two grants written differently that permit the same things compare equal.

Several tools in the Sigil ecosystem had each grown a grant table of their own, with their own idea of what a filesystem root or an allowed command meant. This is the type they can share, so a narrowing computed in one place means the same thing in another.

Narrowing composes, and cannot widen

(grant-covers? g (attenuate g narrowing)) holds for every pair of grants, so attenuating can only ever remove authority. attenuate also returns the greatest grant covered by both of its inputs rather than merely some grant covered by both, which is what makes narrowings safe to compose: applying two restrictions in either order gives the same authority, and a grant narrowed twice is exactly the intersection of the two, not something smaller that happens to be under both.

That distinction is easy to state and easy to get wrong, so the module ships 90 tests that check these as properties over a generated corpus of grants rather than as examples, along with the component boundary that keeps /a/bc from counting as under /a/b, and the argv[0] match that lets an allowlist entry git permit git and refuse /tmp/evil/git.

Grants serialize without changing what they permit

grant->specs renders a grant to strings like fs:rw:/work and exec:allowlist:git,sigil; grant-from-specs parses them back. Every spec string (sigil system grant) accepts today parses to the same authority here.

A grant round-trips through that surface preserving what it permits, and that holds by construction rather than by testing: a resource the grammar cannot carry unambiguously is refused when the grant is built, so the value that would survive the round trip as something broader cannot be created in the first place. The cost is worth knowing up front. An exec command cannot contain a comma, because the comma is what separates commands in exec:allowlist:. Filesystem paths may contain commas and colons, and both round-trip.

What this does not do

This release ships the value and the algebra, and nothing else. Nothing in Sigil consults a grant yet: there is no enforcement, no sandbox, and no VM involvement. Importing (sigil grant) does not restrict what your code can do.

It is useful now to any tool that already decides what it will allow and wants to express that decision in a form another tool can read, compare and narrow. The checking layer is later work.

0.20.3 (2026-09-01)

A build performance and async-correctness release. Sigil recognizes when a package has not changed and skips it outright instead of walking its inputs again, which turns a no-change rebuild from a coffee break into a pause. Under the async scheduler, tcp-connect no longer stalls every other fiber while it waits. Applications built with Sigil also report their own version and commit separately from the runtime they were built against.

Existing projects build without changes. The package cache is content-addressed and fails closed: anything it cannot account for is rebuilt.

Unchanged packages are skipped, not re-walked

A warm rebuild used to spend nearly all of its time in the package layer. Every module and native input printed CACHED, and yet each package still enumerated its inputs, checked its artifacts, handled its assets and rebuilt its context. Compilation and linking were not the problem; the bookkeeping around them was.

Sigil now computes a fingerprint for each package before building it, covering the package declaration and its source, native, asset, docs and vendor inputs, plus the normalized build configuration, target and toolchain identity, and the build mode. When the fingerprint matches a recorded manifest and the artifacts it names are present, the package is skipped entirely.

A fully unchanged Slate build for android-arm64 went from roughly 106 seconds to roughly 7 seconds, with all 15 package manifests hitting. The largest remaining cost is scanning big vendor trees, which is a later change.

The gate is deliberately conservative. A missing artifact, an unreadable input, an unknown configuration field or a schema change all miss and rebuild. A stale hit is a wrong build, so the cache prefers doing the work again over guessing.

Applications report their own version, not the runtime's

An application built with Sigil now carries its own version and workspace commit, separate from the sigil-lang version it was built against, and --version shows both. The commit includes a -dirty marker when the workspace has tracked changes at build time.

This matters for support. A bug report that says only which Sigil built the binary does not say which build of the application is running, and those are different questions.

Generated version metadata no longer disturbs the cache

Sigil writes version metadata into the build tree, which meant a package's own generated file could land inside the input set the fingerprint is computed over. A package could then look changed because of something Sigil itself had just written. Generated version metadata is now excluded from package input roots, so the fingerprint reflects your sources.

tcp-connect no longer stalls the scheduler

Under the async scheduler, tcp-connect blocked the whole thread while the TCP handshake completed. Every other fiber stopped with it, so one slow or unreachable peer paused all concurrent work, and a connect to an unresponsive host held everything for the full connection timeout.

The connect is now split into a nonblocking start and a finish. When a scheduler is running, the fiber yields while the socket is not yet writable and other fibers keep running; the connect completes when the socket is ready. Without a scheduler the behavior is unchanged, so synchronous code sees exactly what it saw before.

Upgrade notes

Nothing is required. The first build after upgrading rebuilds everything once to record the new manifests, and subsequent no-change builds hit the fast path.

0.20.2 (2026-08-29)

An Android toolchain and native-build performance release. Sigil can now build dynamic Android/bionic AArch64 executables with managed Zig and a pinned, data-only Android sysroot, without requiring a separately installed full NDK. Full native release builds also stop compiling their Scheme dependency modules a second time during final bundling.

Existing projects build without changes. Android support is opt-in through an Android build configuration, and the native-build change reuses only objects already validated or produced by the current build invocation.

Build native Android executables without installing a full NDK

Sigil now has an android toolchain for dynamic bionic executables. The CLI installs managed Zig plus the pinned Android NDK r29 headers and target link stubs needed for the requested API, rather than keeping a complete NDK installation:

sigil toolchain install zig
sigil toolchain install android --target aarch64 --api 24

An Android configuration selects toolchain: 'android, target "aarch64-linux-android", and the ordinary native backend. Builds discover a complete managed installation automatically. SIGIL_TOOLCHAIN_DIR can select another durable location, while SIGIL_ANDROID_SYSROOT and SIGIL_ANDROID_LIBC remain explicit overrides.

Release downloads include sigil-android-arm64, a dynamic bionic executable targeting Android API 24 and newer.

The installer verifies the pinned NDK archive before selectively extracting 3,671 headers and 25 link files. A clean installation was exercised from an empty Sigil home, then rediscovered and used for a subsequent build without overrides. The resulting API-24 AArch64 executable ran under Android/Termux.

If the home toolchain directory is not writable, builds use the workspace-local .sigil/toolchain directory. Large downloads and temporary tool caches likewise fall back to writable temporary storage; SIGIL_TOOLCHAIN_CACHE_DIR can choose that location explicitly.

Native sockets work under Android's syscall policy

Android's application seccomp policy can reject the plain accept syscall with SIGSYS, even for an otherwise valid static AArch64 Linux executable. The symptom was a listener that printed its ready message, accepted the first TCP connection, then disappeared without an application diagnostic.

Sigil's native socket runtime now uses accept4(..., SOCK_CLOEXEC) when the target toolchain supports it. It retains an ENOSYS fallback to accept plus the existing close-on-exec setup for systems where accept4 is unavailable. An Android/bionic Slate build served a loopback UI in Termux after this change, including local file and terminal operations from the phone browser.

Target C capabilities are probed instead of guessed

The build API can now compile target-toolchain probes and generate a capability header from the results. This lets runtime code select an implementation based on what the actual target compiler and sysroot provide, rather than host operating-system macros or a growing platform allowlist.

The public surface includes c-probe, probe-c-capabilities, generate-c-capability-header, and c-capability-header-flags. Probe results participate in the native archive inputs, so a capability change cannot silently reuse an incompatible cached runtime.

Release builds no longer compile native modules twice

The package phase already emits and parallel-compiles native Scheme module objects. Final bundling reconstructed their fingerprint without the complete compiler-input closure, rejected the objects it had just built, then emitted and compiled the dependency set again in a serial tail.

Final bundling now recognizes objects whose complete derivation was checked by the current workspace build. That trust is scoped to one build invocation, reset between watch-mode builds, and does not weaken persistent cache keys or the conservative path for direct callers.

In the development baseline under ambient machine load, a forced release build fell from 396 C jobs to 212 and from 634.7 seconds to 260.1 seconds. The differing background load makes the wall-time reduction directional rather than an acceptance benchmark; removal of the 184 redundant jobs and the complete serial dependency tail is exact. Warm serial and parallel release builds produced identical artifact hashes.

Also in this release

  • ZIP extraction can stream a selected archive entry directly to disk. The Android installer uses this to avoid holding the large NDK archive and extracted files in memory together.
  • (sigil build toolchain) provides the shared managed toolchain and cache-location policy to build consumers without duplicating CLI defaults.
  • Native build profiling now attributes package orchestration as well as emission, C pools, archive creation, entry generation, linking and bundle writing. A committed benchmark runner records ambient load, process count and build-owned RSS for repeatable quiet-machine reruns.

Upgrade notes

Nothing is required for existing projects. To build for Android, install the managed Zig and Android toolchains, add an Android configuration to the workspace, then build with that config. The Android installer currently supports AArch64 and defaults to API 24.

0.20.1

A sandbox and native-codegen correctness release. Builds no longer hang when a sandboxed environment exposes Sigil's shared store as readable but not writable, and a caught exception raised by eval-string no longer abandons the rest of a native WASM caller. Process spawning can also give a child its own working directory and environment overrides.

Existing projects build without changes. The cache fallback is automatic when the default store is read-only. An explicitly selected SIGIL_STORE remains a promise that the path is writable and now fails immediately when that promise is false.

A read-only build cache no longer looks like a lock held forever

In a sandboxed environment, $HOME/.cache/sigil/store can be read but not written. A warm build treated the write denial as another writer holding a lock and polled forever. The workaround was a fresh store under /tmp, which made the build proceed by throwing away the warm cache and recompiling the whole dependency graph.

A verified hit is now a read operation and takes no publication lock. When the default store really is read-only, sigil build also opens a stable writable upper store under /tmp/sigil-store-overlay-v1-<hash> and keeps the original store as a read-only fallback. Reads search both; new objects, locks and roots go only to the upper store. No sandbox setting or worker-specific environment variable is required, and the build prints both paths when the overlay is active.

A corrupt fallback object is left untouched and rebuilt into the writable layer. Permission failures now report immediately, and genuine lock contention has a 60-second bound. Measured in the environment that exposed this, the first sigil-stdlib build completed instead of hanging, and the next completed in about five seconds with every module reported CACHED.

A caught eval-string exception resumes native and WASM callers

A guard around eval-string could run its handler and still abandon everything after the guard in a native WASM build. The process exited 0, which made the failure especially quiet: Slate reached a ready state with a working eval channel, but never opened its startup view and never ran the rest of main.

Code after the guard now runs as expected. This covers eval, eval-string and other native calls that can raise indirectly, including applications that evaluate native code from native code.

The differential exception corpus now catches a failing eval-string, checks for caught:continued, then runs a later dynamic-wind case. Bytecode, native ELF and native WASM produce the same 14 lines and exit status.

Spawn a child in its own context

process-spawn accepts cwd: and environment:. The working directory changes only in the child, and environment entries such as "MODE=test" override inherited values without mutating the parent process:

(process-spawn "git" "status"
  cwd: checkout
  environment: (list "GIT_OPTIONAL_LOCKS=0"))

Both options use the native spawn path on Unix, macOS and Windows. Invalid environment entries are refused before the child starts.

Also in this release

  • If a test file registered tests and then failed while loading, the runner reported only the load error and discarded the registered tests. It now runs them and counts the load error as one additional failure.
  • Quotes, backslashes and control characters in package name or version metadata could produce invalid generated C for a native application. The generated string literals are now escaped.

Upgrade notes

Nothing is required. In a sandbox, run sigil build normally and Sigil will select the overlay when it needs one. If you set SIGIL_STORE yourself, point it at a writable directory.

0.20.0

A dependency-declaration release, with one runtime fix that matters if you build to WASM. The sigil: field in package.sgl is now a minimum version rather than a range, so sigil: "0.16" means "needs at least Sigil 0.16" and stops expiring on its own.

Existing projects build without changes. Carets are grandfathered and keep working, with one warning per build. Locks are untouched and no project's runtime moves. The minor bump is because the field's grammar narrowed: a host manifest declaring ~, >=, <, = or * is now refused.

Why a floor, and why the old field expired by itself

A caret on a 0.x version pins the minor, so ^0.16 means [0.16.0, 0.17.0). That made every sigil: declaration expire by construction. A package that declared ^0.16 two releases ago was, by its own declaration, incompatible with 0.19 while running perfectly. Staleness was the resting state rather than a lapse.

The proof that the old reading measured the wrong thing is that the monorepo could not build itself under it. Twenty members declared ^0.17, five declared ^0.18.6, and sigil-cli at ^0.17 depended on four ^0.18.6 members. It built fine every day.

An upper bound is a prediction about software that does not exist yet; a floor is a fact about what you use. Predictions expire and facts do not. If you need a real ceiling you can now say so: sigil: (supports min: "0.16" max: "1.0"), with max exclusive.

Your existing carets keep working, and the tolerance removes itself

A caret with a floor below 0.20 is read as the bare floor, with one compact warning naming the file, the caret and the replacement. The caret is ignored rather than honoured, so a migrated manifest and an unmigrated one resolve identically. Every operatored declaration measured across the estate was a caret, all 2,858 of them, so refusing the others costs nobody anything while keeping the grammar closed against new spellings.

Two mechanisms end the tolerance without anyone having to remember. New caret debt cannot accumulate, because only floors below 0.20 are grandfathered and ^0.20 refuses today. And at Sigil 1.0 the tolerance ends entirely, because ^0.16 read as floor 0.16 must not admit 1.0.

Neither is behind an environment variable, deliberately. A mode switch is a deferred flip, and deferred flips do not happen.

Why the warning names a version

The warning tells you the minimum toolchain to be on before rewriting the value, and that is load-bearing rather than polite. A compiler predating this release reads a bare sigil: value as an exact range. Measured on a real repository: it evicts the lockfile's runtime pin and silently downgrades, exit 0.

So following the advice too early breaks your lock. A refusal never had that hazard, because seeing one proved your compiler was new enough. A warning travels to people whose compiler is not.

Relocking does not move your runtime

sigil deps install keeps the locked sigil-lib and sigil-stdlib even across the migration's range restatement. sigil deps update holds the runtime unless you pass --upgrade-sigil. Only a fresh install with no lockfile takes the newest admitted tag.

If you have been expecting a relock to pull a newer runtime line, it will not, and that is the intended behaviour rather than an oversight.

Macros now expand correctly in native and WASM builds

On a native-codegen build, null?, pair? and symbol? answered about a syntax object's wrapper rather than its datum. The bytecode VM and the C builtins both unwrap first, so the two backends disagreed and only native was wrong.

That matters because a procedural macro transformer is handed the marked syntax object and takes it apart with exactly these predicates. (sigil core)'s own extended-lambda transformer is one of them: its walk over the formals never terminates on (null? formals), calls car on a syntax-wrapped (), and the expansion dies with car: expected pair.

The fingerprint was an odd asymmetry:

(lambda () x)     failed to expand
(lambda (x) x)    worked

because with a non-empty formals list car unwraps, and the next null? then sees a plain ().

If you only ship bytecode, nothing changed for you. The only native-codegen build that expands macros at runtime is a WASM app's eval door, so this reached browser apps that evaluate code the user types. Slate's scratchpad is the case that surfaced it.

Exactly three predicates were affected, and that is the whole class rather than the three we happened to notice. Of Sigil's seven syntax-transparent builtins, only null?, pair? and symbol? have an inlined native fast path; list? and syntax? already fell through to the builtin, and identifier? and keyword? have no CPS primitive at all. number?, string?, boolean?, procedure? and char? are opaque in both backends and stay that way.

Also in this release

  • The deps command paths refused invalid manifests through a mechanism that does not survive native-codegen binaries, so a release binary printed the refusal and installed anyway. All four paths now check positively.
  • A lock carve-out could hold a dependency's sha across a URL change, which then failed worktree creation: a healthy install refused for a stale reason.
  • sigil: " 0.16 " passed validation, which trims, then fell through to legacy resolution, which did not, silently resolving an exact 0.16.0.
  • (supports min: "0.16" max: "0.16.0") was accepted as a window admitting nothing.
  • A held runtime's "there is a newer tag" hint pointed at sigil deps update, the command you may just have run. It now names --upgrade-sigil.

Upgrade notes

Nothing is required. Your carets keep working and will warn once per build.

To silence the warning, drop the operator: sigil: "^0.16" becomes sigil: "0.16". Do this only once every Sigil building the project is at least 0.20.0, which the warning states explicitly. An older compiler reads the bare value as exact and will evict your runtime pin.

If you genuinely need an upper bound, use the supports form rather than an operator.

0.19.4

A build-integrity release. sigil build now checks, before it compiles anything, that your compiler and the runtime you have locked and installed are the same generation. -L also works for the first time on the modules the CLI loads at startup.

Existing projects build without changes. The coherence check ships in reporting mode: it prints what it finds and the build continues.

A build that cannot work says so before it compiles

A build whose compiler and whose locked runtime disagree used to exit 0. It printed "Build complete!" over an artifact that was dead on arrival, and one of those reached a phone.

That matters more than one bad binary, because every other verification you have ends in "it built". A toolchain that can emit a dead artifact and exit 0 means no green anywhere is worth anything.

Three checks now run first, each reading a different source so no two can fail together by construction: compiler identity against the lockfile, the lockfile against the .sigil/deps symlinks, and compiler identity against the headers on disk. That last one never reads sigil.lock, so hand-editing the lock does not get you past it. A fourth check confirms the sigil you resolved is at least the minimum each dependency asks for.

Why it reports instead of refusing

SIGIL_BUILD_COHERENCE takes report, enforce or off, and defaults to report.

That default is the design, not caution. Eleven of the estate's twelve consumer repos are incoherent today, so enforcing on day one means eleven teams discover that the only way to build is to switch the check off. A bypass that becomes a habit is worse than no check: it trains you to reach past the next one too.

Reporting prints one line per finding, detail under --verbose. The default flips to enforce when the estate can pass it, and both modes are gated on the same tree so flip day is not the first run of the code that matters.

-L and SIGIL_LIB now reach the modules the CLI loads first

sigil -L <dir> searched <dir> only for modules the runtime had not already loaded, which in practice exempted every module the CLI imports at startup. The consequence was concrete: an extracted package could not be tested from its own checkout. sigil test in sigil-log ran the binary's copy of (sigil log), and a syntax error appended to the checkout's source changed nothing.

The resolver was never at fault. -L was parsed only in Scheme, long after the CLI's module graph had been imported, and an already-registered module is returned before any load path is consulted. The defect was temporal, not ordinal. Both emitted entry points now scan argv before any Scheme import. Precedence, lowest first: @bundle/lib, <exe>/../lib, SIGIL_LIB, then -L.

Three problems in that change were caught in review and fixed here, two of which did not exist in 0.19.2: 128 -L flags hung forever with no output where 0.19.2 printed an error and exited 1; the argv scan reached bundled applications, so app -L <dir> could replace one of the app's own modules; and "leftmost -L wins" was false, because options were re-registered left to right after the C scan had registered them right to left.

One consequence worth stating plainly: because -L now outranks the embedded archive, a load path can overshadow modules the release binary ships with, including the ones behind registry signature checks. That is intended behavior going forward, and it is what makes an extracted package testable from its own checkout.

Also in this release

  • A missing minisign reported every signature as a bad signature, exit 69, with nothing in the message suggesting you install anything. It failed in the direction that looks like the security check working. An absent or unstartable verifier is now exit 78 and says so.
  • A first sigil deps install in a project with no lockfile wrote a lock and a symlink pointing at different commits. git rev-parse on an annotated tag returns the tag object, not the commit, and both recent releases are annotated.
  • (sigil-abi-version) exposes the ABI version the running compiler was built with, so a build can compare it against the locked runtime instead of dying at link time.

Upgrade notes

If your project is incoherent, sigil build will now say so on every build. The remedy is named in the message: run sigil deps install. A git pull that touched sigil.lock is enough to put you here.

Set SIGIL_BUILD_COHERENCE=off to silence it, or enforce to get the refusal now.

0.19.3

An environment release. sigil env shell now hands your program the search paths its dependencies declare, not just the ones its top-level packages declare, so a tool finds the terminfo database and the helper binaries it was built against instead of quietly falling back to whatever the host happens to have.

Existing projects build without changes. Locks written by an older Sigil are still read, with a warning naming what they cannot express. Re-locking is the act that commits a project to 0.19.3 or newer.

sigil env shell exports what your dependencies declare

Here is the symptom that started this. A tmux built entirely from a Sigil channel, run under sigil env shell, drew its terminal description from the host's system profile rather than from the ncurses it was built against, and tic on PATH was the host's rather than the store's. Nothing failed and nothing warned; the wrong answer was indistinguishable from the right one until you looked at which file had actually been opened.

The consumer composed five hardcoded variables (PATH, MANPATH, INFOPATH, TERMINFO_DIRS, XDG_DATA_DIRS) over the lock's root packages only, so a dependency's paths were never exported at all. The lock now records the runtime closure instead. Any root package whose dependency provides a binary, a manual page or a data directory now finds it.

That is half of the problem. Honouring a package's own search-paths: declaration at runtime is a separate change and is not in this release.

A new lock format, and why it is a bump rather than an extension

env.lock version 2 adds three things: one row per runtime closure member keyed by package name, the base search paths moved out of consumer code, and a system: header key so that air-gapped and store-shared reification can fetch by bare hash.

There was no forward-compatible way to add them. Measured against 0.19.2, a new record type, an extra key on an existing record and an extra header key are all refused with exit 68, exactly as a version bump is. Since every route cost the same breakage, the bump was chosen for the diagnostic it produces: "unsupported env.lock version 3; this sigil reads 1, 2" tells you what happened, where "unknown env.lock record" reads like a corrupt file.

What a lock may contain

The new rows put variable names and subdirectories into lock content, and sigil env exports turns both into shell text. So the reader validates them where the lock is decoded rather than where the text is emitted: a variable name must be a shell identifier, and a subdirectory may not contain quotes, backslashes or control bytes, on top of the existing rules against absolute paths and ...

If you hand-edit an env.lock and it is refused, that is why.

sigil deps install leaves your lock alone

In a workspace, sigil deps install rewrote sigil.lock on every run, so the tree was never clean and no tag could produce a clean build. Committing the regenerated lock was itself what invalidated it, so there was no commit at which deps install did nothing. If you have been committing lock churn you did not cause, this is it.

A dependency that resolves back into your own project is no longer locked at all. Sigil's own lock went from 17 entries to 13, a pure deletion. Two smaller repairs ride along: building without a .git directory no longer records an empty sha: that breaks the next install, and a deps install that keeps a stale lock now says so instead of reporting success.

Publishing and locking survive a busy machine

sigil env lock and sigil channel publish evaluate your channel's own program inside a sandbox with a CPU budget. That budget was 5 CPU-seconds and one cold run costs about half of it, so any other work on the same machine pushed it over. It is now 30 CPU-seconds with a 120-second wall clock. If you have had a channel that publishes fine in the morning and refuses in the afternoon, that was this. One thing worth knowing before you try to tune around it: the cost is invoking the compiler at all, not the size of your channel, so trimming bindings will not help.

The message was wrong in both directions. A sandbox that ran out of CPU reported that your channel had failed, and an unbound variable in your channel.sgl was reported as "evaluation failed within resource boundary": a real defect in your code, blamed on your machine. Resource exhaustion is now a setup failure, exit 75, naming the limit it hit, and a program failure names the program and its exit status. Where the sandbox genuinely cannot tell the two apart, it says so rather than picking one.

A channel whose body calls (exit 0) without producing a result used to fail with cannot open file for reading: .../result.sgl. That is a named refusal now.

0.19.2

A correctness and speed release. Calling a native procedure with the wrong number of arguments now raises an arity error instead of reading whatever happened to be on the stack, native builds work on OpenBSD, and a native build refuses to link against a runtime older than the compiler that produced it rather than producing a binary that misbehaves later. Sigil also starts about three times faster.

Existing projects build without changes, with two exceptions worth reading below: code that over-applied a native procedure now raises, and a native build whose lockfile pins an older sigil-lib than the compiler will now refuse to link.

Calling a native with the wrong number of arguments now raises

(car '(1 2) 3 4) returned 1. (car) returned a type error about something you did not write. (apply car '()) crashed the process with a segmentation fault. Each of those took a different path into the same native procedure, and only one of the four checked how many arguments it was given.

Natives that document an argument count now have it enforced on every path, so all four spellings raise the same arity error naming what was expected and what arrived. The crash was the visible case: a native reading argument one when it was passed none read an uninitialised stack slot, which segfaulted in one build and returned a plausible wrong answer in another.

This can surface existing bugs. If your code passes a native more arguments than it takes, that call used to succeed quietly and now raises. The extra arguments were always ignored; what changed is that Sigil says so.

Variadic natives are unaffected: (apply + (make-list 1000 1)) and a 5000 argument string-append work exactly as before.

Native builds work on OpenBSD

A natively compiled Sigil program died on OpenBSD at the first thing it did inside with-async. No error, no output, nothing in the log. The binary built cleanly, installed cleanly and answered --version correctly, so every check short of running it passed.

OpenBSD refuses a system call made while the stack pointer is in memory the kernel was not told is a stack. Sigil's coroutines allocated their stacks on the heap, so the first system call inside any fiber killed the process. Fiber stacks are now mapped with MAP_STACK on OpenBSD, and nothing changes on other platforms.

If you disabled the native backend on OpenBSD because of this, you can turn it back on.

A native build refuses a runtime older than the compiler

Building natively with one Sigil version while your lockfile pins an older sigil-lib produced a binary that ran, mostly. Calls the newer compiler emitted by name were absent from the older runtime's table, and the lookup failure returned an undefined value rather than raising. Programs that guarded the call saw garbled output. Programs that did not carried the undefined value forward.

The native link now compares what the generated code calls against what the linked runtime provides and refuses when they disagree, naming the procedures that are missing. A native build under the 0.19.2 toolchain with sigil-lib pinned at 0.19.0 or earlier will now stop. Run sigil deps update and it proceeds. Bytecode builds are unaffected, because a bytecode bundle carries the runtime it was compiled against.

The check adds about a second to a native link. Development and test link paths do not perform it yet, because they link no generated code for it to read.

Sigil starts about three times faster

sigil --version took roughly 500ms and now takes roughly 180ms. Garbage collection got faster by a wider margin: a collection heavy workload runs about six times fewer instructions.

Two diagnostic environment variables were being read once per object during collection, and reading an environment variable scans the whole environment. Sigil's speed therefore depended on how many environment variables your shell had, with no way to notice: the same program was measurably slower under a large environment than a small one. The values are now read once per process.

Setting SIGIL_GC_CRASH_DEBUG or SIGIL_GC_TRACE_FREE after the process has started no longer takes effect. Set them before starting the process, as before.

0.19.1

A compiler correctness release. Three optimizer defects could make a guard test a different value than the one you wrote, so a program takes the wrong branch with no crash and no failing test. One has shipped since 0.14.6. All three need optimize: 1 or higher, which means no test suite could catch them: sigil test, sigil eval and every dev build run at optimize: 0, where the code is correct. Rebuild and relock anything built at optimize: 1 or above.

Also in this release: reader errors say what went wrong, an unrecognized define-library clause is refused instead of silently dropped, uncaught errors go to stderr, and a failed dict-ref names the key.

Existing projects build without changes.

A guard could test a different variable than the one you wrote

If a module-level cell is assigned exactly once inside a branch, the optimizer could treat reads that happen before the assignment as though it had already run. Code like this is the shape at risk:

(when (and (number? now-ms) (number? *last*))
  (set! *last* now-ms))

(number? *last*) gets answered with the result of (number? now-ms), while the arithmetic that follows reads the real cell. The guard passes and the guarded operation runs on the unguarded value. A guarded stamp raises, which is loud; a compare-and-set latch quietly becomes (eq? v v) and never fires, which is not.

Present since 0.14.6 (2026-04-27). It is intermittent by nature: the same source flips between correct and miscompiled as surrounding code changes, and the rewrite it needs happens in roughly 0.1% of cases, which is why it went four months unnoticed. Generated code is unchanged everywhere else, verified byte-identical across a 187-module build.

If you are stuck on an older toolchain, SIGIL_CPS_NO_CSE=1 will not help: sigil build clears it for cache determinism. Only optimize: 0 avoids the pass.

Bounds checks could be removed on the same bad reasoning

The same faulty rule fed bounds-check elimination, which could drop a check and emit an unchecked vector access. Demonstrated on a purpose-built case and fixed here. No out-of-bounds access was produced from it, and a sweep of 671 modules found no real-code elimination resting on the bad reasoning. The fix costs no optimization: eliminations are unchanged on the largest sample measured.

Delimited continuations could reach the same wrong conclusion

The optimizer missed two ways control can arrive at a point: the resume edge of abort-to-prompt and the result continuation of call-with-prompt. A missing path turns a merge into a straight line, so the value a guard tests is whatever the continuation was resumed with. Anything using delimited continuations, including guard and the async scheduler, was exposed at optimize: 1 and above. Generated code is unchanged, byte-identical across a 185-module build.

A reader that fails now says why

A #lang file that failed to parse reported read-language-source failed for #lang sigil/doc: with nothing after the colon. The reader's own diagnostic was dropped, so anyone writing in a #lang got the language name and no reason. Reader errors now carry their text through, and an empty diagnostic is reported as empty rather than dressed up as a message.

call-with-output-string works at the prompt

call-with-output-string, open-output-string and get-output-string are available in the REPL and in eval without an import. They live in (sigil io), which the interactive environment does not import, so building a string from a port failed with an error that did not suggest the fix.

An unrecognized define-library clause is refused

Unknown clauses used to be discarded silently, so a single misplaced paren could close the library body early and throw away the rest of the file. The result compiled cleanly and was missing definitions, surfacing much later as an unbound variable in a different package. It is now an error.

Smaller fixes

Uncaught errors print to stderr instead of stdout, so piped or captured output no longer has error text mixed into it. A failed dict-ref names the key it looked for, and flags a keyword used where a symbol was expected or the reverse.

Known issues

Three suite-level failures predate this release, each confirmed by reproducing them on the previous compiler, so make test does not currently pass end to end. None gates this release and fixes for all three are tracked.

  • test-release-suite segfaults. The Makefile marks it mandatory for optimizer changes, so it is named here rather than left to be discovered.
  • test-async-dns times out. The resolver does not yield; the timeout is the symptom.
  • The musl target gate fails under make test-build and passes when run on its own. Cause not established.

Upgrading

Rebuild anything built at optimize: 1 or higher, and relock. sigil-stdlib is compiled by the compiler, so a stdlib pinned before this release keeps carrying the old codegen even after you upgrade the toolchain.

0.19.0

Sigil's core packages move back into the main repository. The toolchain was extracted into a dozen standalone repos a few months ago, each with its own version line, and every project carried a pin for each one. Now the core packages ship from one place on one version, and a project depends on Sigil rather than on a list of its parts. The language is unchanged; what changed is how you say which Sigil you want.

The minor bump is deliberate. A ^0.18 pin resolves anything below 0.19.0, so shipping this as 0.18.7 would have pulled the new layout into every project's next lockfile refresh unasked, and any project still pinning the standalone packages would have ended up with two copies of one module in a single build, chosen by load order. At 0.19.0 you meet the new layout in the same commit where you drop the old pins, and not before.

One dependency instead of twelve

sigil-args, sigil-format, sigil-git, sigil-help and sigil-repl now live in the monorepo alongside the packages already there. Where your package.sgl listed several from-git entries pointing at separate repositories, it now lists Sigil.

Builds behave the same, but pins need updating, because the repositories they name are no longer where the code is published from. The migration is one commit per project: drop the standalone entries, set sigil: "^0.19", relock.

Projects that are not ready stay on 0.18.x and keep working. There is no deadline in this release.

Six libraries are now part of the standard library

(sigil ansi), (sigil fp), (sigil hooks), (sigil match), (sigil socket) and (sigil version) ship inside sigil-stdlib. They were small, stable, and widely depended on, which made a separate repo and release cadence per library pure overhead: a project wanting pattern matching and ANSI colour carried two extra pins to get two files.

Module names are unchanged, so imports stay exactly as they are. Drop the from-git entry and the import keeps working.

Their standalone repositories are deprecated at their final releases: sigil-ansi v0.16.0, sigil-fp v0.9.3, sigil-hooks v0.16.1, sigil-match v0.9.1, sigil-socket v0.17.0, sigil-version v0.16.0.

Leaving a pin in place is the one thing to avoid. A standalone pin still in your dependency closure means two providers of the same module in one build, resolved by load order.

sigil-docs is now sigil-help, and it ships in the monorepo

The package that loads and searches Sigil's API metadata is renamed. It answers "what is this procedure, and what can I call" for tooling: the REPL, editor integrations, sigil help. It was never the documentation site, which is what the old name suggested to nearly everyone who met it.

The standalone sigil-docs repository is deprecated at v0.16.1 and there will not be another release. Module names and the API are unchanged, so switching is a manifest edit, not a code change.

The rename is free at exactly this moment: every project has to touch its pins for this release anyway. Doing it later would mean asking everyone to update twice.

Upgrading

  1. Update sigil: "^0.18.6" to sigil: "^0.19".
  2. Remove every from-git entry for a package that has moved: sigil-args, sigil-format, sigil-git, sigil-repl, and sigil-docs (now sigil-help, from the monorepo).
  3. Remove the entries for the six libraries now in stdlib: sigil-ansi, sigil-fp, sigil-hooks, sigil-match, sigil-socket, sigil-version. Leave your imports alone; the module names did not change.
  4. Run sigil deps update. deps install will not do it: a pin that still satisfies its declared range is left alone, which is correct for install and the wrong tool here.
  5. Build and run your tests.

A module reported from two places means a standalone pin is still somewhere in the dependency closure. Removing it resolves the conflict.

0.18.6

A compiler correctness release. Optimized builds could silently take the wrong branch inside a loop: no crash, no error message, just a wrong answer from a program with no failing test. Three releases carry it, so anything built by 0.18.3, 0.18.4 or 0.18.5 at optimize: 1 or higher should be rebuilt on this release, and relocked as well, since sigil-stdlib is itself compiled by the compiler and a stdlib pinned inside the exposed window keeps carrying miscompiled code after the toolchain is upgraded. Alongside it, define with keyword parameters works in the REPL, eval stops leaking on every call, and a caller can choose the source address of an outbound connection. Two shapes that used to be accepted are now errors, both cases where the old behaviour silently did nothing.

A loop testing (not x) could take the wrong branch every time

Loop-invariant code motion lifted a (not x) test out of a loop, which is a fine thing to do when x really doesn't change. The bug was in working out what x was: where x came from a join, like the two arms of an and or an if, the optimizer picked one arm's value and treated it as the answer. The test was then computed once, before the loop, from the wrong operand. Every iteration read that one stale result.

The symptom in a real program: folio's integrity scanner reported "no buried task ids found" against a corpus that held six. It read the files correctly, split the lines correctly, and resolved ids correctly. The one branch deciding whether a line matched had been decided before the loop started.

That shape is what makes this worth a release on its own. A crash is cheap to find. An answer that is confidently wrong, from a program with no failing test, is not.

Some specifics worth having:

  • Affects 0.18.3, 0.18.4 and 0.18.5. Measured broken on 0.18.3 and 0.18.5.
  • Fires at optimize: 1 and above, on both backends. Switching between bytecode and native doesn't avoid it.
  • 0.18.2 and earlier are unaffected. Builds before August 5th shipped with the CPS optimizer switched off entirely, which is the only reason the exposure window is three releases and not three years.
  • Dev and test configs are optimize: 0, so sigil eval, dev builds and the test suite were all structurally incapable of seeing it. That is why it survived three releases and several investigations.

define with keyword parameters works in the REPL

This was accepted in the REPL and bound nothing:

(define (f x (keys: (y 1))) (+ x y))
(f 2)      unbound variable 'f'

The same form in a file has always worked. Runtime eval shared a code path with the evaluator that runs macro transformers, and that path deliberately skips expansion, so core's define macro never ran and the primitive converter compiled the form instead. Runtime eval now has its own expanding path, which also fixes (: ...) type specs and every other core macro used under eval.

sigil eval, file execution and MCP eval were never affected. The REPL was.

A malformed parameter list says where it is

(define (f 42) ...)

A non-symbol where a parameter name belongs used to compile without complaint and bind nothing, so the failure surfaced later as an unbound variable pointing at the call site rather than the definition. It's a located error now, naming the offending formal.

This is one of the two shapes that used to be accepted and now isn't.

eval stops leaking on every call

Every successful eval leaked. Over 100,000 evals, peak memory went from 242 MB to a flat 76 MB. If you run a long-lived process that evaluates code, this is the change you'll notice.

Choosing the source address of an outbound connection

A new primitive, %tcp-connect-ip-from, binds the local end of a socket to a source address you choose before connecting, so the peer sees the connection arriving from that address. Only the address is pinned. The kernel still picks the port, so rapid reconnects don't fail while a previous connection sits in TIME_WAIT.

This is purely additive. No existing signature changed.

Known regressions

The release-configuration test suite does not run to completion. A release binary built from this source dies with a segmentation fault partway through the standard library tests, after about 124 of 158 files. It dies between files, having finished test-random.sgl and while reaching for test-async-io-fairness.sgl, with async and subprocess children still live. This is not new in 0.18.6. Released 0.18.5 does the same thing at the same point, and so does every tree we measured in between: five reproductions across four source trees, deterministic every time.

It is invisible to the ordinary test suite, which runs at optimize: 0, completes all 158 files, and reports the same 2,713 passes on either side of this release's changes. The crash appears only in a release-configuration binary, which is the one surface that suite exists to cover.

We found it because this release is the first to run that suite as part of verifying a compiler change, and we are disclosing it rather than shipping quietly around it. It is not caused by anything in this release, and the fixes here are verified independently of it. It is being investigated on its own.

Loop-invariant code motion now performs no hoists at all in any build we measured. Before the fix it performed twelve, and all twelve were instances of this bug. Legitimate loop-invariant shapes measured zero hoists on the fixed and unfixed compilers alike. So this release removes unsound optimization without adding sound optimization, and if you were counting on that pass for performance, you were not getting it. Whether the pass earns its place at all is an open question we'd rather answer deliberately than fold into a correctness fix.

eval still leaks roughly 1.3 kB per failing eval. Successful evals are flat. A process that evaluates untrusted or malformed code in a loop will still grow.

0.18.5

Most of this release is about Sigil telling you the truth when something goes wrong. Four of the six changes fix diagnostics that pointed somewhere other than the fault, and two of those could stop your process outright rather than reporting an error at all.

Existing projects build without source changes. One error-handling behaviour did change, and it's described at the end rather than buried in a bullet.

A stray brace no longer hangs the compiler

Typing } where you meant ) used to hang the reader, or climb memory until the process died. One byte was enough: a file containing nothing but } would spin forever. Both cases now report a syntax error, and where there's an opening bracket to point at, the message names it and its line:

(}          mismatched brackets: '(' opened at line 1 closed with '}'
(a {b})     unexpected '{'

If you parse Sigil source inside a service, this matters more than the message does. Source that's untrusted, or simply mid-edit in an editor, could take the process down with a single byte. Dictionary literals are unaffected, since #{ and its closing brace are consumed by the dictionary reader itself.

A misplaced paren no longer silently guts a function

Two malformed shapes used to compile without complaint and evaluate to nothing:

(let* ((x 1)))          ; no body
(if c a b extra)        ; extra arms

The first is what a stray paren looks like after it pushes a body out of its let*. The function still compiles, still runs, and quietly does nothing, which is hard to find because every tool reports success. Both are compile errors now.

Since this makes previously-accepted programs fail, we measured the blast radius first: zero projects newly failed across 60 projects and 3,123 modules, including three WebAssembly builds whose conditional branches a source scan can't see. A begin with an empty body is still allowed, because an empty cond-expand arm produces one.

list-ref tells you what went wrong

(list-ref '(a b c) 3) used to fail with car: expected pair, which named neither the operation, the index, nor the length. A negative index produced cdr: expected pair instead, so even the symptom varied by input. It now reads:

list-ref: index 3 out of range for list of length 3
list-ref: cannot reach index 0; argument is not a proper list (non-pair at position 0)

base64url-encode works on inputs over ~192 bytes

It used to raise apply: too many arguments. The real limit was apply to a builtin, capped at 256 arguments, so any code that builds a list per element and then applies a builtin over it hit the same wall. Applying to a Sigil procedure was never capped, which made the limit surprising as well as arbitrary. The cap is gone.

Stack traces stop pretending to be stacks

The frames printed under an error aren't the call stack. They're reconstructed from a ring buffer of recent calls that never records returns, so functions which already returned appear with confident file and line numbers. That's worse than no trace at all, because it points somewhere specific and the code it names really did run recently, so everything you check nearby looks plausible. In one compiled app failing three calls deep, all 64 printed frames were library initialization history and not one named a function actually on the stack.

This release fixes the honesty rather than the accuracy, since making the frames correct is a much larger change than a patch can carry. A reconstructed trace now says what it is and points you at bisecting with checkpoint output instead.

Two caveats. Every backend is affected, not just native, so switching backends won't buy you a truthful trace. And the new label only appears when a natively compiled program dies on an uncaught error, so an unlabelled trace still isn't trustworthy.

The error message itself was always reliable. Only the frames were invented.

Shutdown stops reading freed memory

Sizing a struct at teardown read the field count out of its type descriptor, which may already have been released. Structs now carry their own count.

This came from a reported segfault in a 0.18.4 static binary, and that crash couldn't be reproduced across four attempts. The undefined behaviour was real and worth removing, but don't read this as having explained that crash.

Behaviour changes

An out-of-range list-ref is no longer a type error. The old failure happened inside car, so it was classified as a type error by accident. Code like this used to catch it and won't now:

(guard (exn ((type-error? exn) ...))
  (list-ref lst i))

Match on the error generally instead. The old classification was an artifact of where the failure happened rather than a designed contract, which is why this is a patch release, but it's worth checking for.

Known limitations

Stack trace accuracy, a native C-stack overflow guard, and a shadow-stack imbalance past 1024 frames of recursion are all tracked but unfixed. The shadow-stack issue is masked on host builds and matters most for WebAssembly.

0.18.4

Sigil source files can now select an installed language reader with #lang. A reader turns its source into ordinary located Sigil syntax, so a language can introduce its own notation while retaining Sigil's compiler, build graph, cache, runtime, and tooling. The same source works through compilation, direct execution, load, tests, bundles, native builds, and both WebAssembly backends.

Sigil Doc is the first separately installed language built on this protocol. It uses #lang sigil/doc for programmable prose: authors can combine document markup with ordinary Sigil bindings and macros, then render the resulting semantic document as a website or book.

Existing standard Sigil projects build without source changes. Compiler and bytecode caches rebuild once because this release advances the bytecode format to version 11 and adds language-reader inputs to cache identity.

A source file can select an installed language

A header such as #lang sigil/doc resolves (sigil doc reader) and calls its exported read-language-source procedure. The reader receives the complete source plus its name, language id, and exact header-end byte offset, then returns syntax for the normal compiler.

Resolution is mechanical. It does not search a registry, install a package, access the network, or fall back to standard Sigil after recognizing a valid header. Missing or invalid readers fail with stable diagnostics. A byte-order mark and a physical-line-one shebang are supported; displaced and overlong headers are rejected.

Files without a language header follow the existing reader path with their original bytes unchanged. The .sgd extension is a useful convention for alternate-language manuscripts, not a semantic requirement.

Language readers participate in the normal dependency graph and caches

A document's compiler inputs now include its selected reader and the reader's complete ordinary transitive dependency closure. Changing only a reader support module invalidates the document correctly, even when manuscript bytes and timestamps are unchanged. The same identity reaches CLI auto-compilation, content-addressed package builds, native code generation, bytecode WebAssembly, and native WebAssembly.

Compiled modules publish compiler-input metadata so later builds can recover the closure without source files. Incomplete metadata fails closed rather than claiming an unsafe cache hit. Identity uses content and logical modules rather than absolute checkout paths, preserving reproducibility across roots.

Every program-file entry point honors the selected language

Language dispatch is shared by the compiler and file evaluator. It works through compile-file, sigil eval -f, direct and shebang execution, load, tests, module source fallback, package builds, bundles, native builds, and both WebAssembly configurations. String and expression interfaces, including the REPL and eval-string, remain standard Sigil by design.

Reader failures preserve caller state, and recognized languages never trigger an installer or network fallback. The matrix covers multiple readers, failure followed by ordinary source, forced garbage collection, and every supported backend.

Generated syntax retains manuscript provenance

Source locations can now carry a bounded generated-origin chain. A language may locate a generated form in its own synthetic source while retaining the authoritative outer manuscript span that caused it. Diagnostics report the manuscript location first, and origin chains survive garbage collection and bytecode serialization.

The syntax library also gains a positioned reader with exact UTF-8 byte offsets, one-based lines and columns, spans, and a caller-owned cursor. Languages can delegate embedded Sigil fragments to it instead of maintaining another Scheme lexer.

Tooling can inspect a language without executing it

The new (sigil language) module exposes pure header inspection and reader availability discovery without loading reader code. Explicit activation separately validates declared capabilities before enabling formatter or document-analysis behavior.

This separation lets editors safely recognize an alternate-language buffer, show its language identity, and ask for trust without running author code on open. Persistent approval can be tied to the exact resolved reader artifact, so changing that artifact revokes the approval rather than silently extending it to new code.

Release builds reject dependency drift

Dependency repositories and worktrees now follow SIGIL_HOME, with ~/.sigil retained as the ordinary default. This gives release and CI builds a real private dependency store instead of allowing an ambient shared worktree to change beneath a locked build.

The bootstrap installer now honors an existing lockfile instead of resolving the latest matching tag. The release procedure also checks every external worktree against its exact committed sigil.lock revision before compilation. A rewritten lock or a missing, dirty, or mismatched checkout stops the build before artifacts are produced.

0.18.3

A toolchain correctness release. Sigil now resolves its runtime packages coherently, builds test suites without manufacturing dependency cycles, runs every discovered test file exactly once and reports a verdict for each one, and recompiles edits even when they land within the same second. Native builds also improve substantially: cross-module optimization summaries are active and bound to the exact compiler and bytecode they describe, native WebAssembly output is smaller and self-contained, and projects can grow past the old 256-module ceiling.

Most projects build without source changes. Native caches rebuild once because the internal runtime ABI moves from version 3 to version 5. A build that supplies a backend flag contradicting its selected configuration now stops with a clear error instead of producing an artifact whose kind disagrees with the package declaration.

Dependency resolution is order-independent and keeps the runtime together

The order of dependencies in package.sgl could change the result of sigil deps install. A transitive dependency resolved first could also silently hold sigil-lib on an older line while sigil-stdlib moved forward, producing a lockfile that installed successfully and failed later while loading.

Resolution is now order-independent for these cases, and sigil-lib and sigil-stdlib always come from the same Sigil worktree. The rule is deliberately narrow: independent packages from the Sigil monorepo can still use different compatible release lines. When an ordinary advisory constraint loses to a version already selected, the resolver continues as before but now warns with the discarded range, the version that won, and the source of the losing constraint. Set SIGIL_DEPS_TRACE_DROPPED=1 for the complete constraint trace.

--tests builds model libraries and test products separately

On 0.18.2, sigil build --tests could stop immediately with Dependency cycle detected. The graph collapsed a package library and its tests into one node, manufacturing a cycle when a package's tests depended on a test runner that ultimately used the package's library.

Libraries and tests are now distinct build products in the dependency graph. Real development-dependency cycles are still detected, while ordinary test harnesses build correctly. This fixes both sigil build <package> --tests and the native test-harness path used by sigil test. Graph failures also distinguish a real cycle from a missing dependency or duplicate node instead of blaming every malformed graph on a cycle.

sigil test runs each discovered file once and tells you what happened

Several runner defects could make a suite look healthy without running the suite: an exit inside one file terminated the whole process, nested dependency directories could trap discovery in a symlink cycle, workspace and explicit discovery could execute the same file twice, and duplicate basenames made the file ledger ambiguous. Test files also shared one module, so a macro defined by one file could poison a later file depending on filesystem enumeration order.

Each test file now loads in its own module and inside an exit trap. A load-time exit becomes that file's load error, a run-time exit becomes a failed test, and the remaining files continue. Discovery skips dependency stores at every depth, rejects ancestor symlink cycles, deduplicates the final file set, and labels every result with its path. Empty files receive an explicit EMPTY verdict, and a truncated run reports how many planned files never reached a verdict. The summary, ledger, and exit status now describe the same execution.

Source edits within the same second are no longer missed

The auto-compiler and incremental package builder compared timestamps only to whole-second precision. A quick edit could therefore leave older bytecode classified as current and execute the previous program. Freshness checks now use nanosecond timestamps throughout both paths, so an edited source always recompiles even when its timestamp shares the same second with the cached output.

Cross-module optimization summaries are active and provenance-bound

Auto-compiled bytecode now publishes its .sgs optimization summary beside the final .sgb instead of leaving the summary under a temporary orphan name. The compiler can use those summaries for cross-compilation-unit bounds-check elimination, with a safe per-unit fallback whenever a summary is absent or rejected.

An SGS v2 summary is accepted only when it has a matching sibling bytecode file and its format, bytecode SHA-256, compiler SHA-256, and payload-integrity digest all validate. Summary discovery is restricted to compiler-owned cache and output directories, never ordinary library or package source paths. This prevents stale, copied, corrupted, or source-tree summaries from supplying facts to a different compilation.

Optimizer settings are keyed, and unsafe loop hoists stay disabled

The selected CPS optimization level now participates in build-cache identity, so changing optimization settings cannot reuse bytecode built under another level. Ambient optimizer controls are cleared from ordinary builds unless explicitly represented by the builder.

This release also closes a latent LICM miscompile exposed when optimization first ran in production: a potentially raising loop-invariant expression could move ahead of the guard that made it safe, turning handled input into an unconditional error. The speculative rewrite now admits only explicitly total, non-allocating operations and refuses captured-cell dereferences without a no-write proof. The pass remains deliberately conservative where the compiler cannot establish those facts.

Native WebAssembly builds are smaller and self-contained

Native WebAssembly output now runs Binaryen -Oz after Asyncify in a separate, ordered step, validates the optimized file before replacing the original, and fails loudly if optimization produces no valid module. In measured production consumers this reduced Enclave from 16.9 MB to 14.2 MB and Slate from 39.8 MB to 31.3 MB, with corresponding gzip reductions of 0.9 MB and 2.9 MB.

Native configurations no longer stage the bytecode library tree, lib.bundle, or bytecode manifest beside a self-contained WASM artifact. Bytecode configurations retain their existing staging behavior. The native-WASM standard-library entrypoint also no longer resolves a host-only wait capability while compiling for the browser.

Backend declarations are enforced

A configuration's declared backend is now an artifact-kind contract. If a CLI --backend option or inherited SIGIL_BACKEND value contradicts that declaration, the build exits before compilation and names both selections. Matching flags remain accepted, and projects that do not override the declared backend are unchanged.

The VM module registry grows beyond 256 modules

The fixed 256-entry module registry is now a growable heap-backed registry. Large workspaces and split applications can register thousands of modules without degrading into a misleading library not found failure after the old ceiling. The expanded registry remains a garbage-collector root set, including entries beyond the original boundary.

This changes the internal native ABI from version 3 in 0.18.2 to version 5 in 0.18.3. Native archives and compiler caches rebuild once on upgrade; bytecode source compatibility is unchanged.

Pipe-spawned processes can own a process group

process-spawn gains new-process-group: #t. On Unix the child starts a new session before exec, so process-signal! can signal the child and everything it spawned while stdout and stderr remain separate pipes. process-pgid returns the live process-group id before the child is reaped. Default spawn behavior is unchanged, and the option is a no-op on Windows where group signalling is unavailable.

Macro failures no longer crash the runtime

A macro that raised while expanding the first element of a list could make the expander write through the empty-list immediate. Instrumented builds reported a misaligned write; release builds could segfault with no useful diagnostic. Expansion now returns through the existing error path without touching an uninitialized list tail, so the original macro error reaches the caller normally.

Additive WebAssembly DOM integrity query

The browser DOM bridge gains wasm-dom-child-at? and dom-child-at?. They check child identity and exact list boundaries without mutating the DOM. This is the primitive used by sigil-web-client 0.15.4 to detect out-of-band DOM drift and return #f before a retained-tree update can falsely report success.

0.18.2

A runtime correctness release, plus a headline feature: Sigil now has first-class shell scripting support via (sigil shell). On the correctness side, four distinct defects in exception dispatch are fixed, the most serious Sigil has shipped: a raise could remove the wrong handler, corrupt a live stack frame, or end the process with exit code 0 while work was still pending. Nine subcommand paths that used to die silently now fail loudly, and scoped package ids resolve local paths correctly. Anything long-lived built on 0.17.x (a server, an MCP tool, a CI controller) should be rebuilt on this release; consumers on a 0.18 caret range pick it up on relock.

First-class shell scripting with (sigil shell)

Sigil is now a real alternative to bash for the scripts around your project. (sigil shell) runs external programs with structured command forms instead of concatenated strings, returns outcomes you can match on instead of exit codes you remember to check, and pumps pipelines through the VM so your script can sit between two processes. A script declares the executables it requires up front and fails closed immediately when one is missing. And a single file with #!/usr/bin/env sigil now runs directly with a preloaded set of useful modules, so automation no longer needs a package around it.

A more flexible package build format

The package format can now describe how software actually gets built, not just how Sigil code compiles. A package declares run phases (arbitrary build steps that run in order and fail the build honestly when one fails), the build inputs those steps consume, and the environment they require, each verified by its own gate. That makes packaging software with its own build process straightforward: a configure step, generated code, bundled assets. The release build itself now declares the tools it depends on and fails in seconds when one is absent instead of late and confusingly.

A raise after a caught exception no longer escapes the enclosing guard

When a guard caught a raise, dispatch popped one handler too many, so a later raise escaped the guard that should have caught it. Ten lines of plain synchronous code reproduce it, and on 0.17.x and 0.18.0 the escaped raise ended the process with exit code 0; 0.18.1 at least exited 1. A fired handler's entry now stays on the stack until its own continuation retires it, on every dispatch path, including a native-compiled function raising under a bytecode guard.

A handler that suspends no longer corrupts the program

A guard handler that suspended (a sleep, blocking I/O, a channel operation) could resume into a corrupted machine: one defect overwrote a live frame's saved instruction offset, so execution continued into foreign bytecode; another delivered a tail-position handler's result as if the raising call had returned, so the body resumed from the raise site. Handlers now run as ordinary frames in the same dispatch loop, so return delivery is correct whether or not the handler suspended.

Native builds keep their handlers across fiber interleaving

The native backend tracked handlers with absolute indexes, but capturing a continuation across a suspension relocates handler entries without moving that bookkeeping. One rebasing suspension left the runtime convinced an unwind was still in progress, and every later native call silently short-circuited: a process that dies with nothing in the log. Handler entries now travel inside the captured continuation and re-push rebased on resume. This is the defect that killed Kiln's static controller on 7 of 9 setup-failure cases; the same Kiln code on this runtime passes 9 of 9, and the runtime was the only variable in the pair.

One narrow, pre-existing case remains open and is tracked: a native-compiled function that raises more than one bytecode frame below its enclosing guard may still resume the intermediate frame.

Nine silent exit doors now fail loudly

Nine subcommand paths used to end the process silently when something raised: deps install, update, and upgrade, task and buildtask raises under run, and the four extb doors. All nine now exit 1 with a real error trace. Most were the guard defect seen from the outside: the subcommand's handler fired, the enclosing guard in main was already gone, and the process fell off the end reporting success.

Scoped package ids resolve local paths

A scoped package id (the sigil/name form the registry uses) broke local path resolution and died with exit 0 when it failed. Scoped ids now resolve correctly, and a failure on that path exits loudly with the reason.

0.18.1

Every way into the Sigil CLI now reports failure honestly. Before this release a mistyped command, a script that raised, and a refused operation could all finish with exit status 0 and print nothing, which meant a CI step could report success for work it never did. Existing projects build without changes.

Fixed

  • sigil <unknown-command> exited 0 and printed nothing. A typo in a CI step reported success indefinitely. It now prints the unknown command on stderr and exits non-zero.
  • Running a file directly, as sigil script.sgl or through a #! line, swallowed errors: execution stopped at the failing form, nothing was reported, and the exit status was 0. Errors now reach stderr and the exit status reflects them. This is the path a shebang uses, so scripts that appeared to succeed may now correctly fail.
  • sigil env show, sigil store, sigil app and sigil pkg could print a refusal and then carry on with the refused operation. Four modules called exit without importing it, and on release builds the resulting error was discarded.
  • sigil bundle <missing-file> exited 0 without a diagnostic. It now checks its input before starting.
  • Unknown subcommands under sigil channel, sigil registry and sigil project printed help and exited 0. Parse errors went to stdout rather than stderr, and a lone - argument was silently dropped instead of being treated as a positional.

Added

  • An exit-contract gate covering every entry point into the CLI, 43 checks, each verified in both directions: the check fails when the defect is reintroduced and passes when it is not. The gate runs on release binaries, not only development builds, because several of these defects were visible only in release builds.

0.18.0

The release that gives Sigil a verifiable supply chain. sigil env builds a project's whole toolchain from a lockfile, fetches prebuilt outputs from a signed registry instead of compiling them, and refuses anything whose bytes don't match what was signed. Alongside it, two build-integrity fixes you can see from the outside: builds that failed while reporting success now exit non-zero, and the same source built twice now produces the same binary. This is a minor bump because (sigil env) changed meaning. It's the environment engine now, and the old dotenv helpers moved to (sigil dotenv). Projects importing (sigil env) for dotenv need that one-line change; everything else builds without modification.

Environments from a lockfile

sigil env builds and enters a pinned toolchain. A project records its environment in env.lock, which pins every output by the hash of its contents rather than by a version number, so entering that environment on another machine gets the same bytes or fails loudly. sigil env shell drops you into it and passes back the exit status of whatever you ran, so it composes in scripts.

A signed registry, and substitutes instead of compiling

Sigil can fetch a prebuilt output rather than building it. Outputs live in a content-addressed registry, published as ordinary static files, so you can mirror the whole thing with rclone and serve it yourself. The client verifies a signature over the exact bytes it was served, then the archive's hash, then the hash of the unpacked tree, before anything reaches the store. A tampered byte, a wrong path, a replayed old index, or a stale key all refuse rather than degrade.

Trust starts from a single public key compiled into the client. That key signs a registry document listing the operational keys, each scoped to a role, so a compromised publishing key can be revoked without reissuing the client. Retired keys keep verifying documents they signed before retirement and refuse anything newer.

Channels

sigil channel publish writes a signed catalogue and snapshot describing a set of packages, in one transaction. It verifies the staged result before touching anything mutable and rolls back every root if a write fails. Publishing the same content twice is a no-op rather than a new revision.

Builds that fail now say so

A build task that raised an error could print ✓ Build complete! and exit 0, having produced nothing. A build that doesn't run to completion now refuses to exit 0 and says which stage was interrupted. This was reachable from several places, including a failed libsigil.a archive step and a package whose manifest fails validation, and every one of them reported success.

If you script around sigil build and have been checking output text because the exit code was unreliable, you can go back to checking the exit code.

The same source builds the same binary

Two consecutive builds of unchanged source could produce different binaries. Two builders wrote the same libsigil.a, and the cache fingerprinted the inputs rather than the bytes on disk, so the second build served an archive the first had overwritten. Archives are now fingerprinted by content, and a cross-environment gate builds the same commit on two distributions and compares bytes.

Known limitations

Error propagation itself isn't repaired. In a native release build, a short-lived guard during startup overwrites the top-level error handler, so from then on an error that no inner guard catches is swallowed and execution continues. That's why a failing build could report success, and why a few unrelated commands could too.

0.18.0 contains this at the sites we know about, using checks that verify a result directly instead of waiting for an exception to arrive. It doesn't fix the cause, which needs a redesign of how the handler stack is reclaimed. Every cheap patch we tried breaks something else, so we'd rather ship the containment and do the repair properly. The good news is that it's finally understood rather than merely observed.

The build latch catches builds that didn't finish. It doesn't catch a build that finished and produced the wrong artifact, and sigil check staying quiet on a syntax error is a known instance still open.

What's next for sigil env

The machinery here is complete and verified, and there's nothing to point it at yet. No substitute registry is deployed and no core package channel is published, so sigil env today builds what you pin rather than fetching it.

That's the intended shape of this release rather than a shortfall. Building the registry and the channel needs a client that can already consume them, a signing chain that can already be trusted, and formats settled enough to publish against. 0.18.0 brings all three online. Next is standing up the registry itself and publishing the core packages into it, and that work can now proceed against a client in the field instead of alongside one still being designed.

0.17.20

A release that hardens the Sigil build. Three build-integrity improvements make a build's output verifiable: compiling the same source in two different directories now produces byte-identical bytecode, the build cache keys its entries by a canonical encoding so it can never hand back the wrong bytes, and the build store re-verifies every artifact it serves. Existing projects build without changes; the build cache rebuilds once on upgrade, since the bytecode and cache-key formats changed.

Bytecode records package-root-relative source paths

Compiled bytecode used to embed the absolute path of every source location, so the same code built in two different directories produced different .sgb. Locations are now recorded relative to the package root, so two clean builds of the same source are byte-for-byte identical. Error messages and source snippets are unchanged, and the only effect on upgrade is a one-time cache rebuild.

The build cache keys builds by a canonical serialization

The build cache reuses an earlier build by hashing its inputs into a key. That key used to come from formatted strings, where two different inputs could produce the same text, so the cache could hand back bytecode built for something else. Inputs now run through a canonical, injective encoding, so distinct inputs always get distinct keys, and the compiler's environment is folded in too, so an ambient machine setting can't quietly change the output while the key stays the same.

The build store verifies every artifact it serves

The build store keeps each artifact under the hash of its contents, but it used to trust that a stored file still matched, so a truncated or overwritten file could be handed back and used. That is the failure behind the occasional need to wipe the cache and rebuild. The store now re-hashes an artifact as it serves it and treats any mismatch as a miss, quarantining the bad copy, and writes are atomic so a killed build leaves nothing half-written behind. A new sigil store verify command re-checks and repairs a damaged store, so the wipe-and-rebuild workaround is gone.

0.17.19

A responsiveness release with two new capabilities. The headline is a scheduler fix that removes a roughly 100 millisecond stall from interactive apps: typing into a terminal or shell session that Sigil drives, and the output coming back, are now near-instant instead of laggy. Alongside it: pipe-based process primitives for driving a subprocess over ordinary pipes, and a WebAssembly binary lane that lets a wasm app return raw bytes to its host without encoding them to a string. Bytecode projects build without changes; native builds should be rebuilt from clean, since the native module ABI changed.

Interactive apps stop stalling about 100ms per event

An app that waited on a network connection and a terminal or subprocess session at the same time hit a scheduler stall: readiness on one had to wait out a full quantum of about 100 milliseconds blocked on the other. A keystroke's round trip measured about 100ms and an echo about 200ms, felt as constant lag. The scheduler now waits on both at once, so either wakes it immediately: those round trips now measure about half a millisecond, roughly 200x faster. Timers, goroutines, and sleep are unchanged, and the idle scheduler still sleeps at near-zero CPU. (This is separate from 0.17.18's idle-CPU fix: that one cut the work per idle wake, this one removes the lag between an event and the reaction.)

Run a subprocess over pipes, not just a pty

New (sigil process) primitives run a child process with its stdin and stdout on ordinary non-blocking pipes, a companion to the pty-based ones. Some programs refuse to run on a terminal (a tmux control-mode client hard-fails on a tty), and a plain pipe is the transport they need: process-spawn-pipe to launch, process-stdout-fd and process-stdin-fd to wait on readiness, process-pipe-read and process-pipe-write for non-blocking transfer, and 1process-pipe-close-stdin! to signal end of input. Additive and Unix-only; existing spawn behavior is unchanged.

WebAssembly apps return binary data to the host without encoding it

A Sigil program compiled to WebAssembly returned its results to the JavaScript host through a text-only channel, so anything binary (an image, a font, a decoded file) had to be encoded to a string first, slow enough to be visible for a multi-megabyte payload. The channel now carries a bytevector's raw bytes directly, with a flag the host reads to tell bytes from text. Text results are unchanged. A browser-hosted Sigil app can now move an image or other binary file to the page as bytes, decoded in wasm and handed across to become a Blob, instead of paying a per-byte encoding cost.

0.17.18

A Slate preparation release. sigil app install can now install and launch a Sigil GUI desktop application, interface built in, so a graphical app like Slate installs with one command and runs from your application menu or your shell like any other program. Two runtime fixes land alongside it that matter for any long-running Sigil app: a memory leak that slowly grew until the app froze is cured, and idle CPU use drops sharply. Native builds are more trustworthy too: a compile that would once have silently shipped a broken artifact now fails loudly, and a changed source is never silently ignored. A correctness fix also lands for bitwise operations on large integers, which could silently return wrong values. Bytecode projects build without changes; native builds should be rebuilt from clean after upgrading, since the native module ABI changed.

sigil app install installs GUI desktop apps

You can now install a Sigil GUI desktop app with one command. sigil app install <source> builds the app, adds it to your application menu with its icon, and puts it on your PATH, so you launch it from the menu or by name in a terminal like any other program. No more manual guix shell, hand-set LD_LIBRARY_PATH, or hand-written .desktop file. A local checkout works as a source too (sigil app install ./my-app) for offline and dev installs, and update works from any directory afterward.

Installed apps behave well day to day: launches stay fast even after a guix gc or guix pull, a "Starting" notification appears if an app takes a few seconds to warm up so it never looks dead, and app remove is now instant.

Packages can build their assets at install time

A GUI web app needs its built interface embedded in the shipped binary. A package can now declare a before-build step that runs before its native build, so an app like Slate produces its web bundle and bakes it in during sigil app install. The installed app is self-contained, with nothing to stage or serve separately.

A memory leak in long-running apps is fixed

Every code evaluation leaked a small amount of memory that was never reclaimed. This is invisible in ordinary batch use, but a graphical app like Slate evaluates code on every frame and every keystroke, so the leak grew to roughly 40MB a minute, reached 2.6GB, and eventually froze the app after about an hour of active use. Memory use is now flat across a 30,000-evaluation run.

Idle CPU use drops sharply

The async scheduler did far more work than it needed to on every timer wake, which a graphical app's constant polling multiplied into a real cost: Slate burned about half a CPU core just sitting idle. That idle cost is now roughly seven times cheaper, dropping a typical idle workload from about half a core to about a tenth, with no change to how timers, goroutines, or sleep behave.

New async primitive: wait on any of a set of file descriptors

await-readable-fds blocks a goroutine until any file descriptor in a given set becomes readable, or until an optional timeout elapses, then resumes once. It is the raw file descriptor companion to await-readable-any, letting a program wait on a whole set of descriptors through the scheduler instead of a blocking poll, so other goroutines keep running while it waits. This is what a program uses to drive an external event loop, such as a native GUI toolkit's main loop, at near zero idle cost: it sleeps on the loop's poll descriptors and wakes only when something actually happens.

A changed source file is never silently ignored

In some cases a source edit could fail to take effect: the build treated an out-of-date bytecode file as current and shipped a build that quietly lacked the change. The build now checks its cache correctly, so a changed source always recompiles while an unchanged source still reuses its cached result.

Bitwise operations on large integers return correct results

bitwise-and, bitwise-ior, and bitwise-xor could silently return wrong values for large positive integers, first appearing at 2^63: (bitwise-ior 0 (expt 2 63)) came back negative, and a varint-style codec that reassembles a large integer with shift and ior corrupted values like (expt 7 500) on the way back in. No error was raised, so the corruption was easy to miss. All three operations now return correct results across the full integer range, arithmetic-shift and bitwise-not were verified unaffected, and regression tests cover the boundary in both the bytecode and native backends.

Native builds fail loudly instead of shipping something broken

In rare cases the native backend could hit a compile error, produce a broken artifact anyway, and still exit successfully. Because the build looked like it succeeded, the bad artifact was cached and served silently, and the failure surfaced only much later at runtime. A native build now either produces a correct result or stops with a clear error and writes no artifact, so a broken compile can never be silently cached. A nondeterministic miscompile in workspace builds is fixed in the same pass.

Exact float bytes with the IEEE-754 bytevector accessors

Sigil now ships the standard IEEE-754 bytevector accessors: bytevector-ieee-double-ref and bytevector-ieee-double-set! for 8-byte doubles, and bytevector-ieee-single-ref and bytevector-ieee-single-set! for 4-byte floats. They read and write a float's exact bits at a byte offset, bounds-checked, with an optional endianness argument (little, big, or native, defaulting to native). Because they are runtime primitives they behave identically in native and WebAssembly builds, and they are both exact and faster than deriving a float's bits by arithmetic. A library that moves floats through bytes, such as an audio buffer or a binary codec, can now use one exact primitive instead of a C cast that does not exist under WebAssembly or a slower arithmetic workaround. Reading a float from arbitrary bytes always returns a valid number: a bit pattern that would otherwise decode to an invalid, unusable value is normalized to a safe not-a-number, so untrusted input can never smuggle a corrupt value in through a float. Existing projects build without changes.

0.17.17

A native-codegen continuations release. Delimited continuations, coroutines, and generators now work correctly under the native backend compiled to WebAssembly, closing the last place they didn't. A companion fix stops long-running coroutine and async workloads from exhausting the fiber table, macro errors got much more specific, and there's a new experimental opt-in for compiling the standard library to native code. Existing projects build without changes.

Delimited continuations work under native WebAssembly codegen

reset/shift, call-with-prompt, coroutines, and generators now run correctly when compiled through the native backend to WebAssembly. Before this, a continuation that resumed into a loop and then suspended again would either replay the loop from the beginning or trap outright, so anything built on delimited continuations was unreliable on the wasm native target. Desktop native was already fine; this closes the wasm-specific gap, which was the last asterisk on native continuations. Getting here also hardened the asyncify instrumentation the native backend emits, so that machinery is on firmer ground generally.

Coroutines and async workloads stop leaking fiber slots

Every prompt-wrapped resume that suspended again used to strand a fiber slot. After about 60 exchanges, or 60 suspending scheduler slices, the fiber table filled up and continuations broke with a confusing "car: expected pair". This is fixed at the root: an abandoned wrapper fiber is freed the moment its continuation becomes unreachable, and completed fibers are reclaimed immediately in both resume paths. A coroutine loop that used to die around 60 exchanges now runs cleanly through hundreds, and a 100-iteration receive-loop (the shape a long-running message client uses) is a permanent regression test now. Multishot continuations keep working: a continuation invoked after its fiber has completed replays from its own captured state, so its lifetime follows ordinary garbage collection. And a fiber left suspended mid-computation whose continuation is then dropped, never resumed and never completed, is reclaimed too, by a sweep during major garbage collections, so the fiber table cannot fill from abandoned work of any shape.

Macro transformer errors say what actually went wrong

A define-syntax whose transformer isn't callable used to fail with a generic "transformer must be a procedure", which is nearly useless in a large module graph. The error now names the macro, the module it lives in, and the actual value class it found. That one change is what pinned a startup failure in the experimental native-stdlib path below.

Experimental: compiling the standard library to native code

A new opt-in, the SIGIL_NATIVE_DEPS=1 environment variable, compiles dependency modules including the whole standard library to native code inside a bundle, instead of shipping them as bytecode. It is off by default and still experimental. The early numbers are promising: a native-stdlib CLI reaches the compile phase in about 1.6 seconds versus about 9.5 for the full bytecode path. A couple of gaps remain before it can become the default, so treat it as a preview. Default builds are unchanged.

0.17.16

A browser runtime release. Two additive changes to the WASM DOM bridge and the web runtime let downstream web applications reconcile the DOM in place and register native modules of their own, with no change for existing projects. Both are plumbing consumed by the sigil-web-client and sigil-vt packages in their own releases.

DOM primitives for in-place child reconciliation

(sigil browser dom) gains dom-insert-before and dom-remove-child (mirrored in the legacy (sigil wasm dom) alias as wasm-dom-insert-before and wasm-dom-remove-child). Together they let a virtual-DOM library move and remove individual child nodes in place instead of detaching and re-appending an element's whole child list on every render. The first consumer is the sigil-web-client keyed-reconciliation release: interfaces built on it keep DOM node identity across renders, so a focused element stays focused through a re-render and per-render DOM mutation counts fall sharply (a list re-render that previously issued over 100 mutations now issues about 4). Existing web-client code is unaffected until you upgrade sigil-web-client.

A registration hook for app-shipped native modules

The web runtime now weak-calls a sigil_wasm_vt_register hook next to the built-in browser bridges, so an extracted native package can register its primitives into a bytecode web build the same way the first-party bridges do, without the application hand-writing a registration shim. The immediate beneficiary is the new sigil-vt native terminal core. The hook resolves to nothing unless an app links a package that provides it, so every existing build is unchanged.

0.17.15

A string and character performance release. Text operations that used to crawl over long buffers are now native and linear: indexing an ASCII string is roughly 230 times faster, scanning mixed text has a new native cursor, and the bulk string, vector, and bytevector operations are single-pass native code. It also picks up shebang support for executable scripts and two runtime fixes from earlier in the series. Existing projects build without changes.

Indexing ASCII text is fast again

string-ref and substring now take an O(1) fast path when a string is all ASCII, in both the native builtins and the bytecode VM. A scan that touches every character of a 160,000-character ASCII string dropped from 157.7 seconds to 0.68 seconds, about 230 times faster. Syntax highlighters, lexers, and any code that walks a string by index get this with no changes. One caveat: a single non-ASCII character anywhere in a string sends the whole string back to the linear-decode path, so use the new cursor (below) for mixed-content scanning.

A native string cursor for scanning mixed text

(string->cursor str [start [end]]) returns a cursor you can walk with cursor-peek, cursor-next!, cursor-eof?, and cursor-pos. Each step is amortized O(1), decoding one UTF-8 character forward, so a lexer over a buffer full of non-ASCII text stays linear without copying substrings. A scan of an 80,000-character mixed string fell from 35.1 seconds to 0.32 seconds. string-for-each and string-map now iterate through the cursor, so they are linear too (a 40,000-character mixed string-for-each went from 19.9 seconds to 0.58 seconds).

Bulk string, vector, and bytevector operations are native

string-copy, string-copy!, string-fill!, string-join, vector-copy, bytevector-copy, bytevector-copy!, and the list/vector converters are now single-pass native code. string-join in particular was quadratic (joining 16,000 parts took 8.5 seconds); it is now a native single-pass builder. string->vector on a one-million-character ASCII string dropped from 2.36 seconds to 0.13 seconds.

Two correctness improvements come with the sweep: string-copy! and bytevector-copy! now have memmove overlap semantics, so a self-copy that shifts a region to the right no longer corrupts the data (the old forward loops did). And string-copy! validates the whole destination range before it writes, so a bad range fails cleanly instead of writing part of the data and then erroring mid-loop.

Native vector mutation

vector-copy!, vector-fill!, list->vector, and vector->list are now native, continuing the bulk-operation work from the string sweep on the vector side.

Async continuations survive suspension in nested native frames

A suspending operation reached through a frame that a native procedure had called could lose its continuation, so the work after the suspension silently never ran. The VM now preserves the continuation across that boundary, so async code behaves the same whether or not a native frame sits in the call stack.

Scripts can start with a shebang line

A leading #! line is now skipped when Sigil reads a whole file (file evaluation, load, and eval-string), so you can make a Sigil script executable with #!/usr/bin/env sigil at the top. A #! anywhere other than the first line is still a syntax error, and source locations stay correct (the content reports its real line numbers).

0.17.14

A runtime and FFI release. The async scheduler stops oversleeping ready tasks, so timers and event loops stay responsive under load. The dynamic FFI gains native pseudo-terminals and wider callbacks, so Sigil can drive a real shell and take native signals with any number of arguments. And a fiber-corruption bug in native callbacks is fixed. Existing projects build without changes.

The async scheduler no longer oversleeps a ready task

A task that became runnable could sit idle while the scheduler slept on to its next deadline. The worst case was a fast periodic loop among slower ones: a 5ms timer sharing the scheduler with 50, 100, and 200ms goroutines dropped from about 190 wakeups a second to 40, with a tail near 100ms. A socket or pipe waiter with no nearer timer had the same problem, left up to a full poll interval (100ms) behind its own readiness. The scheduler now checks for a runnable task before it sleeps and runs it at once: in a socket repro, waiter latency fell from about 98ms to under 1ms. Anything mixing a fast loop or an event-driven server with background work stays responsive under load.

(sigil process) can spawn a process on a pseudo-terminal

You can now start a child process on a real pseudo-terminal instead of pipes. A new pty-aware spawn hands back the master file descriptor and the child's pid; from Sigil you stream the master as an async fd, resize the terminal, signal the process group, and reap the child. This is what a terminal emulator needs: a shell, vim, or htop runs against a real tty and behaves exactly as it would in your terminal, its output streaming back for a Sigil front end to render. All of the fork-time setup stays on the C side, so nothing forks from Scheme.

Wider native callbacks with sigil_applyN

Native code can now call a Sigil procedure with any number of arguments, through a new sigil_applyN in the VM C-API alongside the fixed-arity sigil_apply0 through sigil_apply3. The dynamic FFI uses it to carry native signals with four or more arguments, such as a GTK or WebKit navigation-policy callback, back into Sigil, which the old three-argument ceiling blocked.

FFI callbacks into Sigil are now reliable

A callback invoked from C, or any Sigil code re-entered from native code, could be corrupted if it suspended or was preempted at the wrong moment: its fiber was lost and the program could misbehave or crash. The VM now shields a native re-entry from preemption, so a callback keeps its fiber intact even when it yields. Dynamic-FFI callbacks that call back into Sigil, the kind a GTK or WebKit signal delivers, are now dependable.

0.17.13

A short follow-up to 0.17.12. sigil app install installs again, now building the app as a native binary the way a release build does, and the browser WebSocket layer can tell a still-connecting socket from a closed one. Under the hood the async runtime is more robust under concurrency: a busy goroutine no longer starves I/O, and channel wakeups no longer deadlock a long-running program. Existing projects build without changes.

sigil app install builds and installs a native binary

sigil app install, and sigil app update, had stopped installing anything. The command compiled the app and then exited without writing a binary, registering it, or reporting an error, leaving you on the previous version with no sign anything went wrong. It now builds the app exactly as its release build does, with build --config release, and installs that native binary. A failed install is loud: a real error and a non-zero exit, never a silent success.

Installing from a workspace with more than one package is now explicit. --package selects the package to install; without it, the command uses the workspace's declared default app, or the sole package that has an entry point, and otherwise lists the candidates and asks you to choose rather than guessing. The standalone sigil bundle command, whose bundling path this release stops relying on for app installs, now also fails loud instead of exiting silently.

A connecting WebSocket is no longer mistaken for a closed one

sigil-wasm-net exposes a readyState accessor for a browser WebSocket, so code can tell a socket that is still opening from one that has closed. A browser WebSocket opens asynchronously, and until now a poll during that opening window looked the same as a closed connection, so a client could give up on its very first poll and never recover, a failure that surfaced only over real network latency and never on localhost. With the distinction available, sigil-websocket can report "no data yet" instead of "closed" while a socket is still connecting.

Async I/O no longer starves under a busy goroutine

When one goroutine stayed busy, the async scheduler could starve everything else: pending socket reads, pipe drains, and subprocess waits would stall. Under load that meant an async server could stop answering, or a build could hang waiting on a subprocess that had already finished. The scheduler now keeps I/O moving even when a goroutine is hot, so servers stay responsive and builds don't stall on work that's already done.

Channels no longer deadlock when waking a blocked peer

Channels could deadlock a program. When a send, a receive, or a channel close woke a peer that had been waiting, that peer could get stranded and never run again, freezing the whole program at zero CPU with no error to point at. It was rare in short runs, but a long-running program moving data across channels could hit it under load. Those wakeups are now reliable. Two smaller channel fixes ride along: a value you send is never confused with an internal scheduler marker, and a buffered channel delivers messages in the order they were sent.

A musl build finds its toolchain, or stops

sigil-build now detects a musl cross-toolchain by scanning PATH directly instead of caching a first look that could go stale inside a nested shell, so a toolchain: musl build reliably finds musl-gcc when it is present. When it falls back to guix to supply the toolchain, it does so in a clean, isolated environment, so the musl objects come out the same regardless of the surrounding shell. And when no musl compiler is found at all, the build fails with a clear error instead of quietly compiling against the host's glibc and producing a binary that only looks static. That stricter behavior is deliberate: a musl build that cannot find a musl toolchain should stop, not silently fall back.

0.17.12

A records-and-methods release, with a native-async correctness fix that was silently breaking MCP servers. Two new core libraries, (sigil record) and (sigil method), bring extensible typed records and cached multimethod dispatch to the standard library, and a compiler fix lets procedural macros export across module boundaries at last. Alongside them, browser programs can now route DOM events straight into Sigil, a native-backend regression that dropped an async server's response mid-request is fixed, and two toolchain footguns are gone. Existing projects build without changes.

(sigil record): extensible typed records

(sigil record) adds define-record, a dict-backed record with a unique type identity, declared fields, a constructor, a predicate, and per-field accessors, plus single inheritance through include:. Records are the open, extensible counterpart to (sigil struct)'s fixed-slot types: a record is a dict underneath, so it carries arbitrary namespaced keys alongside its declared fields, updates immutably while preserving its type, and datafies carrying its own type tag. The tag is deliberately visible, so a record serializes and inspects as what it is, and a plain dict is never mistaken for one. A derived type declared with include: inherits its parent's fields and answers the parent's predicate.

(sigil method): multimethods with cached dispatch

(sigil method) adds define-multi and define-method, open dispatch over records, structs, or core values through a generic type walk. A method registers from any module, so a package can add a type and its behavior without touching the code that dispatches on it, and a base type's method serves as the inherited default up the parent chain, with a :default fallback. Dispatch is cached for hot paths: a per-multimethod memo backed by a monomorphic fast path, invalidated on every method (re)definition, so redefining a method at the REPL always takes effect. One primitive covers both protocol-style dispatch on type and multimethod-style dispatch on data.

Procedural macros export across modules

A procedural macro (a define-syntax transformer, including syntax-case) now exports from a define-library and works in an importing module exactly like a syntax-rules macro does. Until now it silently did not, so a macro that synthesizes identifiers, the kind syntax-rules cannot write, could not be shared across modules as a library. This is what lets (sigil record) ship its generated accessors as an ordinary library, and it fixes procedural-macro export for every package.

DOM events into Sigil, in the browser

(sigil web client) can now wire a DOM event straight to a Sigil closure. An sxml attribute of the form on-click:, on-input:, or on-key: whose value is a procedure is registered as a real event listener through a new addEventListener bridge in sigil-wasm-dom, and the closure is called with the event's useful properties: key, target value, caret, and the keyboard modifiers (ctrl, meta, alt, shift). Browser UI event handling can now live in Sigil instead of hand-written JavaScript.

A native-async fix that was silencing MCP servers

An MCP server built against 0.17.11 answered its initialize request and then went quiet on tools/list: it exposed no tools, logged no error, and was useless. This release fixes it. The bug lived in the native backend's async runtime, where a background task interrupted at the wrong moment could drop the response it was in the middle of building. Any native-compiled async server is affected, an MCP server or an HTTP server running handlers on goroutines, and now completes its request correctly. If you build on the native backend, upgrade to pick this up.

Toolchain fixes

  • sigil cli install no longer bricks itself. Reinstalling over the currently-running binary could leave both the versioned binary and its active copy truncated to zero bytes. The install now writes to a temporary path and renames atomically, and refuses to overwrite the executable it is running from.
  • scripts/bootstrap.sh runs. It invoked a bootstrap/Makefile that does not exist; it now invokes the top-level Makefile.

0.17.11

The graphics-and-audio-on-the-web release. A Sigil program that draws with (sigil graphics) and plays sound through (sigil audio sink) now compiles to native WebAssembly and runs in a browser, with no Emscripten anywhere in the toolchain and no compile-C-directly workarounds in the build. This is the foundation the Cinder Cadence web port stands on: the full music-reactive game, native-compiled to a single WebAssembly module, rendering and playing in the browser. Two native-backend correctness fixes round out the release. Existing projects build without changes.

Graphics in the browser through your own GL binding

The new sigil-wasm-gles3 package renders reused sokol_gfx in the browser through a raw WebGL2 binding, with zero Emscripten. It exposes the full set of gl* entry points sokol's GLES3 backend calls (around 122 of them) as a WebAssembly import module, backed by a WebGL2 context and integer-to-object handle tables on the JavaScript side. The surface is complete by construction: real implementations for the entry points WebGL2 actually uses, plus warn-once stubs filling the rest, so the wasm always instantiates.

On top of the binding sits an app-shell, the browser counterpart to sokol_app. A Sigil graphics program drives a render loop (gles3-use-canvas, resize-to-display, start-loop / stop-loop) and loads textures over the network with an async image fetch (gles3-fetch-image), decoding the fetched bytes with stb_image through (sigil graphics image), no filesystem required. The import surface of a built web app stays gl.* plus wasi plus the binding's own module, nothing else.

Audio in the browser

sigil-wasm-audio gains an AudioWorkletNode path: streaming audio now drains on the audio thread over a SharedArrayBuffer when the page is cross-origin isolated, and degrades to a ScriptProcessorNode over a plain ArrayBuffer when it is not. Both paths share one atomic ring buffer, so the sink's synchronous per-frame backpressure behaves identically either way.

For sound effects there is play-audio-sample, a fire-and-forget one-shot that reads a short PCM bytevector and plays it through a WebAudio buffer source. WebAudio mixes it with the streaming music and any other concurrent one-shots, and reclaims the node when it ends, so overlapping effects like rapid fire and hits need no mixing on the Sigil side. The platform does it.

First-class web builds

Dependencies can now be gated by build config. A configs: field on a from-git, from-path, or from-workspace dependency names the configs it applies to; a dependency whose list omits the active config is dropped from the build. That makes sigil build --config web first-class: a native-only C dependency like sigil-app (whose SOKOL_GLCORE and X11 sources cannot build for wasm32-wasi) is simply excluded from a web build rather than worked around. Fetching a dependency's source stays config-agnostic, so deps install is unaffected; the filtering happens in the build and link path where the active config is known.

Native-backend correctness

Two fixes for the native backend, both surfaced by real programs:

  • Aborting to a prompt no longer corrupts a neighboring frame's registers. The 0.17.9 preemptive-yield work parked an abort's tag and values at the current frame's register-window boundary, which was unsound: on the bridged path a call return between the abort and its relay could leave that pointer aimed at the wrong register file, and consumed values could linger as stack residue exactly where an overlapping register window places a later frame's registers. The symptom was a "call: not a procedure" crash when a second async session reused a slot the first session's abort had left dirty. Abort values now travel out of band, off the value stack entirely.
  • A suspended fiber's exception handlers no longer catch raises from other code. A native call-with-prompt body runs on a fiber and pushes no bytecode frames, so guard handlers it installed lingered on the shared handler stack while the fiber was suspended, and a raise from anywhere else (the prompt handler, or code that ran after the prompt returned) could be caught by the suspended fiber's guard. Native fibers now save and restore their own slice of the handler stack, mirroring the existing per-fiber treatment of dynamic-wind winders.

0.17.10

A live-redefinition and interactive-correctness release. Native-compiled programs can now pick up redefinitions while they run, your editor can attach to an already-running program and evaluate straight into its modules, and a cluster of macro, module-resolution, and exception-cleanup bugs that surfaced during live-coding are fixed. Existing projects keep building without changes.

Redefine code in a running native binary

Native-compiled code can now be redefined while it runs. Until now, a define-library body compiled its defines to lexical cells, but the bindings table kept a separate snapshot cell per binding, so evaluation (through the REPL or nREPL) and compiled code operated on different storage. Redefining a binding in a running native binary had no effect, at any optimization level. Those cells are now unified: a captured define installs its lexical cell directly as the binding cell, so an evaluated redefinition reaches every compiled call site on its next call. This is the foundation for live-coding into a running native program, a game loop or a web server, and having your edits take effect without a restart.

This also closes a native-backend use-after-free that shipped in 0.17.8: the generated code cached a binding's storage location inline, and after a redefinition replaced and freed the old cell, a cached pointer could dereference freed memory. A version counter now stamps every native binding cache and bumps on each redefinition, so a stale entry re-resolves instead of reading a freed cell. The same counter is what lets a redefinition propagate to native call sites at all, bringing the native backend to parity with the register VM.

Attach your editor to a running program

sigil-nrepl-attach connects Emacs to an nREPL server that is already running, one embedded in your game or web server, without spawning a new one. Previously the editor could only drive a server it had started itself. Alongside it, sigil-send-region and sigil-send-definition now evaluate in the source buffer's own module rather than the session's default, so a redefinition lands where the running program can see it, across module boundaries, with no manual module switch. Paired with the native live-redefinition work above, you can edit a definition in your buffer and watch a running native program pick it up.

Live-coding correctness

Four fixes for bugs that surfaced while live-coding during streams:

  • Nested evaluation no longer crashes or hangs the host. A preemptive yield (added in 0.17.9) could fire in the middle of a nested eval, compile, or macro-expansion and abort into C code that was not prepared to be unwound, taking down a long-running server. Because the process kept running elsewhere, it looked like a hang rather than a crash. Preemptive yields are now held behind a barrier so these nested extents stay atomic and yield only at a safe point.
  • syntax-local-value resolves against the caller's module. Broken since 0.15.0: once module initialization had finished, looking up a macro binding with syntax-local-value from the module that defined it found nothing, because resolution only consulted a context that is set during expansion. It now falls back to the calling frame's module, so the lookup works at ordinary runtime.
  • macroexpand resolves against the caller's module. The same staleness, one function over. (macroexpand '(my-macro ...)) called at runtime from the module that defines my-macro now expands it instead of returning it untouched.
  • dynamic-wind cleanup runs through an intervening prompt. Since the 0.17.5 exception-cleanup hardening, a dynamic-wind after thunk was skipped when an exception unwound through a call-with-prompt that was not the raise's target. The scheduler wraps every task in a prompt, so this silently skipped cleanup in async code whenever an exception crossed a dynamic-wind. Cleanup now runs for every winder between the raise and the handler that catches it, regardless of a delimited prompt in between.

A busy connection no longer starves other sockets

Sigil's async runtime is a single-threaded cooperative scheduler, and it serviced socket and file-descriptor I/O only when its run queue had nothing else ready. That left a gap: a task that stays perpetually ready, for example an HTTP server's connection-drain loop while a browser holds a long-lived connection open, kept the queue busy, so the first read on every other socket waited until the busy task finally let the queue drain. Timer and channel work kept flowing, which is why the symptom was so confusing: a live dashboard's periodic updates ticked along normally while a chat client's first message, an OBS websocket's connect, or an audio stream's startup could hang for thirty seconds to several minutes before their socket was ever polled.

This release adds await-readable-any, a single waiter over a set of sockets that wakes once as soon as any one of them is readable, so a server can wait on its whole socket set through the scheduler in one suspension instead of busy-polling. Paired with the sigil-http release that adopts it (its serve loop now suspends on the socket set rather than spinning), an application that holds a long-lived connection open no longer starves its other sockets: first reads land in milliseconds instead of minutes.

0.17.9

A build-reproducibility and live-evaluation-foundations release. sigil build now produces byte-identical bundles from the same source and toolchain, two new runtime primitives lay the groundwork for interactive and streaming evaluation, and WebAssembly applications can now play audio through the browser. Existing projects keep building without changes.

Reproducible builds

Building the same project twice from the same source and toolchain now produces byte-identical output. Three sources of nondeterminism in the bundling step are fixed:

  • The appended bundle archive stamped every entry with the wall-clock time at build, so two builds minutes apart differed in the timestamp field of every bundled file. Bundle entries now carry a fixed timestamp.
  • Files were added to the bundle in filesystem-directory order, which varies from one machine to another. Bundle entries and the module manifest are now sorted by name.
  • Object files were linked, and library-archive members ordered, in filesystem-directory order too, so a binary's section layout could differ across machines even when nothing else changed. Both are now sorted by name.

The payoff is verifiable builds: a release built on one machine and rebuilt on another, or on your own CI, can be compared byte-for-byte to confirm the published binary corresponds to its source.

Foundations for live evaluation

Two lower-level primitives land in support of interactive and streaming code evaluation, the machinery behind the ongoing nREPL live-eval work:

  • Custom output ports. You can now create an output port backed by your own procedure, so everything written to it, including writes from native code, is delivered to your callback instead of to a file or a string. A host can capture or stream a program's output as it is produced.
  • Preemptive yield in the register VM. A running evaluation can now yield cooperatively at safe points, so a host can interleave or interrupt long-running work instead of waiting for it to finish.

Audio in the browser

A new sigil-wasm-audio bridge package provides a Web Audio backend for the (sigil audio sink) facade. An application that streams audio through that facade now runs in the browser as well as on native, playing through the browser's Web Audio API. This pairs with sigil-audio 0.12.0, whose portable facade selects the native sokol backend or the WebAssembly bridge with cond-expand.

0.17.8

A WebAssembly and build-reliability release. Existing projects keep building without changes, with one action item for custom loaders (below).

One dispatch entry on both backends

Bytecode web builds now export sigil_wasm_dispatch_event(type, payload), the typed entry native builds already had. JavaScript hands the app two strings and the app's sigil-web-dispatch-event handler receives them as string values: no Scheme source to build, no escaping to get wrong. A string result comes back through sigil_wasm_last_result_length / sigil_wasm_copy_result, and a handler that suspends on async work returns -2 and resumes via sigil_wasm_resume_async, the same flow the eval path uses.

Action needed for custom loaders: both backends now export the dispatch entry, so its presence no longer identifies a native build. Detect native via the sigil_wasm_native_start export instead. The stock loader is already updated; a loader still keying on sigil_wasm_dispatch_event will misdetect bytecode builds as native and fail to load their /lib files.

Build-cache reliability

Two fixes close the failure where builds run under different toolchain environments (two different guix shells, say) could poison each other's cached C objects and end in a silent, empty .wasm. The C-object cache now fingerprints the compiler's full toolchain environment, so different toolchains never share cache entries. Expect a one-time recompile of cached C objects after upgrading. And the wasm asyncify step now validates its output and fails the build loudly instead of leaving behind an empty module that a later build could mistake for already built.

The unbound-variable crash on the release-mode error path, fixed in 0.17.7, has been re-verified and its regression test is back in the suite.

0.17.7

A native-codegen correctness release. Native compilation now resolves module bindings the way the bytecode interpreter always has: a re-export through a cond-expand import, and a nested eval that begins while another eval is still on the stack, both used to fail only under native compilation and now work. Native WebAssembly builds export the full eval ABI again, and a build bug that could silently link stale object files into a mixed-ABI binary after a header change is fixed. Existing projects keep building without changes.

Facade re-exports resolve under native compilation

A library that re-exports a name it imports from a sibling, where that import sits inside a cond-expand branch (the usual shape for a platform facade that pulls in a different backend on WebAssembly than on native), used to bind only its own definitions under native compilation. The re-exported names came back unbound, on both eval and compiled cross-module calls, while the same code resolved correctly on bytecode. The cause was in how the native build computes a module's dependencies: it read only top-level import clauses and never looked inside cond-expand, so a facade whose import lived in a cond-expand WebAssembly branch recorded no dependency edge, and its module initializer ran before the module it re-exports from was ready. The dependency scan now descends into cond-expand, selecting the branch that matches the build's feature set the same way the compiler does. A facade re-export chain now resolves identically on bytecode, native desktop, and native WebAssembly.

A reentrant eval uses its own environment

When one eval was already running and a second eval began before it finished (for example, a browser event handler firing during a render that was itself driven by eval), the inner eval resolved its symbols against the outer, interrupted module rather than the environment it was handed. Bytecode keeps the module context per call frame, so it was never affected; the native backend tracked it in a single global that the interrupted call had left pointing at itself. The native path now pins each eval's environment across reentrancy while still honoring a deliberate eval-in-a-named-module, so a nested eval resolves where it should on every backend.

Native WebAssembly builds export the full eval ABI

Native web builds omitted the five eval-ABI symbols (sigil_wasm_eval and its companions) that the bytecode web build exports, so a native browser application that evaluates code at runtime could not reach the entry point. The native web export list now includes them, matching the bytecode ABI, so the eval surface is the same whichever backend you compile the browser build with.

Incremental builds no longer link stale objects after a header change

sigil build's C-compilation step had a shortcut: if an object file was newer than its .c source, it reused the object without checking anything else. But a C source includes headers, and a header can change without touching the .c file's timestamp. So after an edit to a widely-included header that changed a struct's layout, an incremental build silently reused objects compiled against the old layout and linked them alongside freshly compiled ones, producing a binary with a mixed, inconsistent ABI. The resulting compiler then miscompiled native builds in ways that looked like unrelated runtime failures. The C-compilation step now consults the content-addressed build cache, whose key includes the full header include-closure, before it falls back to the timestamp shortcut, so a header change correctly invalidates every dependent object. The timestamp-only path remains only for builds that have no cache available. As a backstop, a development binary built from a modified working tree now stamps -dirty into its reported version, so a binary that has drifted from its commit says so.

0.17.6

A correctness release. The headline is that the default way to build an application works again: sigil build produced a broken binary for the whole 0.17 series, and that is fixed. Alongside it, the exception-cleanup work started in 0.17.5 is now complete, so dynamic-wind and logging behave correctly even when an exception unwinds through a goroutine boundary; garbage collection gains frame-scheduling and server-heap controls plus a soundness fix in its incremental step; and an internal define-syntax that captures an enclosing binding now works. Existing projects keep building without changes.

The default sigil build produces a runnable app again

sigil build bundles your application by appending its archive onto a base binary, and that base is meant to dispatch to your app's entry point. Since 0.17.1, once the CLI itself became native-compiled, the base ran the CLI instead of your app, so a freshly built app failed at startup with unbound variable 'option' and never ran. Native builds through SIGIL_BACKEND=native were unaffected, which is why it stayed hidden. The generated program entry now checks for a bundled application and runs it, falling back to its own entry only when there is none, so sigil init followed by sigil build produces a working app again. A build-and-run check for a default bundle now runs in the release suite so this cannot slip through again. If you want a lean, optimized standalone binary rather than the bundled form, --native remains the way to get one.

Exception cleanup is now correct across goroutine boundaries

0.17.5 made dynamic-wind cleanup run when an exception unwinds through it. 0.17.6 finishes the harder cases:

  • A caught exception that crosses a yield inside a goroutine now runs the right cleanup thunks, because each fiber carries its own winding state instead of sharing one. This also fixes a separate crash where a dynamic-wind spanning a yield could corrupt the scheduler on normal completion.
  • Writing to an async port (for example, logging) while an exception is unwinding no longer recurses without bound and crashes the process. During unwinding, such a write completes synchronously instead of trying to suspend with no live prompt to suspend to. This protects any async program that logs on an error path, which is most of them.
  • The scheduler now restores its current-scheduler context on the exception exit path, not only on normal exit, which was the underlying cause of the logging recursion above.

Two latent crashes that turned out to be already fixed by this exception-handling family are now covered by regression tests so they cannot return: an idle-tick car crash in long-running servers, and a sigil test deadlock when a test reloads (sigil core).

Garbage collection: frame scheduling, server heap controls, and a soundness fix

Building on the pause and memory observability from 0.17.5:

  • (gc-frame-hint!) runs a minor collection only when the young generation is filling up, so a game loop can end each frame with one call and turn random mid-frame pauses into scheduled ones.
  • Server programs get heap controls: SIGIL_GC_HEAP_GROW_FACTOR to trade a little collection frequency for a lower steady-state heap, (gc-idle-hint!) to collect between requests, and an opt-in malloc_trim after a major collection so freed pages return to the operating system. All are additive and off by default.
  • The %gc-step! incremental collection path had a latent unsoundness: an object allocated while a collection was marking could be freed while still live. It now allocates such objects black and re-scans roots at mark termination, with a tri-color audit mode that checks the invariant. Default collection was never affected; this matters for code that drives collection in steps.

Internal define-syntax that captures an enclosing binding works

A define-syntax written inside a function body whose template refers to a surrounding binding used to produce #<undefined> or crash, while the same macro at module level worked. Body-position capture now works when you run your code (eval, the REPL, sigil test). One honest limitation: this capture does not yet survive into a fully compiled, bundled binary, and capturing a per-call parameter is separate, larger work; both are tracked for a future release.

0.17.5

A bug-fix release centered on exception handling. On native and release builds, an uncaught exception could silently evaluate to #<undefined> and let execution continue instead of stopping; that is fixed, and dynamic-wind cleanup now runs correctly when an exception unwinds through it. Alongside that, the sigil mcp developer-tools server works again after being broken since an earlier refactor, and (gc-stats) gains real pause and memory observability. Existing projects keep building without changes.

Uncaught exceptions on native builds now stop and report

The native backend had no propagation path for an exception with no handler: the raising expression evaluated to #<undefined>, surrounding side effects kept running, and the program exited successfully. Now a handler-less exception prints its error and a trace and exits with a failure code, matching bytecode. This also covers the release sigil eval case where an uncaught error was swallowed, and it removes a class of silent-wrong-answer bugs where a native build quietly ran past a failure.

dynamic-wind cleanup runs when an exception unwinds

An exception caught by an outer guard used to skip the after thunks of any dynamic-wind it unwound through, so cleanup (closing a file, releasing a lock, restoring a parameter) was silently missed. Every exception-dispatch path now runs those after thunks in the correct inside-out order. A related hazard where concurrent goroutines could run each other's cleanup thunks, which had been corrupting a large share of cold parallel builds, is fixed as well.

The sigil mcp developer-tools server works again

sigil mcp serve (the server exposing format, check, lint, eval, test, run-file, and the rest as MCP tools) had been exiting immediately on startup ever since a refactor moved it between modules and left two imports pointing at the wrong place. It starts and serves correctly now. It is also sturdier: a runaway eval, a hanging test, or a script that blocks on input can no longer wedge the server, each is bounded by a timeout and the server stays up.

(gc-stats) gains pause and memory observability

(gc-stats) now reports collection pause times (a maximum plus a bucketed histogram), resident memory, young and tenured object counts, and which collector backend produced the numbers. Two new procedures, gc-minor-collect! and gc-young-occupancy, let latency-sensitive code such as a game loop schedule a minor collection at a quiet moment like a frame boundary instead of taking an allocation-triggered pause mid-frame. These are additive, with no change to when collection happens on its own.

0.17.4

A bug-fix release. The headline is a startup crash: native WebAssembly builds broke in 0.17.3, and this release restores them. Alongside that, several failures that used to vanish silently now surface properly: a failed sigil eval, a process that cannot be started, and an unbound variable in native code all raise instead of slipping past. A reader crash on deeply nested input is fixed, and the build cache now notices when the compiler itself changes. Existing projects keep building without changes, though the first build after upgrading recompiles from scratch (see the last note).

Native WebAssembly builds start again

0.17.3 shipped a regression that aborted every native WebAssembly build at startup on any sizable module. The native backend registers a garbage-collection root for each local in a compiled function, and a whole module compiles to a single function, so a large module like the core language needs thousands of roots in one frame. The old fixed cap of 2048 roots aborted the process the moment it was exceeded, which took out the core module itself. The root list now grows on demand, and the fixed 16 MB shadow-stack reservation shrinks to a few kilobytes plus whatever a program actually uses. If you build for the browser through the native path, this is the release you need.

Failures that used to disappear now surface

Three separate paths were swallowing errors:

  • A sigil eval that raised an uncaught error on a release build printed nothing and exited successfully. It now prints the error and exits with a failure code, matching the development build.
  • process-run returned a quiet false when a child could not be started or waited on, so a fork failure under heavy load looked like an ordinary result. It now raises an I/O error naming the failing stage, and retries a waitpid interrupted by a signal.
  • The native binding cache returned an undefined value for an unbound variable instead of reporting it. It now raises unbound variable, the same as bytecode.

A reader crash on deeply nested input is fixed

The reader kept a large scratch buffer on the C stack, and under some toolchains that buffer was inlined into the parse loop's frame, so deeply nested input could exhaust the stack before the depth guard fired. The buffer now lives off the stack, and deep input reaches the depth limit cleanly.

The build cache notices compiler changes

Build caches are now keyed on the content of the compiler executable, so rebuilding or reinstalling the compiler correctly invalidates stale cached output instead of reusing it. One consequence: the first build after upgrading to 0.17.4 recompiles everything once, because the old cache keys predate this field. After that, caching resumes as normal.

0.17.3

An additive release. The headline is that the threading macros move back into the core language, so ->, some->, and chain are available everywhere with no import. Alongside that: async web apps compiled to bytecode now suspend correctly in the browser, the auto-compile cache stops handing back stale modules, and a few build and CLI edges are cleaned up. Existing projects keep building without changes.

Threading macros return to the core language

->, some->, and chain now live in (sigil core) and are auto-imported everywhere, so you no longer pull in a standard library module just to thread a value through a series of calls. This also closes a sharp-edged bug: the macros previously lived in a module that was not always carried into a compiled binary, so a program that threaded cleanly at the REPL could fail with -> unbound once built. Threading now works identically in eval and in a built artifact.

Async web apps built to bytecode now suspend correctly

This complements the native WebAssembly async suspension shipped in 0.17.2. The --config web bytecode target now runs Binaryen's Asyncify pass whenever a build imports the asyncify controls, so async code (channels, goroutines, anything that suspends and resumes) instantiates and runs in the browser instead of failing to load with an unresolved asyncify import. This is the fix that brings the in-browser REPL back to life. One tradeoff to know about: async web bytecode builds are larger for now, roughly three times the size, because the transform currently applies to the whole module. A future targeted onlylist will bring that back down.

The auto-compile cache no longer serves stale modules

The auto-compile cache now invalidates correctly when a source file changes. Before this, an edit could be missed and a previously cached module reused, so you could run code that no longer matched your source.

Faster builds when native dependencies share headers

The C compiler cache now memoizes header inputs across sources that share include directories, cutting redundant work in builds that pull in native dependencies (mbedTLS-backed TLS and crypto, for example). Pure Sigil projects are unaffected; builds with native transitive deps get quicker.

sigil test runs again for packages with native dependencies

Running sigil test in a package that pulls in native dependencies (anything backed by TLS, crypto, or sockets) built a native test harness that died before any test ran, reporting unbound variable 'process-spawn'. The cause was in module resolution: a transitively imported module that registers native primitives at startup was treated as already fully loaded, so its Scheme-level wrapper layer never ran and part of its public API went missing. Imports now always complete both halves of a module, the native primitives and the Scheme wrappers, no matter how the module is reached. Native-dependency test suites run again.

The CLI picks the right release variant on upgrade

sigil upgrade and sigil use now select the correct static or musl release variant for the host when installing or switching versions, so a fresh install lands on a variant that actually runs on the machine.

A live web-app scaffold and a new guide

sigil init --template web-app now scaffolds a minimal live web application: a shared list you can add to and delete from, updating in every connected browser at once, with no client-side JavaScript to write. It uses the current (sigil web) API (http-response/page, the sg-* UI components, ui-routes, and a live-collection). A new Building Web Applications guide walks from the empty scaffold to a complete live to-do app in about a hundred lines, and points at the Vinyl Vault demo for a fuller worked example.

0.17.2

Another additive release: a major new capability for the native WebAssembly target, plus two correctness fixes for async programs and the macro system. Existing projects keep building without changes.

Native WebAssembly async suspension

Sigil programs that use the async scheduler (channels, goroutines, anything that suspends and resumes) can now be compiled to the native WebAssembly target and suspend and resume correctly, including across loop iterations. Previously the native-WASM target couldn't yield and resume an async task at all. This is built on Binaryen's Asyncify, with the suspend-set emitted by the compiler itself rather than a hand-maintained list, the minicoro fiber path un-gated for WASM, and a fix so GC roots survive the asyncify rewind across loops. The practical upshot: async Sigil code now works as native WASM, which is the missing piece for running something like the Enclave web client in the browser.

Eval-mode fix for capturing macros

Importing a package that defines a procedural syntax-rules macro which captures an internal definition (for example (sigil match)) no longer crashes under sigil eval. The capture pre-pass recognized only the primitive %lambda as a closure boundary, not the surface lambda that procedural transformers are written with, so a captured definition could be missed and its cell left dangling, producing a GET_CELL error at module load. The compiled path was never affected; this closes the eval-versus-compiled gap. Thanks to Trevor Arjeski, who reported it (issue #8).

Async-port logging no longer recurses

Logging to an async port (stdout, stderr, or an async file) while an exception was unwinding could recurse without bound and take the whole process down, which bit long-running async programs and MCP servers exactly when something had already gone wrong and you most needed the log line. The async write now detects when its scheduler prompt has been unwound past and falls back to a synchronous write instead of aborting into a prompt that no longer exists. Async behavior is otherwise unchanged.

0.17.1

A small, additive release that makes long-running Sigil programs more robust: the kind that supervise child processes, and the kind that dial network peers. Two new standard library capabilities and one REPL fix. Existing projects keep building without changes.

Die-with-parent process supervision

process-spawn gains a die-with-parent: keyword. When you set it, the spawned child asks the kernel to send it SIGTERM the moment its parent process dies. A supervisor that crashes or gets force-killed can no longer leave an orphaned child behind, which closes a whole class of orphaned-worker bugs that explicit shutdown code cannot reach: an uncatchable kill (SIGKILL) never runs your cleanup. On Linux this uses PR_SET_PDEATHSIG, with a re-check after the fork to close the well-known parent-death race window. On other platforms it is a documented no-op. The existing process-spawn signature is unchanged, so nothing you have written needs to move.

Timed TCP connect

(sigil socket) adds bounded numeric TCP connect, so a dial against a dead peer, a filtered route, or a refused socket can no longer block forever. There are two forms: one keeps the existing socket-or-#f shape, and one returns a status so callers can tell connection refused, connection timed out, host unreachable, and network unreachable apart from each other. It uses a nonblocking connect with SO_ERROR inspection, handles numeric IPv4 and IPv6, and treats a negative timeout as the original blocking behavior. Thanks to Trevor Arjeski, who built this for bounded peer transport in a Sigil Bitcoin node.

REPL nREPL startup fix

sigil repl --nrepl PORT no longer crashes on its first poll. The nREPL server path called sleep without importing (sigil time); the import is now in place. Thanks again to Trevor Arjeski.

0.17.0

0.17.0 is the browser release: you can now build and ship real web apps in Sigil! The whole browser stack is new and labeled experimental, but it is no longer a demo. It has been validated by building real applications on it: a browser game and a PWA chat client, both running on the new bridges with zero app-author JavaScript. This release also makes async networking more resilient, hardens the register VM and the reader, and cleans up a few standard library and build edges. Existing native projects keep building without changes.

Browser apps in Sigil (experimental)

A Sigil package can now declare a web build config and compile to a complete, servable web app. You check in your Sigil source, your CSS, and optionally a small HTML template; sigil build --config web generates the rest: the index.html shell, the runtime loader, the WebAssembly binary, and the bridge assets it needs. A new sigil serve command serves the output locally with correct MIME types and gzip compression, so the edit-build-reload loop needs no JavaScript tooling at all.

The browser bridges cover the APIs an app actually needs:

  • DOM with SXML rendering, so you build and update your page's markup from Sigil data structures.
  • WebSockets for real-time client networking.
  • IndexedDB and localStorage for persistence.
  • Canvas2D and WebGL for 2D and 3D graphics.

Bridge imports are dependency-gated: your app's .wasm only imports the bridges your package actually depends on, so a storage-only app carries no networking surface.

One honest caveat before you build on this: the browser and WASM APIs are still very experimental and driven by ongoing app development. They work today and are ready to explore, but their APIs may still shift before they are locked down.

How the browser modules are organized

The browser surface comes in two tiers. The raw platform layer lives under (sigil browser ...), with each module named after the Web platform API it wraps:

  • (sigil browser dom) and (sigil browser dom sxml), exporting dom-*
  • (sigil browser websocket), exporting websocket-*
  • (sigil browser canvas2d) and (sigil browser gl)
  • (sigil browser local-storage), (sigil browser indexeddb), and (sigil browser async)

Most apps should not import these directly. The front doors are the portable libraries and the framework: (sigil websocket) for client networking and (sigil web client) for browser-side framework code. Reach for (sigil browser ...) only when no portable module covers the capability yet, like raw canvas access or storage.

If you experimented with the pre-release (sigil wasm ...) module paths from development builds, they still work in this release as deprecation aliases that re-export the old identifier names. They will be removed before 1.0, so use the (sigil browser ...) paths going forward.

Native WASM groundwork (experimental)

Sigil modules can now compile natively to browser WASM, the same native-codegen path used for native binaries. For synchronous apps this already works end to end: native browser builds start without the bytecode bundle, load fewer resources, and ship a smaller payload.

Async is the missing piece: await suspension is not implemented under native WASM yet, so any app that relies on the cooperative scheduler in the browser should use the bytecode path. Bytecode is the supported way to ship a browser app in 0.17.0; native WASM is groundwork worth watching, not the default.

Async DNS resolution

DNS lookups no longer freeze async programs. Previously, a stalled system resolver could block the cooperative scheduler inside getaddrinfo, freezing every fiber in the process. The runtime now offloads resolution to bounded worker jobs that wake the scheduler through fd readiness, so a slow or unreachable DNS server delays only the connection that asked for it.

This release ships the runtime side of the fix: the new resolver natives live in the Sigil runtime itself. The async socket API in the sigil-socket package uses them in its companion 0.17.0 release, so async socket users get the new behavior once both are in place.

A sturdier register VM

The register VM now handles very large module bodies without running out of registers, and it reports allocation overflow clearly instead of failing in a confusing way. Programs with big generated modules, like macro-heavy code or large data tables, are the main beneficiaries.

A more robust reader

The reader is now hardened against malformed, oversized, and pathological input. Where bad input could previously crash the process, hang it, or be silently misread, the reader now returns clean, catchable errors and reads edge-case literals exactly. If you parse data or code from sources you do not control, this release is a meaningful correctness upgrade. Regression tests shipped with the fixes.

Standard library and build tidy-ups

  • (scheme cxr) is now owned solely by sigil-r7rs. The duplicate copy was removed from sigil-stdlib so the R7RS boundary is clean. The caar/cadr family of helpers stays available in (sigil core), so most code is unaffected.
  • Native build store keys now include the native tool inputs, so a change to the toolchain correctly invalidates cached build artifacts instead of serving a stale one.
  • Workspace Sigil-version validation now checks each member against the workspace version rather than comparing member ranges against one another, and it only applies to workspaces that ship the language itself. This fixes false conflicts in monorepos that carry packages forward from an earlier language version, without affecting application workspaces. It also fixes a development-build regression where a freshly created sigil init app failed sigil deps install with a spurious version conflict.
  • The reader accepts escaped-symbol syntax.

Known limitations

  • The browser and WASM packages are experimental and outside the 1.0 stability plans. APIs may change between releases while the surface settles.
  • Native WASM browser builds do not support async apps yet; bytecode is the supported browser path in this release.
  • The async DNS natives ship in this release, but the sigil-socket package release that uses them is still pending. Async socket code picks up the new behavior when the companion sigil-socket 0.17.0 ships shortly after this release.

0.16.0

0.16.0 is about making native Sigil applications feel faster in real use. The release focuses on runtime pacing, native-code generation, and the package versions used by the CLI, rather than changing the surface language.

Generational GC By Default

Sigil now builds with the generational garbage collector by default. The collector is designed for allocation-heavy programs: servers, tools that keep long-lived state around, and interactive apps that create many short-lived values per frame. Those workloads should see less time spent in full-heap collection and smoother latency under steady allocation.

The old mark-sweep collector is still available as an escape hatch:

SIGIL_GC=marksweep sigil build ...

Minor collection is enabled adaptively by default. Sigil watches how effective young-generation collection is and grows the young-generation threshold when a program is keeping most of what it allocates. That means allocation-heavy programs can benefit from minor collection without paying an unnecessary pause tax in workloads where young collections are not helping.

Faster Native Builds And Binaries

Native code generation now uses cross-compilation-unit optimization by default. Sigil writes and reads compact CPS summaries between modules, which lets the native compiler make better decisions when code in one module calls code from another. This helps larger programs most, especially apps split across many modules.

You can opt out while investigating a native-codegen problem:

SIGIL_CPS_NO_CROSS_CU=1 sigil build ...

Several common native call paths were also tightened up. Calls to known native functions, small helper closures, native struct accessors, vector field accessors, imported math helpers, and self-recursive named let loops now emit less general-purpose dispatch code when Sigil can prove a direct path is safe. For users, the expected effect is simpler generated C, smaller hot paths, and better runtime performance in code that leans on small abstractions.

Sigil also reduces metadata allocation for anonymous native closures. This matters most for programs that create many small closures in hot loops or frame updates; the runtime now avoids constructing location metadata that would never be useful to the user.

Smoother Interactive Apps

This release was validated against Cinder Cantata and the local native MCP server fleet. The practical target was not just "does it compile," but whether native Sigil programs can run for a while with stable frame pacing and without the small allocation-driven shivers that were visible in interactive workloads.

The GC and native-codegen defaults in 0.16.0 are the result of that validation. They make the optimized path the normal path, while keeping environment-variable opt-outs available for bisecting regressions.

0.15.0

0.15.0 reverses the 0.14 core toolchain extraction. Splitting the compiler, runtime, standard library, build system, dependency resolver, and test runner into separate release streams made the version story harder than it needed to be. It became too easy to ask "what version of Sigil is this?" and get a different answer from each part of the toolchain.

Those pieces are back in the main Sigil repository and they now release in lockstep. Libraries and optional ecosystem packages can still move on their own cadence, but the core toolchain should always be one coherent unit: one sigil version, one matching runtime, one matching stdlib, one matching build system, and one matching test runner.

The practical effect is a simpler build and versioning model. Native downstream builds no longer have to line up a CLI from one repo, a runtime from another repo, and build logic from a third repo. Local dev-redirects.sgl workflows also behave more predictably because workspace-owned packages now win over stale installed copies when the manifest says they should.

Native build reliability

Native builds are stricter about runtime caches now. The native test and runtime paths resolve package manifests as well as workspace manifests, find the matching sigil-lib through sibling packages when needed, and reject keyless libsigil.a archives when they cannot be validated. This catches the bad case early: fresh bytecode being linked or tested with an old native runtime.

Generated-module CPS summary generation is also bounded. Sigil still writes normal .sgs summaries, but generated mega-functions over 10,000 continuations skip the best-effort summary pass. CPS summaries are new in this release and are used by native optimization work. SIGIL_CPS_CROSS_CU=1 enables cross-compilation-unit consumption of those summaries for additional native optimization. The guard keeps pathological generated modules from spending unbounded time in summary generation while preserving the optimization path for normal modules.

Testing back in the toolchain

sigil-test and sigil-test-runner are part of the release again. The small assertion DSL stays separate from the heavier discovery, reporting, and native execution layer, so packages that only need sigil-test do not have to depend on the runner directly.

New projects created with sigil init now target ^0.15 in their generated package manifests. They no longer declare an explicit sigil-stdlib dependency; the build system derives the matching core stdlib from the sigil: field. Templates that use optional extracted libraries keep those dependencies on the current stable ecosystem range.

GC status

0.15.0 also includes the first generational GC work. It is opt-in for now: SIGIL_GC=generational enables the generational runtime build path, and the default GC has not changed. This makes the new collector available for testing without making it the production default.

The early generational backend already helps with some allocation-heavy workloads, but minor collection still needs more investigation before it is recommended for production builds.

0.14.0

The compiler, runtime, build system, and standard libraries have been split into their own repositories. codeberg:sigil/sigil is the user-facing CLI; codeberg:sigil/sigil-lang is the compiler and runtime; codeberg:sigil/sigil-build is the build system; and twenty-plus standalone library repos make up the rest of the ecosystem. To match this, packages declare a single sigil: field that the build system uses to specify which version range of Sigil they are compatible with. Most package manifests get smaller as a result.

This is a breaking change for the ecosystem. Existing packages that pull from the old monorepo URLs (e.g. (from-git url:"codeberg:sigil/sigil" package:"sigil-stdlib" ...)) need to migrate to the new extracted-repo URLs (codeberg:sigil/sigil-lang, codeberg:sigil/sigil-build, codeberg:sigil/sigil-X for individual libraries). The new URL conventions are listed in the extraction table below. Going forward, packages target the extracted repos directly and bump them on their own cadence; the monorepo no longer hosts library code.

The sigil: package field

Every Sigil package now declares which language version it targets:

(package
  name: "my-app"
  version: "0.1.0"
  sigil: "^0.14"
  ...)

The sigil: field is load-bearing build-system input. From it, the build system derives:

  • sigil-stdlib (always, for any package that imports anything from (sigil ...))
  • sigil-lib (when the package declares c-sources:, for the C runtime headers)

Existing packages that explicitly declare those deps continue to work; the auto-derive only injects when no explicit declaration is present. The win is in new packages and library refreshes: a typical pure-Scheme package.sgl no longer needs the (from-git url:"codeberg:sigil/sigil-lang" package:"sigil-stdlib" version:"^0.13.1") boilerplate every previous version required.

The field is now required for every package. Build commands fail loudly if a package manifest is missing sigil:; the error names the file and suggests the fix.

For multi-package workspaces, sigil: ranges across member packages must intersect. The build system pairwise-intersects member ranges and reports an honest error when they conflict, naming both members and their declared ranges.

The default-package: workspace field

Multi-package workspace repos can now declare a default package for cross-repo dependency references:

(workspace
  default-package: "sigil-build"
  packages: (list
    "packages/sigil-package"
    "packages/sigil-deps"
    "packages/sigil-build"))

When a downstream package writes (from-git url: "codeberg:sigil/sigil-build") without a package: selector, the resolver picks up the workspace's default-package: and uses it to disambiguate which sub-package to install. This makes single-package consumers of multi-package repos look like single-package consumers of single-package repos: no package: keyword needed for the common case.

The field is explicit-only; the build system does not infer a default. If a downstream package refers to a multi-package workspace via from-git without package: and the workspace doesn't declare default-package:, resolution errors loudly with the list of member packages so the consumer can pick.

The completed extraction

The package set extracted from codeberg:sigil/sigil over 0.12.x and 0.13.x is now closed. What stays in the monorepo (for now):

  • sigil-cli (the user-facing tool)
  • sigil-run (the bytecode runtime ship unit)
  • sigil-wasm-runtime (the WASM runtime variant)

What lives in its own repo:

RepositoryContents
codeberg:sigil/sigil-langsigil-lib (C runtime), sigil-stdlib (standard library), compiler
codeberg:sigil/sigil-buildsigil-package, sigil-deps, sigil-build (build system + manifest + dep resolver)
codeberg:sigil/sigil-ansiterminal styling
codeberg:sigil/sigil-argscommand-line argument parsing
codeberg:sigil/sigil-cryptocryptography primitives (vendored mbedTLS)
codeberg:sigil/sigil-docsdocumentation generation
codeberg:sigil/sigil-ffiforeign function interface
codeberg:sigil/sigil-formatcode formatting
codeberg:sigil/sigil-gitgit integration
codeberg:sigil/sigil-hookslifecycle hooks
codeberg:sigil/sigil-httpHTTP client + server
codeberg:sigil/sigil-jsonJSON parsing + encoding
codeberg:sigil/sigil-logstructured logging
codeberg:sigil/sigil-lsplanguage server
codeberg:sigil/sigil-mcpMCP (Model Context Protocol) library
codeberg:sigil/sigil-nreplnetwork REPL protocol
codeberg:sigil/sigil-replREPL library
codeberg:sigil/sigil-socketsocket primitives
codeberg:sigil/sigil-sqliteSQLite (vendored amalgamation)
codeberg:sigil/sigil-storecontent-addressed storage
codeberg:sigil/sigil-testtest runner
codeberg:sigil/sigil-tlsTLS layer (consumes sigil-crypto)
codeberg:sigil/sigil-versionversion helpers
codeberg:sigil/sigil-webweb framework

Each library now releases on its own cadence. Mixed-version ecosystems are by design: sigil-http at v0.14.0 may pin sigil-stdlib at ^0.13, and that is fine. The extraction was about decoupling release cadences, not enforcing lockstep.

sigil cli upgrade and sigil cli use

sigil cli upgrade is faster, more reliable, and verifies binary integrity. Downloads now ship with mandatory sha256 checks; mismatched downloads are rejected and cleaned up. sigil cli use <version> lets you pin to a specific historical version on demand. The upgrade flow is now decoupled from any specific release-repo URL, so future hosting moves won't require a new sigil binary.

init templates refreshed

sigil init templates were updated for the post-pivot ecosystem. Each template's package.sgl now references the new extracted-repo URLs (codeberg:sigil/sigil-args, codeberg:sigil/sigil-http, codeberg:sigil/sigil-mcp, etc.) rather than the previous codeberg:sigil/sigil#vX.Y.Z + package: shape. The default *sigil-version* baked into newly-scaffolded projects bumped from v0.7.0 (badly stale) to ^0.14 and tracks current stable going forward.

Hygienic macros: syntax-case and quasisyntax

Sigil's hygienic macro implementation is now feature-complete enough to use as the primary macro system for any project. R6RS syntax-case with full hygiene, ellipsis patterns, and quasisyntax templates all work end-to-end. This matters because hygienic macros are the difference between "macros that occasionally surprise you with variable-capture bugs" and "macros that compose cleanly without a thought" — and well-written code in this style relies on the corner cases being correct.

  • Quasisyntax shorthand — the reader now accepts ` # `, #,, and #,@ as shorthand prefixes for quasisyntax, unsyntax, and unsyntax-splicing` (matching R6RS).
  • Quasisyntax expansion — full hygienic template expansion lands. Templates can use unsyntax to splice values back into a syntax object during macro expansion.
  • syntax-local-value — R6RS compile-time environment query, available inside macros.
  • syntax/#' outside syntax-case/with-syntax — fixed; previously raised an "unbound variable" error when used at the top of a macro body without an enclosing pattern matcher.
  • syntax-case ellipsis patterns — fixed a long-standing bug where the ellipsis bound to the wrong template variable in nested patterns.

Build system and dependency resolver

The dep resolver fails loudly where it used to fail silently. The 0.13.x resolver had several "report success and break the consumer at build time" failure modes; those are gone. New behavior is loud and named: when a fetch errors, when a lockfile entry diverges from the manifest, when a config name doesn't exist. The motivating goal: turn unreproducible build mysteries into immediate, actionable errors at the source.

  • Lock-entry staleness — sigil deps install now invalidates a lock entry when its from-git URL or package selector drifts from the manifest, not just when its version range changes. Previously, swapping a from-git dep's URL while keeping the same name would silently reuse the old SHA from the lockfile.
  • Loud-fail on any fetch error — sigil deps install now exits non-zero when any fetch errors out. Previous behavior printed the error in red but still returned success and wrote the lockfile.
  • --config validation — sigil build --config <name> now fails loudly when the config name is undeclared in package.sgl. Previously the command silently fell through to the default config.
  • Improved diagnostic on path-resolve failures — the "Failed to resolve redirected path" error now names the missing package and suggests the fix.

Native codegen

Native builds are smaller, faster, and more visible. The CPS variable-declaration packing alone took the worst-case per-module zig-cc compile from "stuck at 24 minutes" to "completes cleanly," which makes native builds practical at the upper end of module size. Cumulative effect: native is now a comfortable default, not a "use at your own risk" path.

  • CPS variable declaration packing — emitted C now packs CPS variable declarations 8 per line instead of 1 per line. On large modules (e.g. the live-crafter theme module) this reduced emitted C from ~109k lines to ~77k lines, taking the per-module zig-cc compile time from "stuck after 24 minutes" to "completes cleanly."
  • PUSH_BINDING cache-miss path factored out of line — the binding-cache miss path was inlined into every binding access; pulling it out of line cut both binary size and per-binding hot-path code size.
  • Case-lambda runtime workaround retired — 332 lines of C in packages/sigil-lib/src/macro.c were removed after the underlying case-lambda hygiene bug was fixed in the syntax-case path.
  • Visible libsigil.a build progress — sigil-build's libsigil compile path now prints one line per call (libsigil.a: cache hit (...) or Building libsigil.a from source (...)) so users can see the C runtime being processed during native builds. Previously this was guarded behind --verbose.

Bug fixes

  • dep-name falls back to from-git-package — previously, deps installed via (from-git url: ... package: ...) shape resolved to a name of #f in error messages.
  • fetch-sgl symlink race — create-package-symlink now tolerates the dangling-symlink edge where a previous delete-file raced with file-exists? returning true.
  • WASM-incompatible code excluded from WASM builds — sigil-lib's setjmp-based exception scaffolding and tzset()-based time code are now #ifndef __wasm__-gated so WASM-musl cross-compiles cleanly.
  • Stale lock reuse on version-range change — fixed the case where sigil deps install would reuse a lockfile entry whose pinned SHA no longer matches the new version range.

0.13.0

Sigil ships as a native binary. The release CLI is compiled end-to-end through the native C backend — no bytecode, no interpreter loop, no runtime-compile on startup. Every sigil invocation on 0.13.0 exercises the native runtime. This release also introduces a full Language Server Protocol implementation (sigil lsp serve), restructures the native compile pipeline for visibility and parallelism, and lands the cell-elision and escape-analysis codegen optimizations default-on.

Native sigil CLI

sigil build sigil-cli -c release produces a fully native binary. No extra flags required — the workspace release config declares backend: 'native. All shippable configs (release, prerelease, and every cross-compile target) now declare backend: 'native; the WASM target keeps the bytecode VM.

Linux x86_64 ships in two variants:

  • sigil-linux-amd64 — dynamic glibc, cross-compiled via zig. The default download for normal distro users: smaller, faster startup, plays with system libc.
  • sigil-linux-amd64-static — static musl. Self-contained bootstrap binary for unknown distros, containers, and downstream CI that needs "runs anywhere."

Showcase: Cinder Cantata

Cinder Cantata v0.2.0 — a music-reactive bullet-hell built on Sigil — ships alongside this release as a demonstration of the native backend's performance. The full game loop runs on the 0.13.0 runtime: native codegen for the per-frame hot path, goroutines for audio streaming, FFI for graphics/audio sokol bindings, and the motif audio composition engine driving enemy bullet patterns in lockstep with the soundtrack. Download a playable binary for Linux x86_64 from the release page.

Language Server (sigil lsp serve)

Sigil now ships a full LSP 3.17 implementation, invoked as sigil lsp serve. Editors and coding agents can wire it up over stdio for:

  • Diagnostics — reader errors, compile errors, and lint warnings reported inline with source spans
  • Hover — signature + full docstring for any binding at point, with fallback to the runtime spec when source isn't available
  • Completion — context-aware completion items with proper filterText and textEdit for editors like eglot
  • Go-to-definition — jumps to the exporting .sgl source line; descends into library re-exports
  • Formatting — full-document and range formatting via (sigil format)
  • Quick-fix code actions — suggest-import proposes imports for undefined bindings and remove-unused-import cleans up lint warnings; both offered on cursor position, not just on diagnostics
  • Unused-import linter — detects unused imports with precise source spans, including inside re-exporting libraries

The server warms its docs index at initialized so first hover/completion is snappy. sigil lsp serve --log-file ... --log-level ... gives you traceable logs when debugging editor integrations.

Per-config backend: declarations

Build configs now declare their backend directly: (config name: 'release backend: 'native ...). Replaces the previous pattern where users had to use --backend native at build time. Each project's dev/release configs now declare their own backends (bytecode for fast iteration, native for shipping).

Per-package native compile pipeline

The native build pipeline was restructured to emit mod_*.c and compile to .o inside each package's build step instead of at the end in a silent 200K-line blob. Visible COMPILE mod_X.o per-package progress, parallelizable, better cache locality.

Cache correctness

Cache keys now include compiler identity + SGB version. Three caches (deps fetch, SGB auto-compile, runtime native) are keyed honestly against the exact compiler + runtime fingerprint they were produced with. Fresh checkouts and compiler rebuilds now produce consistent results instead of resurrecting phantom bytecode from a prior compiler version, eliminating a whole class of compiler errors.

Codegen optimizations (default-on)

  • Escape analysis — closures whose captures don't escape the enclosing frame are now stack-allocated instead of heap-allocated. Includes a trampoline-frame fix for TAIL_CALL.proc.
  • Cell elision v2 — post-conversion pass removes cells for variables that are read-only after definition. Register-allocator clobber bug that had blocked enabling cell elision by default (in 0.12.0) is fixed.

Both can be disabled via SIGIL_CPS_NO_CELL_ELISION=1 and similar env vars for the escape-analysis pass.

MCP server reliability

sigil-mcp (the Model Context Protocol server that lets Claude Code drive sigil) got a reliability pass:

  • Subprocess isolation for the sigil/test tool (was inheriting sigil-mcp's environment)
  • Per-server --log / --log-level flags for traceable multi-worker setups
  • JSON-RPC id: null compliance for notification-shaped messages

Worker fleets running sigil-mcp across many concurrent Claude Code sessions are now stable.

Bug Fixes

  • Self-rebuild trampoline crash — fixed a dangling inline_args pointer in the native-bridge trampoline. The pointer referenced a stack slot that had already been unwound by the time the trampoline bounced back through it. Harmless in short-running programs, fatal on the self-rebuild cycle where the compiler recursively compiles itself.
  • Closure serialization across threads — cell elision could leave some captures as raw values while siblings remained upvalue cells. Value-level serialization now carries a per-capture is-cell flag, so deserialized closures rebuild with the exact capture shape the code expects. Fixes cross-thread closure passing under default codegen settings.
  • Nested-guard codegen — exception-unwinding flag is now cleared after a tail handler fires, fixing a latent bug in deeply-nested guard forms.
  • Compiler self-bootstrap — 0.12.0 compilers can now build 0.13.0 source without guarded-reference crashes (SIGIL_ABI_VERSION reference is #ifdef-gated).

0.12.0

Register VM stabilized, stack VM removed, and experimental native C codegen. This release makes the register-based virtual machine the sole execution backend — the legacy stack VM and direct compiler have been removed. The register VM is now battle-tested across production MCP servers (folio, courier, bureau) and the full Sigil ecosystem. This release also introduces an experimental C native code generation backend.

Stack VM Removal

The stack-based virtual machine and direct compiler have been removed. The CPS compiler and register VM are now the only toolchain — no opt-in flags needed.

  • Removed stack VM interpreter, direct compiler, and CPS stack emitter
  • Removed mixed-mode dispatch
  • Renamed opcodes and headers (ROP_ → OP_, regvm.h → opcodes.h)
  • SGB format v8+ only (older binaries rejected)

Register VM Stabilization

The register VM introduced as opt-in in 0.11.0 is now the default and only backend. It has been stabilized through extensive testing and production use:

  • All ecosystem tests pass (~2,700 tests across 23 repos)
  • Running in production as the backend for MCP servers (folio, courier, bureau, minder)
  • 20+ bug fixes addressing edge cases in exception handling, macro hygiene, continuations, and module loading

Experimental Native C Codegen

New --backend=native flag for sigil build compiles Sigil programs to C, then to native binaries via the Zig toolchain. This is opt-in and experimental.

The native codegen compiles CPS IR directly to C functions with a trampoline-based calling convention. All modules — entry and dependencies — are compiled to native C. Features:

  • Full-program native compilation — all modules compiled to C, linked into a single binary
  • Module system support — cross-module calls, binding caches, module initialization
  • Delimited continuations — fiber-based C stack capture via ucontext (Linux/glibc)
  • Async goroutines — native fibers for concurrent I/O (scheduler, stdin read loops)
  • GC integration — shadow stack for local variables, conservative C stack scanning, trampoline closure rooting
  • MCP server support — folio and courier run as native MCP servers through Claude Code

87 of 91 native differential test files pass (matching bytecode output). The native backend is functional for real applications and delivers significant performance improvements over the bytecode VM.

Platform Limitations

  • Linux (glibc): Full support including async fibers
  • Linux (musl): Builds and runs, but no fiber support (musl ucontext stubs return ENOSYS)
  • macOS: Builds and runs, but no fiber support (ucontext is deprecated)
  • Windows: Builds and runs, but no dynamic native module initialization

Async goroutines require fiber support. On platforms without it, persistent servers (MCP, HTTP) that use goroutines for I/O will not work in native mode.

Bug Fixes

  • Fix macro hygiene: literal comparison and free identifier resolution
  • Fix let* variable scope leak causing GET_CELL crash
  • Fix raise-continuable in tail position
  • Fix GC use-after-free in compile-file for dependency builds
  • Fix register VM parallel arg move clobbering in tail calls
  • Fix define-library body defines leaking to top level
  • Fix guard/with-exception-handler across eval boundaries and tail calls
  • Fix collect_internal_defines and capture analysis for module bodies
  • Fix MCP server crash on resources/templates/list requests

0.11.0

Register VM and self-hosting CPS compiler. This release adds a register-based virtual machine as an alternative backend for the CPS compiler introduced in 0.10.0. The new toolchain is opt-in via SIGIL_NEXT=1 and is 7-57% faster than the direct compiler across all benchmark categories. It is fully self-hosting — the CPS compiler running on the register VM can compile itself.

Register VM

Enable with SIGIL_NEXT=1. This activates both the CPS compiler and the register VM together. All stdlib modules compile, all ecosystem tests pass (zero regvm-specific failures across ~2,700 tests in 23 repos).

The register VM uses 32-bit fixed-width instructions with 3-address register operands. Key design features:

  • Liveness-aware register allocation via CFG-based backward dataflow analysis
  • Register coalescing eliminates redundant moves between registers
  • Self-tail-call loop optimization converts recursive tail calls to JMP instructions
  • Call window optimization overlaps caller/callee register frames
  • Full continuation support — call/cc, delimited continuations, abort-to-prompt
  • Exception handling — raise, guard, with-exception-handler

New source files:

  • regvm.c -- register VM interpreter (90+ opcodes)
  • regvm.h -- register VM instruction encoding and opcodes
  • cps-emit-reg.c -- CPS IR to register bytecode emission

Performance

Benchmarks across 60+ tests (ratio = register VM time / direct compiler time, lower is faster):

CategoryRatioImprovement
Compute (fib, tak, ack)0.79-0.93x7-21% faster
Closures0.56-1.03xUp to 44% faster
Tail calls0.76-0.96xUp to 24% faster
String processing0.43-0.90xUp to 57% faster
Allocation/trees0.80-1.01xUp to 20% faster
Higher-order functions0.72-1.03xUp to 28% faster
JSON encode/decode~1.0xParity (stdlib-bound)

The register VM went from 3.3x slower (initial prototype) to consistently faster than the direct compiler through register allocation, coalescing, and call window optimizations.

Self-Hosting Bootstrap

The CPS compiler + register VM can compile itself. A two-stage bootstrap (stack VM binary compiles regvm binary, regvm binary recompiles itself) produces byte-identical output for 43/44 stdlib modules. This proves the new toolchain is correct enough to replace the direct compiler.

CPS Compiler Improvements

Fixes and enhancements to the CPS compiler from 0.10.0:

  • Forward-referenced macro expansion during CPS conversion
  • CFG-based liveness analysis for register and slot allocation
  • Rest parameter slot allocation fix (was dropping the rest param)
  • GC rooting for import set datums during module loading
  • Module context save/restore across nested module loads
  • call/cc stack reservation for lambda locals
  • CPS emitter abort-to-prompt continuation routing for delimited continuations
  • 19 primitive operation fallbacks added to register emitter
  • Syntax transparency for register VM type predicates (car, cdr, null?, pair?, symbol?)
  • Bignum arithmetic operations (mod, rem, neg, abs, number->string)

Other Changes

  • SIGIL_NEXT=1 flag activates CPS + register VM together
  • SIGIL_MODULE_MAX_MACROS increased from 64 to 256
  • Benchmark suite added at benchmarks/ for release-to-release comparison

0.10.0

Experimental CPS compiler with optimization passes. This release adds a continuation-passing style intermediate representation to the compiler pipeline, sitting between macro expansion and bytecode generation. The CPS path is opt-in and does not change default compiler behavior.

CPS Compiler

Enable with SIGIL_USE_CPS=1. The CPS pipeline converts expanded S-expressions into CPS IR, runs optimization passes, then emits bytecode. All existing tests pass through the CPS path.

New source files:

  • cps.h -- CPS IR data structures and arena allocator
  • cps-convert.c -- S-expression to CPS conversion
  • cps-emit.c -- CPS to bytecode emission
  • cps-opt.c -- optimization pass pipeline
  • cps-dump.c -- CPS IR pretty-printer for debugging

Optimization Passes

Control the optimization level with SIGIL_OPT_LEVEL (0, 1, or 2). Individual passes can be disabled with environment variables.

-O1 (contification + dead code elimination):

  • Contification: converts single-call closures in tail position into continuation jumps, eliminating closure allocation. Non-tail contification enabled for pure callees.
  • Dead code elimination: removes unreachable continuations via BFS reachability analysis.

-O2 (adds inlining, constant propagation, capture minimization):

  • Beta reduction / inlining: inlines small functions (10 or fewer CPS terms, called at most twice) at tail call sites.
  • Constant propagation: folds fixnum arithmetic and boolean operations at compile time, eliminates constant branches. Overflow-safe using compiler builtins.
  • Capture minimization: removes unused captures from closures after dead code elimination.

Pass ordering: contification, inlining, constant propagation, DCE, capture minimization. At -O2, the pipeline iterates up to 3 times for cascading opportunities.

Debugging

  • SIGIL_DUMP_CPS=1 -- dump CPS IR before optimization
  • SIGIL_DUMP_CPS_OPT=1 -- dump CPS IR after optimization
  • Individual pass toggles: SIGIL_CPS_NO_CONTIFY=1, SIGIL_CPS_NO_DCE=1, SIGIL_CPS_NO_INLINE=1, SIGIL_CPS_NO_CONSTPROP=1, SIGIL_CPS_NO_CAPMIN=1

Other Changes

  • cond-expand implemented as an expression form in the CPS converter
  • case-lambda CPS expansion bypasses hygiene interaction bug in syntax-rules dispatch
  • Fix sigil_apply{0,1,2,3} to reserve stack space for locals
  • Fix module context inheritance for closures created inside define-library body
  • Fix make-vector to use native call instead of primitive opcode
  • make default target now builds stdlib (previously required make stdlib)

0.9.1

Transitive dependency resolution, version ranges, and lockfile support. The package manager now recursively resolves the full dependency tree — projects only need to declare their direct dependencies, and everything else is fetched automatically. This is the feature that makes the extracted ecosystem from 0.9.0 practical to use at scale.

Transitive Dependency Resolution

sigil deps install now recursively follows from-git dependencies. When a package is fetched, its package.sgl is read and its own dependencies are queued for fetching. Deduplication by package name prevents redundant fetches and cycles.

Before (0.9.0): if your project depends on press, and press depends on sigil-markdown, sigil-sxml, sigil-fp, and 7 other packages, you had to declare all of them manually in your package.sgl.

After (0.9.1): declare press and everything else resolves transitively. sigil-site went from 4 declared dependencies to 2, with 12 more resolved automatically.

Version Ranges

The from-git dependency form now supports an optional version: field with Cargo-style semver ranges:

(from-git url: "codeberg:sigil/sigil-sxml" version: "^0.9.0")

Supported range syntax:

  • ^0.9.0 — minor-compatible: >=0.9.0, <0.10.0
  • ~0.9.0 — patch-only: >=0.9.0, <0.9.1
  • Exact version: 0.9.0
  • * or omitted — any version (latest tag)

Resolution happens after cloning the bare repo: available tags are listed and the highest matching version is selected.

Lockfile

sigil deps install now writes sigil.lock with the exact commit hash for every resolved dependency (direct and transitive):

;; Auto-generated by sigil deps install. Do not edit.
(lock
  (package name: "press" url: "codeberg:sigil/press" ref: "v0.1.0" sha: "abc123...")
  (package name: "sigil-stdlib" url: "codeberg:sigil/sigil" ref: "v0.9.0" sha: "def456...")
  ...)
  • sigil deps install reads the lockfile and uses pinned refs for reproducible installs
  • sigil deps update ignores the lockfile, re-resolves from package.sgl ranges, and writes a fresh lockfile
  • --redirects skips the lockfile entirely (neither read nor written) since local development overrides everything

Version Conflict Detection

When a transitive dependency requires a version range incompatible with an already-resolved version, sigil deps install errors with an actionable message suggesting you add an explicit pin in your package.sgl.

Bug Fixes

  • Added sigil/deps/semver and sigil/deps/lock to Makefile bootstrap modules

0.9.0

Lean core, ecosystem split, Zig everywhere. Sigil's monorepo sheds over 25,000 lines as libraries move to independent repositories with their own release cycles. The standard library is streamlined for AI-native development — pattern matching, transducers, R7RS compatibility, and functional programming utilities move to optional packages. Zig replaces GCC as the default C compiler for native builds, and Emscripten is replaced entirely by Zig + WASI for WebAssembly, enabling the Sigil REPL to run in browsers without heavyweight toolchain dependencies.

Ecosystem Split

The monorepo shrinks from 39 packages to 26. Extracted libraries are now independent repositories on Codeberg under the sigil/ organization, each with their own version tags, CI, and release cycle.

Extracted PackageRepositoryDescription
sigil-pegcodeberg.org/sigil/sigil-pegPEG parser combinators
sigil-sxmlcodeberg.org/sigil/sigil-sxmlSXML to HTML/XML conversion
sigil-markdowncodeberg.org/sigil/sigil-markdownCommonMark parser with YAML front matter
sigil-webcodeberg.org/sigil/sigil-webWeb application framework
sigil-csscodeberg.org/sigil/sigil-cssProgrammatic CSS generation
sigil-matchcodeberg.org/sigil/sigil-matchPattern matching
sigil-fpcodeberg.org/sigil/sigil-fpFunctional programming utilities (compose, pipe, partial, threading macros)
sigil-transducerscodeberg.org/sigil/sigil-transducersComposable transducers
sigil-r7rscodeberg.org/sigil/sigil-r7rsR7RS-small compatibility (scheme *) modules and SRFI-9 records
sigil-web-replcodeberg.org/sigil/sigil-web-replBrowser-based REPL (was sigil-playground)
presscodeberg.org/sigil/pressStatic site generator (was sigil-publish)
sigil-sitecodeberg.org/sigil/sigil-siteOfficial Sigil website
sigil-web-democodeberg.org/sigil/sigil-web-demoDemo web application

Extracted packages use from-git dependencies and dev-redirects.sgl for local development. All existing functionality is preserved — packages just live in their own repos.

AI-Native Standard Library

The standard library is streamlined based on a comprehensive audit of how AI agents actually use Sigil across 12 production codebases (117 libraries). Features with zero AI usage were moved to optional packages:

  • Pattern matching (match, match-let) — moved to sigil-match
  • Transducers (mapping, filtering, transduce) — moved to sigil-transducers
  • Functional combinators (compose, pipe, curry, partial, cut/cute) — moved to sigil-fp
  • Threading macros (chain, ->, some->) — moved to sigil-fp
  • R7RS compatibility ((scheme base), (scheme write), etc.) — moved to sigil-r7rs
  • SRFI-9 records (define-record-type) — moved to sigil-r7rs

The core language retains what AI agents naturally reach for: when/unless, keyword arguments, define-struct, guard/error, map/filter/apply/find, dicts, broadcast channels, and go concurrency.

Zig as Default Compiler

Zig is now the preferred C compiler across all build paths:

  • Native builds: detect-default-compiler and compile-native-tests prefer zig cc when available, falling back to system cc/gcc
  • Cross-compilation: All eight platform targets use Zig (unchanged from 0.8)
  • CI: Test and GC stress jobs use Zig via with-zig wrapper
  • WebAssembly: New web build config uses zig cc -target wasm32-wasi (see below)

WebAssembly Without Emscripten

The web build target is completely rewritten. Emscripten is replaced by Zig targeting wasm32-wasi with a browser WASI polyfill:

  • All 34 C source files compile with zig cc -target wasm32-wasi
  • 3.3MB WASM binary (comparable to Emscripten output)
  • Uses @bjorn3/browser_wasi_shim for WASI polyfill in browsers
  • New __SIGIL_WASM__ platform define replaces __EMSCRIPTEN__ guards
  • Web build config: sigil build sigil-web-repl --config web
  • Sigil REPL runs fully in the browser — expression evaluation, module loading, interactive input

sigil-docs Trimmed

sigil-docs is now a lightweight JSON lookup package for REPL inline help:

  • Removed (sigil docs commands) — the sigil docs CLI subcommand. Press handles all doc site generation now.
  • Removed (sigil docs execute) — code block execution for doc validation
  • Removed dependencies on sigil-markdown and sigil-sxml
  • Remaining: (sigil docs lookup), (sigil docs search), (sigil docs types) — just reads the JSON files the compiler generates

sigil-cli Slimmed

The CLI drops several optional command modules and dependencies:

  • Removed sigil docs command (handled by press)
  • Removed sigil publish command (handled by press)
  • Removed sigil changes command (package deleted)
  • Removed dependencies: sigil-docs, sigil-web, sigil-changes

0.8.0

Dynamic FFI and portable foundations. Sigil gains the ability to call any C library at runtime without writing glue code, a content-addressable build cache for faster incremental builds, and seven new packages spanning graphics, game development, calendar sync, and messaging. Cross-compilation now passes across all eight platform targets, and a new app management system makes it easy to install and run Sigil applications from git repos. With this release, Sigil reaches a stability point that enables us to begin decoupling libraries from the monorepo for independent releases and faster iteration.

Dynamic FFI

The headline feature. sigil-ffi lets you call C library functions directly from Scheme without writing any C glue code. Load shared libraries with ffi-open, look up symbols with ffi-ref, define call signatures, and invoke — all at runtime.

(import (sigil ffi))

(define lib (ffi-open "libm.so.6"))
(define c-sqrt (ffi-ref lib "sqrt" '(double) 'double))
(c-sqrt 144.0) ; => 12.0
  • Callbacks: c-callback creates function pointers that call back into Scheme, enabling integration with event-driven C APIs like GUI toolkits
  • Pointer finalizers: set-pointer-finalizer! attaches GC-triggered cleanup to foreign pointers (pass #f to clear)
  • dyncall backend: Replaced pure-C trampolines with dyncall for portable calling convention support, including mixed int/double argument signatures used by Cairo and similar libraries

Content-Addressable Build Store

A new sigil-store package provides content-addressable storage for build artifacts. C compilation results and static libraries are cached by content hash, so unchanged native code is never recompiled. Build outputs are properly rooted for GC, and the store integrates transparently with the existing build system.

App Management

New sigil app commands for installing, updating, and removing Sigil applications from git repositories:

  • sigil app install — clone a repo, resolve dependencies, bundle the app, and register any MCP servers
  • sigil app list — show installed applications
  • sigil app update — pull latest changes and rebuild
  • sigil app remove — clean up installed app and deregister MCP servers
  • sigil app rollback — revert to the previous version

Supports --scope for MCP server registration, --env and secret-env for configuration, and graceful handling of missing environment variables.

New Packages

PackageDescription
sigil-ffiDynamic FFI for calling C libraries at runtime via dyncall
sigil-cairoCairo 2D graphics bindings — paths, transforms, surfaces, PNG loading, compositing operators, glyph rendering
sigil-freetypeFreeType font loading and glyph metrics via FFI
sigil-harfbuzzHarfBuzz text shaping via FFI
sigil-raylibRaylib game development bindings — 2D shapes, text, input, collision, audio
sigil-caldavCalDAV calendar client with iCalendar parsing, event CRUD, recurrence expansion, timezone handling
sigil-telegramTelegram Bot API client with bot framework, MarkdownV2 formatting, inline keyboards, file uploads

MCP Server Improvements

  • Channel servers: New (sigil mcp channel) module with helpers for building MCP channel-based servers (stdin/stdout transport)
  • Server-initiated notifications: MCP servers can now push notifications to clients, declare custom capabilities, and provide instructions
  • Structured logging: MCP message handling uses sigil-log with log-configure-from-args! for CLI integration
  • Better error reporting: Stack traces included in MCP tool error responses

HTTP Improvements

  • /json route helpers now return JSON bodies on all HTTP status codes, not just 2xx
  • url-encode-value and build-query-string for proper URL query parameter encoding
  • Fixed UTF-8 split across HTTP read chunk boundaries
  • Fixed chunked transfer decoding for non-ASCII UTF-8 content
  • Fixed Content-Length using character count instead of byte length
  • Fixed partial socket writes truncating large HTTP responses
  • Unix domain socket server support via unix-listen

CalDAV and iCalendar

Beyond the new sigil-caldav package, significant improvements to calendar handling:

  • Timezone-aware datetime parsing with correct DST offset calculation via mktime
  • Attendee parameter support for event creation
  • Calendar name and timezone fields on ical-event
  • RRULE recurrence expansion with RECURRENCE-ID exception suppression
  • Date range filtering for non-recurring events in recurrence expansion

Formatter Performance

The code formatter was optimized from 30 seconds to under 2 seconds on large files (2800+ lines):

  • Replaced struct-based tokens with raw vectors
  • Inlined tokenizer hot loops with delimiter lookup table
  • Zero-allocation form type matching
  • Fixed tokenizer mishandling of delimiter character literals

Build System

  • Content-addressable store for cached C compilation and static libraries
  • Fixed build cache ignoring dependency changes
  • Fixed false source file matches in module dependency resolution
  • Fixed test runner dependency resolution for external packages
  • Fixed build store hard-link self-copy corruption
  • Hardened test runner against load errors
  • Default release/prerelease configs switched to glibc dynamic linking (supporting FFI and dlopen)
  • Static build configs available as release-static and prerelease-static

JMAP Improvements

  • jmap-email-count for lightweight email counting without fetching messages
  • jmap-download-blob for downloading JMAP blobs to files
  • Batch email move support
  • Fixed send compatibility issues

Performance and Code Quality

  • Fixed quadratic string-join — now uses linear accumulation
  • Improved list-unzip to use simultaneous traversal
  • Deduplicated inspect-vector and inspect-array with shared helper
  • Dict-based lookups in diagnostic module and watch module for faster type-guard and mtime resolution

Bug Fixes

  • Fixed crash when format displays bignum values
  • Fixed segfault from unbounded macro export recursion
  • Fixed preemptive yield escaping through sigil_apply boundaries
  • Fixed string->list to properly decode UTF-8 characters
  • Fixed string-join overflow with large lists
  • Fixed double-encoded UTF-8 in build output and diagnostics
  • Fixed YAML encoder to handle Sigil arrays
  • Raised module binding limit from 2048 to 4096
  • Fixed async stdin handling for MCP servers (pipe detection, fd-waiter integration)
  • Fixed char-ready? for pipes with async-aware stdin polling
  • Added Scheme module definition for sigil-tls package
  • Fixed Windows cross-compilation: fcntl/FD_CLOEXEC guards replaced with SetHandleInformation
  • Fixed OpenBSD cross-compilation: setvbuf workaround for opaque FILE struct in Zig headers
  • Fixed OpenBSD executable path resolution, poll fd limit, and partial write handling
  • Set FD_CLOEXEC on TCP sockets to prevent child process inheritance

0.7.0

Parallelism and production readiness. Sigil gains multi-core execution, better I/O scalability, proper exception semantics, machine-readable procedure specifications, structured logging, and four new packages. With the stability and DX foundations from 0.5.0 and 0.6.0, this release focuses on the features needed to build and run serious applications.

Multi-VM Threading

The headline feature. Each thread-spawn creates a new VM with its own stack, heap, GC, symbol table, and module registry — zero shared mutable state, no locks needed. Threads communicate by message passing with automatic serialization/deserialization. parallel-map provides a high-level API for data-parallel workloads, partitioning work across N workers and collecting results.

Tail-Optimized Exception Handling

Replaced prompt-based exception handling with a dedicated VM-level handler stack. guard bodies now execute in true tail position, eliminating the 512-prompt practical limit. Deep recursive guard patterns (10,000+ iterations) work without overflow. Closure arity errors are now catchable by guard. Cross-context exception propagation works correctly through eval and load boundaries.

I/O Multiplexing

Replaced select(2) with epoll (Linux) and kqueue (macOS/BSD) behind the existing socket-select and fd-select APIs. Purely internal change with no Scheme API impact. Handles 200+ concurrent file descriptors efficiently.

Preemptive Yield Points

Yield checks at loop back-edges prevent CPU-bound tasks from starving other coroutines in the async scheduler. Default disabled — only activates when running inside with-async. Configurable interval via SIGIL_YIELD_INTERVAL environment variable.

Procedure Specifications

Machine-readable specs for procedure parameters and return values using real Scheme predicates. Specs use the inline (: ...) syntax placed as the first expression in a define or lambda body:

(define (add a b)
  (: number? number? -> number?)
  (+ a b))
  • Runtime checking: Optional predicate checks gated by the sigil-checks feature — zero cost in release builds
  • Predicate combinators: any-of, maybe, list-of, none-of for expressive type constraints
  • Struct field annotations: define-struct fields accept type predicates, auto-generating specs for constructors, accessors, and mutators
  • Full coverage: 330+ specs across 35+ modules, all audited against conventions
  • Tooling: Specs displayed in MCP search/exports output and generated API documentation

define-native Compiler Form

Declares bindings provided by native C code at runtime. Scheme modules can document and export native functions without fallback implementations, with optional specs and docstrings. Uses the new OP_DEFINE_IF_UNBOUND opcode to avoid overwriting native bindings registered at VM startup.

New Packages

PackageDescription
sigil-tuiImmediate-mode terminal UI toolkit with native C grid, layout system, components, event parsing, and text input
sigil-jmapJMAP email client (RFC 8620/8621) with session discovery, mailbox operations, email query/send
sigil-yamlYAML parsing and serialization with block/flow collections, block scalars, anchors/aliases, multi-document
sigil-logStructured logging with levels, text/JSON output, configurable targets, and lazy macros

sigil-tui-demo provides a chat client simulation exercising all TUI components.

HTTP Improvements

  • Streaming HTTP download support with binary-safe reads
  • File serving with Range request support and port positioning

Developer Tooling

  • MCP server: Procedure specs shown in search and exports output
  • API docs: sigil docs api fixed and now displays spec signatures in generated documentation
  • Build system: Test compilation propagates build config features (e.g., sigil-checks)

Bug Fixes

  • Fixed false positive capture corruption check in serializer
  • Fixed Windows build: mingw macro collision with _inline field name
  • Fixed Windows build: use define-native for unix-connect (not available on Windows)
  • Fixed exception handler double-pop causing test runner segfault
  • Fixed bootstrap compilation: replaced when with if in %set-spec!

0.6.0

This release brings Sigil much closer to full R7RS compliance, adds a complete web development workflow with live browser reloading, and introduces three new packages: a PEG parsing library, an Org Mode parser, and an XMPP client. Developer tooling improves significantly with rich error diagnostics, an enhanced MCP server, and watch mode for incremental builds.

R7RS Compliance

The biggest area of work in this release. Sigil now supports nearly all of R7RS Small:

  • Control flow: dynamic-wind, do syntax, guard prompt limit raised for deeper exception handling
  • Macros: let-syntax, letrec-syntax
  • Modules: include, include-ci, import set modifiers (only, except, prefix, rename)
  • I/O: Bytevector ports, read-bytevector!, char-ready?, u8-ready?
  • Output: write-shared with datum labels for circular structure printing
  • Feature detection: features, cond-expand, syntax-error
  • Libraries: (scheme time), (scheme process-context), (scheme repl) with interaction-environment
  • Literals: #u8(...) bytevector literal syntax in compiler and reader

Web Development

A full live-development workflow for web applications:

  • Live reloading: nREPL server with SSE-based browser reload — edit code, see changes instantly
  • Higher-level UI components: Expanded component library for sigil-web
  • API refinements: Improved ergonomics for routing, form handling, and GET requests
  • CSS generation: css-sel for compound selectors
  • MCP browser tools: 7 new tools for live browser control (morph, navigate, execute JS, reload CSS, error overlay)
  • Demo application: Vinyl Vault, a full CRUD demo showcasing the web stack

New Packages

PackageDescription
sigil-pegParsing Expression Grammars with S-expression DSL, backtracking, and captures
sigil-orgOrg Mode parser producing SXML (headlines, metadata, source blocks, tables, lists)
sigil-xmppXMPP client with STARTTLS, SASL (SCRAM-SHA-1/PLAIN), roster, presence, and MUC

Supporting packages: sigil-crypto gains HMAC-SHA1 and PBKDF2-SHA1, sigil-tls adds tls-upgrade for STARTTLS.

Developer Tooling

  • Rich error diagnostics: Structured error type hierarchy with Levenshtein-based "did you mean?" suggestions, source snippets with caret indicators in stack traces
  • MCP server: Package-provided tool discovery, multi-form eval, format auto-fix, runtime doc fallback for undocumented exports, nREPL describe and modules operations
  • Watch mode: sigil build --watch and sigil test --watch with --format json support
  • Value inspection: New (sigil inspect) module for structured introspection via nREPL and MCP
  • Project templates: sigil init gains web-app, api-service, and mcp-tool templates
  • Docstring collection: Variable definitions now collect docstrings; native docs propagate through re-exports

Performance

  • Dict lookup: FNV-1a hash cached on string keys, avoiding repeated computation

Library Improvements

  • sigil-markdown: Inline parser rewritten using PEG grammars (~120 fewer lines, same behavior)
  • sigil-json: Handle UTF-16 surrogate pairs in unicode escapes per RFC 8259
  • sigil-docs: Re-export descriptions resolved from source module entries

Bug Fixes

  • Fixed SIGIL_NATIVES_MAX limit (increased to 2048) for projects with many native bindings
  • Fixed --no-color flag in test runner
  • Fixed git-fetch to include tags when updating cached bare repos
  • Fixed binary-safe nREPL wire protocol and SIGPIPE handling
  • Fixed MCP format tool crash

0.5.0

This release is all about stability and correctness. A comprehensive audit of the garbage collector uncovered and fixed a critical write barrier gap, and GC stress testing now runs across the entire compiler pipeline. The HAMT dict implementation is complete, arbitrary-precision integers (bignums) are fully integrated, and every open issue has been resolved. Performance improves with inline caching and superinstructions, and the multimedia packages have been reorganized for flexibility.

GC Correctness

The most impactful change in this release: the tri-color write barrier was implemented but never called. Every mutation site that writes a heap pointer into an existing object could violate the tri-color invariant during incremental marking. This was the root cause of several hard-to-reproduce crashes.

  • Write barriers: Added sigil__gc_write_barrier() calls to all 13 heap mutation sites across vm.c and natives.c
  • Compiler rooting: Removed the SIGIL_GC_DISABLE_DURING_COMPILATION workaround entirely. Fixed 46+ unrooted allocation sites in the compiler through systematic audit
  • GC stress testing: SIGIL_GC_STRESS=1 forces GC on every allocation, maximizing the window for bugs. All tests pass under stress

Performance

  • Inline caching: Module binding lookups now use per-code cache arrays. On benchmarks, 3.3M hash lookups reduced to ~38K cold-start misses, with 100% steady-state cache hit rate
  • Superinstructions: Peephole optimizer fuses 8 common opcode patterns (e.g., PUSH_LOCAL+PUSH_LOCAL, PUSH_CONST+ADD). Compiler-level fusion merges source location tracking with call opcodes

Arbitrary-Precision Integers

Integers larger than 2^47 now automatically promote to bignums instead of raising an error:

  • Full arithmetic operations (add, subtract, multiply, divide, modulo, GCD)
  • Comparison and bitwise operations
  • Transparent promotion and demotion (bignum results that fit in a fixnum automatically demote)
  • Serialization support in bytecode files

Dict Completeness

The HAMT (Hash Array Mapped Trie) implementation is now feature-complete:

  • Printing: HAMT dicts display all entries with display and write
  • Equality: equal? walks HAMT nodes for structural comparison
  • Collision handling: Fixed silent key dropping when hash collisions occur at the same node
  • Removal: Proper structural dict-remove with sharing, branch collapse, and HAMT-to-array demotion when count drops below 8

Error Handling

  • Three exit(1) calls in the module system replaced with sigil__vm_error() so the Scheme-level exception system handles them gracefully
  • OP_ABORT_TO_PROMPT now extracts real error messages from exception objects
  • Macro expansion errors propagate properly to the compiler
  • match raises an error instead of printing a warning when no clauses match

Stack Traces

Procedures now track where they were defined. Stack traces show definition locations, making it easier to identify which function failed:

Stack trace (most recent call first):
  0: my-helper
     at utils.sgl:42:5

Multimedia Packages

sigil-studio has been split into three independent packages for more flexible dependency management:

PackageDescription
sigil-appWindowing, input, application lifecycle (sokol_app)
sigil-graphics2D rendering, image loading, font rendering (sokol_gfx, stb)
sigil-audioSound effects and music streaming (sokol_audio, stb_vorbis)

sigil-studio remains as a meta-package that pulls in all three. New: playlist support with wait-music-end and play-playlist.

Build System

  • Test harness generation for packages with native C dependencies
  • c-flags field on library definitions
  • --profile flag for opcode-level performance analysis
  • Build safety: warns when replacing the running binary, errors on 0-byte output or missing target package

Test Coverage

Added test suites for 15+ packages that previously had none, including sigil-crypto, sigil-tls, sigil-web (routing, middleware, cookies, SSE, security), sigil-socket, sigil-process, sigil-mcp, sigil-jwt, sigil-websocket, sigil-markdown, sigil-format, and sigil-sxml. Error path tests added across the standard library.

Bug Fixes

  • Fixed assq-ref to return values instead of aliasing assq; added assv-ref
  • Fixed number->string precision for exact IEEE 754 roundtrip (was losing digits on large flonums)
  • Fixed REPL string-length error after exception recovery
  • Fixed cross-module symbol resolution corruption
  • Fixed bundle directory listing for bundled applications (VFS)
  • Fixed sigil-run re-import of (sigil core) before calling entry main
  • Fixed process port caching for stable repeated access
  • Removed dead opcodes (OP_LOAD_BYTECODE, OP_MAKE_CLOSURE_RT, OP_WRITE_BYTECODE_FILE)

0.4.0

This release makes Sigil leaner and faster. Native list operations cut compilation time in half, release binaries are 44% smaller, and a new profiling module helps you find bottlenecks in your own code. Macro authors get full hygiene support, and error handling improves with catchable VM errors and cleaner stack traces.

Breaking Changes

  • sigil-web: Renamed UI components from sigil-* to sg-* prefix (e.g., sigil-button becomes sg-button) to mirror data-sg-* attributes

Performance

Profiling revealed hotspots in list operations during macro expansion. Native C implementations of assoc, assq, assv, member, memq, and memv deliver significant speedups:

  • Test suite runs 2x faster (33s → 15s)
  • Module compilation 50% faster
  • Release binaries 44% smaller (3.2MB → 1.8MB)

Macro Hygiene

Full hygienic macro support for syntax-rules and syntax-case. Macros now properly respect lexical scope, preventing accidental variable capture:

  • Local and module-level hygiene
  • free-identifier=? for comparing identifiers during expansion
  • #' reader shorthand for (syntax ...) expressions
  • New (sigil syntax) module with syntax->datum, datum->syntax, generate-temporaries

Error Handling

  • Catchable VM errors: guard can now catch type errors, division by zero, and out-of-bounds access. Use the new vm-error? predicate in (sigil error).
  • Better stack traces: Errors display file paths with line and column positions. Anonymous lambda frames are filtered by default for cleaner output.

Profiling

New (sigil profile) module for performance analysis:

(with-profiling
  (my-expensive-operation))
(profile-report)  ; Shows call counts, timing, allocations

Library Improvements

  • sigil-http: Keyword arguments for make-http-server and http-serve (port:, host:, handler:)
  • sigil-sxml: New sxml->html for converting SXML trees to HTML strings
  • sigil-version: New package for semantic version parsing and comparison

Bug Fixes

  • Fixed source file names in error stack traces
  • Fixed socket API consistency across platforms
  • Fixed with-api-docs path resolution in sigil-publish
  • Fixed sigil-irc package dependency declaration

0.3.0

This release focuses on modular compilation and improved application bundling. Native dependencies like SQLite, TLS, and crypto are now separate packages, so applications only compile what they actually use. The new sigil publish command provides a complete static site generation pipeline, and bundling gains script mode support for simpler deployments.

Breaking Changes

  • sigil-http: API review changes including lazy TLS loading for compile-time independence

Modular Native Packages

Heavy native dependencies have been extracted from sigil-lib into separate packages:

PackageDescription
sigil-sqliteSQLite database bindings
sigil-tlsTLS/SSL support
sigil-cryptoCryptographic functions

Applications that don't use these features no longer compile them, resulting in smaller binaries and faster builds.

Application Bundling

  • Script mode: Bundle single-file scripts with sigil bundle script.sgl
  • Custom runtimes: Build tailored runtimes with only needed native modules
  • Native package detection: Automatic entrypoint generation for native dependencies
  • Production runtime (sigil-run): Script mode support for bundled applications

New Command: sigil publish

Static site generation with a pipeline-based architecture:

(site output-dir: "dist"
      pipeline: (list markdown-processor syntax-highlighter)
      watch-patterns: '("content/**/*.md" "templates/**/*.html"))

Site operators integrate with the threading macro for composable pipelines.

Library Improvements

  • sigil-json: Added json-get-in for nested access
  • sigil-stdlib: New conveniences and predicates from API review
  • sigil-args: New features and comprehensive documentation
  • sigil-test: Native test build integration and new assertions
  • sigil-build: Consolidated shared code and improved test support

Bug Fixes

  • Fixed temp file creation on Windows (sigil-lib)

0.2.0

This release focuses on improving the idiomatic functional programming style of Sigil. New modules for sequences and function composition make it easier to write clean, composable code. Pattern matching gains dict, vector, and struct support, and several APIs have been refined for consistency.

Breaking Changes

  • Module renamed: (sigil exceptions) → (sigil error)
  • API changes: map and for-each now operate on single lists only. Use list-map and list-for-each for multi-list operations.
  • Renamed functions: remove-directory → delete-directory, channel-close → channel-close!

New Modules

ModuleDescription
(sigil random)Random number generation, shuffling, and sampling
(sigil seq)Polymorphic sequences and transducers
(sigil array)Efficient fixed-size collections
(sigil fn)Function composition: compose, pipe, partial, juxt

Pattern Matching Improvements

  • Dict pattern matching: (match d ((dict a: x b: y) ...))
  • Vector literal syntax: #(a b c) patterns
  • New forms: match-lambda, match-lambda*
  • Struct and vector pattern support

Other Enhancements

  • Unicode case conversion: char-upcase, char-downcase, string-upcase, string-downcase with full Unicode support
  • HTTP client: Added chunked transfer encoding support
  • CLI: sigil cli upgrade now uses native HTTP client (no curl dependency)
  • Dict utilities: dict-select, dict-rename-keys, dict-get-in for nested access
  • Threading: some-> macro for nil-short-circuiting pipelines

0.1.0

The first public release of Sigil, a practical Scheme for building standalone applications.

NOTE: Sigil is still highly experimental and changing fast, but don't let that stop you from trying it out and providing feedback!

Highlights

  • Standalone executables: Bundle your application with all dependencies into a single binary (~1-2MB base size)
  • Fast startup: Bytecode VM with incremental garbage collection
  • Hygienic macros: Full syntax-rules with syntax-case support
  • Batteries included: JSON, HTTP client/server, sockets, process management, and more

Core Runtime (sigil-lib)

The foundation: bytecode VM, S-expression reader, incremental mark-sweep GC, and native bindings for I/O, filesystem, networking, and process management.

Standard Library (sigil-stdlib)

Nearly complete R7RS base with practical extensions:

  • (sigil core) - Pattern matching, records, delimited continuations, array/dict utilities
  • (sigil math) - Trigonometric, exponential, and bitwise operations
  • (sigil string) - String manipulation utilities
  • (sigil path) - Cross-platform path handling
  • (sigil fs) - Filesystem operations
  • (sigil io) - Extended I/O primitives
  • (sigil process) - Process spawning and pipes
  • (sigil time) - Date/time operations
  • (sigil exceptions) - Exception handling
  • (scheme *) - R7RS compatibility modules

Build System (sigil-build)

Package-based workspace architecture with:

  • Dependency resolution across workspace packages
  • Multiple build configurations (dev, debug, release)
  • Executable bundling with embedded bytecode archives

CLI Tools (sigil-cli)

Development workflow commands:

  • sigil build - Compile packages and dependencies
  • sigil bundle - Create standalone executables
  • sigil test - Run test suites
  • sigil repl - Interactive development
  • sigil format - Code formatting
  • sigil changes - Release management
  • sigil cli - Install and manage Sigil installations

Libraries

PackageDescription
sigil-jsonJSON parsing and serialization
sigil-httpHTTP/1.1 client and server with TLS support
sigil-socketTCP/UDP networking
sigil-websocketWebSocket client and server
sigil-sqliteSQLite database bindings
sigil-gitGit repository operations
sigil-replInteractive REPL with readline
sigil-nreplNetwork REPL for editor integration
sigil-testTest framework with assertions
sigil-formatS-expression code formatter
sigil-markdownMarkdown parsing with frontmatter
sigil-sxmlSXML document construction
sigil-ansiTerminal colors and formatting
sigil-argsCommand-line argument parsing
sigil-hooksExtensible hook system
sigil-mcpModel Context Protocol server
sigil-jwtJWT token encoding and verification
sigil-docsDocumentation generation
sigil-webWeb application framework
sigil-publishStatic site generation pipeline
sigil-ircIRC client
sigil-cssCSS generation from Sigil code
Note: Library APIs may change between minor releases until 1.0. Core modules (sigil-lib, sigil-stdlib) aim for stability.

Platform Support

PlatformArchitectureStatus
Linuxx86_64, aarch64Primary development platform
macOSarm64, x86_64Builds and runs, not yet extensively tested
Windowsx86_64Builds and runs, not yet extensively tested
FreeBSDx86_64Cross-compiles, not yet tested on real hardware
OpenBSDx86_64Cross-compiles, not yet tested on real hardware
NetBSDx86_64Cross-compiles, not yet tested on real hardware
Web/WASM-Runs in browser via Emscripten

Known Limitations

  • R7RS Small coverage is not yet complete; full compliance planned for a future release
  • Limited SRFI coverage

Getting Started

See the Getting Started guide for installation and first steps.