Sigil Releases
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 installno 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.shruns. It invoked abootstrap/Makefilethat does not exist; it now invokes the top-levelMakefile.
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-promptbody 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-valueresolves against the caller's module. Broken since 0.15.0: once module initialization had finished, looking up a macro binding withsyntax-local-valuefrom 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.macroexpandresolves against the caller's module. The same staleness, one function over.(macroexpand '(my-macro ...))called at runtime from the module that definesmy-macronow expands it instead of returning it untouched.dynamic-windcleanup runs through an intervening prompt. Since the 0.17.5 exception-cleanup hardening, adynamic-windafterthunk was skipped when an exception unwound through acall-with-promptthat 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 adynamic-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
yieldinside 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 adynamic-windspanning 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_FACTORto trade a little collection frequency for a lower steady-state heap,(gc-idle-hint!)to collect between requests, and an opt-inmalloc_trimafter 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 evalthat 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-runreturned 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), exportingdom-*(sigil browser websocket), exportingwebsocket-*(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 bysigil-r7rs. The duplicate copy was removed fromsigil-stdlibso the R7RS boundary is clean. Thecaar/cadrfamily 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 initapp failedsigil deps installwith 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-socketpackage release that uses them is still pending. Async socket code picks up the new behavior when the companionsigil-socket0.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 declaresc-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:
| Repository | Contents |
|---|---|
codeberg:sigil/sigil-lang | sigil-lib (C runtime), sigil-stdlib (standard library), compiler |
codeberg:sigil/sigil-build | sigil-package, sigil-deps, sigil-build (build system + manifest + dep resolver) |
codeberg:sigil/sigil-ansi | terminal styling |
codeberg:sigil/sigil-args | command-line argument parsing |
codeberg:sigil/sigil-crypto | cryptography primitives (vendored mbedTLS) |
codeberg:sigil/sigil-docs | documentation generation |
codeberg:sigil/sigil-ffi | foreign function interface |
codeberg:sigil/sigil-format | code formatting |
codeberg:sigil/sigil-git | git integration |
codeberg:sigil/sigil-hooks | lifecycle hooks |
codeberg:sigil/sigil-http | HTTP client + server |
codeberg:sigil/sigil-json | JSON parsing + encoding |
codeberg:sigil/sigil-log | structured logging |
codeberg:sigil/sigil-lsp | language server |
codeberg:sigil/sigil-mcp | MCP (Model Context Protocol) library |
codeberg:sigil/sigil-nrepl | network REPL protocol |
codeberg:sigil/sigil-repl | REPL library |
codeberg:sigil/sigil-socket | socket primitives |
codeberg:sigil/sigil-sqlite | SQLite (vendored amalgamation) |
codeberg:sigil/sigil-store | content-addressed storage |
codeberg:sigil/sigil-test | test runner |
codeberg:sigil/sigil-tls | TLS layer (consumes sigil-crypto) |
codeberg:sigil/sigil-version | version helpers |
codeberg:sigil/sigil-web | web 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 forquasisyntax,unsyntax, andunsyntax-splicing` (matching R6RS). - Quasisyntax expansion — full hygienic template expansion lands. Templates can use
unsyntaxto splice values back into a syntax object during macro expansion. syntax-local-value— R6RS compile-time environment query, available inside macros.syntax/#'outsidesyntax-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-caseellipsis patterns — fixed a long-standing bug where the ellipsis bound to the wrong template variable in nested patterns.
Build system and dependency resolver
The dep resolver fails loudly where it used to fail silently. The 0.13.x resolver had several "report success and break the consumer at build time" failure modes; those are gone. New behavior is loud and named: when a fetch errors, when a lockfile entry diverges from the manifest, when a config name doesn't exist. The motivating goal: turn unreproducible build mysteries into immediate, actionable errors at the source.
- Lock-entry staleness —
sigil deps installnow invalidates a lock entry when itsfrom-gitURL or package selector drifts from the manifest, not just when its version range changes. Previously, swapping a from-git dep's URL while keeping the same name would silently reuse the old SHA from the lockfile. - Loud-fail on any fetch error —
sigil deps installnow exits non-zero when any fetch errors out. Previous behavior printed the error in red but still returned success and wrote the lockfile. --configvalidation —sigil build --config <name>now fails loudly when the config name is undeclared inpackage.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-craftertheme 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_BINDINGcache-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.cwere removed after the underlyingcase-lambdahygiene bug was fixed in the syntax-case path. - Visible
libsigil.abuild progress — sigil-build's libsigil compile path now prints one line per call (libsigil.a: cache hit (...)orBuilding 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-namefalls back tofrom-git-package— previously, deps installed via(from-git url: ... package: ...)shape resolved to a name of#fin error messages.fetch-sglsymlink race —create-package-symlinknow tolerates the dangling-symlink edge where a previousdelete-fileraced withfile-exists?returning true.- WASM-incompatible code excluded from WASM builds —
sigil-lib'ssetjmp-based exception scaffolding andtzset()-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 installwould 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
filterTextandtextEditfor editors like eglot - Go-to-definition — jumps to the exporting
.sglsource line; descends into library re-exports - Formatting — full-document and range formatting via
(sigil format) - Quick-fix code actions —
suggest-importproposes imports for undefined bindings andremove-unused-importcleans 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/testtool (was inheriting sigil-mcp's environment) - Per-server
--log/--log-levelflags for traceable multi-worker setups - JSON-RPC
id: nullcompliance 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_argspointer 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
guardforms. - Compiler self-bootstrap — 0.12.0 compilers can now build 0.13.0 source without guarded-reference crashes (
SIGIL_ABI_VERSIONreference is#ifdef-gated).
0.12.0
Register VM stabilized, stack VM removed, and experimental native C codegen. This release makes the register-based virtual machine the sole execution backend — the legacy stack VM and direct compiler have been removed. The register VM is now battle-tested across production MCP servers (folio, courier, bureau) and the full Sigil ecosystem. This release also introduces an experimental C native code generation backend.
Stack VM Removal
The stack-based virtual machine and direct compiler have been removed. The CPS compiler and register VM are now the only toolchain — no opt-in flags needed.
- Removed stack VM interpreter, direct compiler, and CPS stack emitter
- Removed mixed-mode dispatch
- Renamed opcodes and headers (
ROP_→OP_,regvm.h→opcodes.h) - SGB format v8+ only (older binaries rejected)
Register VM Stabilization
The register VM introduced as opt-in in 0.11.0 is now the default and only backend. It has been stabilized through extensive testing and production use:
- All ecosystem tests pass (~2,700 tests across 23 repos)
- Running in production as the backend for MCP servers (folio, courier, bureau, minder)
- 20+ bug fixes addressing edge cases in exception handling, macro hygiene, continuations, and module loading
Experimental Native C Codegen
New --backend=native flag for sigil build compiles Sigil programs to C, then to native binaries via the Zig toolchain. This is opt-in and experimental.
The native codegen compiles CPS IR directly to C functions with a trampoline-based calling convention. All modules — entry and dependencies — are compiled to native C. Features:
- Full-program native compilation — all modules compiled to C, linked into a single binary
- Module system support — cross-module calls, binding caches, module initialization
- Delimited continuations — fiber-based C stack capture via ucontext (Linux/glibc)
- Async goroutines — native fibers for concurrent I/O (scheduler, stdin read loops)
- GC integration — shadow stack for local variables, conservative C stack scanning, trampoline closure rooting
- MCP server support — folio and courier run as native MCP servers through Claude Code
87 of 91 native differential test files pass (matching bytecode output). The native backend is functional for real applications and delivers significant performance improvements over the bytecode VM.
Platform Limitations
- Linux (glibc): Full support including async fibers
- Linux (musl): Builds and runs, but no fiber support (musl ucontext stubs return ENOSYS)
- macOS: Builds and runs, but no fiber support (ucontext is deprecated)
- Windows: Builds and runs, but no dynamic native module initialization
Async goroutines require fiber support. On platforms without it, persistent servers (MCP, HTTP) that use goroutines for I/O will not work in native mode.
Bug Fixes
- Fix macro hygiene: literal comparison and free identifier resolution
- Fix
let*variable scope leak causing GET_CELL crash - Fix
raise-continuablein 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-librarybody defines leaking to top level - Fix
guard/with-exception-handleracross eval boundaries and tail calls - Fix
collect_internal_definesand capture analysis for module bodies - Fix MCP server crash on
resources/templates/listrequests
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 opcodescps-emit-reg.c-- CPS IR to register bytecode emission
Performance
Benchmarks across 60+ tests (ratio = register VM time / direct compiler time, lower is faster):
| Category | Ratio | Improvement |
|---|---|---|
| Compute (fib, tak, ack) | 0.79-0.93x | 7-21% faster |
| Closures | 0.56-1.03x | Up to 44% faster |
| Tail calls | 0.76-0.96x | Up to 24% faster |
| String processing | 0.43-0.90x | Up to 57% faster |
| Allocation/trees | 0.80-1.01x | Up to 20% faster |
| Higher-order functions | 0.72-1.03x | Up to 28% faster |
| JSON encode/decode | ~1.0x | Parity (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=1flag activates CPS + register VM togetherSIGIL_MODULE_MAX_MACROSincreased 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 allocatorcps-convert.c-- S-expression to CPS conversioncps-emit.c-- CPS to bytecode emissioncps-opt.c-- optimization pass pipelinecps-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 optimizationSIGIL_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-expandimplemented as an expression form in the CPS convertercase-lambdaCPS 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-librarybody - Fix
make-vectorto use native call instead of primitive opcode makedefault target now builds stdlib (previously requiredmake 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 installreads the lockfile and uses pinned refs for reproducible installssigil deps updateignores the lockfile, re-resolves frompackage.sglranges, and writes a fresh lockfile--redirectsskips 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/semverandsigil/deps/lockto 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 Package | Repository | Description |
|---|---|---|
| sigil-peg | codeberg.org/sigil/sigil-peg | PEG parser combinators |
| sigil-sxml | codeberg.org/sigil/sigil-sxml | SXML to HTML/XML conversion |
| sigil-markdown | codeberg.org/sigil/sigil-markdown | CommonMark parser with YAML front matter |
| sigil-web | codeberg.org/sigil/sigil-web | Web application framework |
| sigil-css | codeberg.org/sigil/sigil-css | Programmatic CSS generation |
| sigil-match | codeberg.org/sigil/sigil-match | Pattern matching |
| sigil-fp | codeberg.org/sigil/sigil-fp | Functional programming utilities (compose, pipe, partial, threading macros) |
| sigil-transducers | codeberg.org/sigil/sigil-transducers | Composable transducers |
| sigil-r7rs | codeberg.org/sigil/sigil-r7rs | R7RS-small compatibility (scheme *) modules and SRFI-9 records |
| sigil-web-repl | codeberg.org/sigil/sigil-web-repl | Browser-based REPL (was sigil-playground) |
| press | codeberg.org/sigil/press | Static site generator (was sigil-publish) |
| sigil-site | codeberg.org/sigil/sigil-site | Official Sigil website |
| sigil-web-demo | codeberg.org/sigil/sigil-web-demo | Demo 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 tosigil-match - Transducers (
mapping,filtering,transduce) — moved tosigil-transducers - Functional combinators (
compose,pipe,curry,partial,cut/cute) — moved tosigil-fp - Threading macros (
chain,->,some->) — moved tosigil-fp - R7RS compatibility (
(scheme base),(scheme write), etc.) — moved tosigil-r7rs - SRFI-9 records (
define-record-type) — moved tosigil-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-compilerandcompile-native-testspreferzig ccwhen available, falling back to systemcc/gcc - Cross-compilation: All eight platform targets use Zig (unchanged from 0.8)
- CI: Test and GC stress jobs use Zig via
with-zigwrapper - WebAssembly: New
webbuild config useszig 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_shimfor 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)— thesigil docsCLI subcommand. Press handles all doc site generation now. - Removed
(sigil docs execute)— code block execution for doc validation - Removed dependencies on
sigil-markdownandsigil-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 docscommand (handled by press) - Removed
sigil publishcommand (handled by press) - Removed
sigil changescommand (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-callbackcreates 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#fto 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 serverssigil app list— show installed applicationssigil app update— pull latest changes and rebuildsigil app remove— clean up installed app and deregister MCP serverssigil 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
| Package | Description |
|---|---|
sigil-ffi | Dynamic FFI for calling C libraries at runtime via dyncall |
sigil-cairo | Cairo 2D graphics bindings — paths, transforms, surfaces, PNG loading, compositing operators, glyph rendering |
sigil-freetype | FreeType font loading and glyph metrics via FFI |
sigil-harfbuzz | HarfBuzz text shaping via FFI |
sigil-raylib | Raylib game development bindings — 2D shapes, text, input, collision, audio |
sigil-caldav | CalDAV calendar client with iCalendar parsing, event CRUD, recurrence expansion, timezone handling |
sigil-telegram | Telegram 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-logwithlog-configure-from-args!for CLI integration - Better error reporting: Stack traces included in MCP tool error responses
HTTP Improvements
/jsonroute helpers now return JSON bodies on all HTTP status codes, not just 2xxurl-encode-valueandbuild-query-stringfor 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-Lengthusing 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-IDexception 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-staticandprerelease-static
JMAP Improvements
jmap-email-countfor lightweight email counting without fetching messagesjmap-download-blobfor 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-unzipto use simultaneous traversal - Deduplicated
inspect-vectorandinspect-arraywith shared helper - Dict-based lookups in diagnostic module and watch module for faster type-guard and mtime resolution
Bug Fixes
- Fixed crash when
formatdisplays bignum values - Fixed segfault from unbounded macro export recursion
- Fixed preemptive yield escaping through
sigil_applyboundaries - Fixed
string->listto properly decode UTF-8 characters - Fixed
string-joinoverflow 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-tlspackage - Fixed Windows cross-compilation:
fcntl/FD_CLOEXECguards replaced withSetHandleInformation - Fixed OpenBSD cross-compilation:
setvbufworkaround for opaqueFILEstruct in Zig headers - Fixed OpenBSD executable path resolution, poll fd limit, and partial write handling
- Set
FD_CLOEXECon 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-checksfeature — zero cost in release builds - Predicate combinators:
any-of,maybe,list-of,none-offor expressive type constraints - Struct field annotations:
define-structfields 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
| Package | Description |
|---|---|
sigil-tui | Immediate-mode terminal UI toolkit with native C grid, layout system, components, event parsing, and text input |
sigil-jmap | JMAP email client (RFC 8620/8621) with session discovery, mailbox operations, email query/send |
sigil-yaml | YAML parsing and serialization with block/flow collections, block scalars, anchors/aliases, multi-document |
sigil-log | Structured 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 apifixed 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
_inlinefield name - Fixed Windows build: use
define-nativeforunix-connect(not available on Windows) - Fixed exception handler double-pop causing test runner segfault
- Fixed bootstrap compilation: replaced
whenwithifin%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,dosyntax,guardprompt 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-sharedwith datum labels for circular structure printing - Feature detection:
features,cond-expand,syntax-error - Libraries:
(scheme time),(scheme process-context),(scheme repl)withinteraction-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-selfor 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
| Package | Description |
|---|---|
sigil-peg | Parsing Expression Grammars with S-expression DSL, backtracking, and captures |
sigil-org | Org Mode parser producing SXML (headlines, metadata, source blocks, tables, lists) |
sigil-xmpp | XMPP 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
describeandmodulesoperations - Watch mode:
sigil build --watchandsigil test --watchwith--format jsonsupport - Value inspection: New
(sigil inspect)module for structured introspection via nREPL and MCP - Project templates:
sigil initgainsweb-app,api-service, andmcp-tooltemplates - 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_MAXlimit (increased to 2048) for projects with many native bindings - Fixed
--no-colorflag in test runner - Fixed
git-fetchto 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_COMPILATIONworkaround entirely. Fixed 46+ unrooted allocation sites in the compiler through systematic audit - GC stress testing:
SIGIL_GC_STRESS=1forces 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
displayandwrite - 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-removewith sharing, branch collapse, and HAMT-to-array demotion when count drops below 8
Error Handling
- Three
exit(1)calls in the module system replaced withsigil__vm_error()so the Scheme-level exception system handles them gracefully OP_ABORT_TO_PROMPTnow extracts real error messages from exception objects- Macro expansion errors propagate properly to the compiler
matchraises 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:5Multimedia Packages
sigil-studio has been split into three independent packages for more flexible dependency management:
| Package | Description |
|---|---|
sigil-app | Windowing, input, application lifecycle (sokol_app) |
sigil-graphics | 2D rendering, image loading, font rendering (sokol_gfx, stb) |
sigil-audio | Sound 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-flagsfield on library definitions--profileflag 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-refto return values instead of aliasingassq; addedassv-ref - Fixed
number->stringprecision for exact IEEE 754 roundtrip (was losing digits on large flonums) - Fixed REPL
string-lengtherror after exception recovery - Fixed cross-module symbol resolution corruption
- Fixed bundle directory listing for bundled applications (VFS)
- Fixed
sigil-runre-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-*tosg-*prefix (e.g.,sigil-buttonbecomessg-button) to mirrordata-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 withsyntax->datum,datum->syntax,generate-temporaries
Error Handling
- Catchable VM errors:
guardcan now catch type errors, division by zero, and out-of-bounds access. Use the newvm-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, allocationsLibrary Improvements
- sigil-http: Keyword arguments for
make-http-serverandhttp-serve(port:,host:,handler:) - sigil-sxml: New
sxml->htmlfor 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-docspath 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:
| Package | Description |
|---|---|
sigil-sqlite | SQLite database bindings |
sigil-tls | TLS/SSL support |
sigil-crypto | Cryptographic 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-infor 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:
mapandfor-eachnow operate on single lists only. Uselist-mapandlist-for-eachfor multi-list operations. - Renamed functions:
remove-directory→delete-directory,channel-close→channel-close!
New Modules
| Module | Description |
|---|---|
(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-downcasewith full Unicode support - HTTP client: Added chunked transfer encoding support
- CLI:
sigil cli upgradenow uses native HTTP client (no curl dependency) - Dict utilities:
dict-select,dict-rename-keys,dict-get-infor 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-ruleswithsyntax-casesupport - 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 dependenciessigil bundle- Create standalone executablessigil test- Run test suitessigil repl- Interactive developmentsigil format- Code formattingsigil changes- Release managementsigil cli- Install and manage Sigil installations
Libraries
| Package | Description |
|---|---|
| sigil-json | JSON parsing and serialization |
| sigil-http | HTTP/1.1 client and server with TLS support |
| sigil-socket | TCP/UDP networking |
| sigil-websocket | WebSocket client and server |
| sigil-sqlite | SQLite database bindings |
| sigil-git | Git repository operations |
| sigil-repl | Interactive REPL with readline |
| sigil-nrepl | Network REPL for editor integration |
| sigil-test | Test framework with assertions |
| sigil-format | S-expression code formatter |
| sigil-markdown | Markdown parsing with frontmatter |
| sigil-sxml | SXML document construction |
| sigil-ansi | Terminal colors and formatting |
| sigil-args | Command-line argument parsing |
| sigil-hooks | Extensible hook system |
| sigil-mcp | Model Context Protocol server |
| sigil-jwt | JWT token encoding and verification |
| sigil-docs | Documentation generation |
| sigil-web | Web application framework |
| sigil-publish | Static site generation pipeline |
| sigil-irc | IRC client |
| sigil-css | CSS 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
| Platform | Architecture | Status |
|---|---|---|
| Linux | x86_64, aarch64 | Primary development platform |
| macOS | arm64, x86_64 | Builds and runs, not yet extensively tested |
| Windows | x86_64 | Builds and runs, not yet extensively tested |
| FreeBSD | x86_64 | Cross-compiles, not yet tested on real hardware |
| OpenBSD | x86_64 | Cross-compiles, not yet tested on real hardware |
| NetBSD | x86_64 | Cross-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.