sigildocs

Sigil Releases

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 stalenesssigil 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 errorsigil 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 validationsigil 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 racecreate-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 buildssigil-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 actionssuggest-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.hopcodes.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., PUSHLOCAL+PUSHLOCAL, 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 (sokolaudio, stbvorbis)

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-directorydelete-directory, channel-closechannel-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.