{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Sigil News",
  "home_page_url": "https://usesigil.org/",
  "description": "Announcements, articles and releases from the Sigil project.",
  "language": "en",
  "feed_url": "https://usesigil.org/news/feed.json",
  "authors": [
    {
      "name": "David Wilson"
    }
  ],
  "items": [
    {
      "id": "https://usesigil.org/releases/0.23.0/",
      "url": "https://usesigil.org/releases/0.23.0/",
      "title": "Sigil 0.23.0",
      "date_published": "2026-10-21T00:00:00Z",
      "content_html": "<p>A memory and runtime release. Native and C-to-WebAssembly programs use less memory, idle programs wake less often, and a full Unix socket queue no longer stalls other fibers.</p><p><strong>Compatibility: native <code>current-jiffy</code> now counts microseconds. Code that assumes a fixed tick rate must use <code>(jiffies-per-second)</code> when converting ticks to seconds.</strong></p><h2>Owned allocator</h2><p>Static Linux x86_64 programs, including the shipped <code>sigil</code> command, and C-to-WebAssembly builds using Zig&apos;s WASI target now use mimalloc 3.5.3. In a fixed 18-minute room tour with the same Harkfell build, the owned allocator cut peak WebAssembly memory by about 213 MiB (31%).</p><p>Other targets keep their current allocator. To keep the stock allocator, add <code>allocator-stock</code> to your build config&apos;s features. <code>allocator-mimalloc</code> requests mimalloc explicitly and fails on an unsupported target. The first build after upgrading rebuilds the allocator because its identity is now part of the build cache.</p><p>The <code>sigil-allocator</code> query from <code>(sigil meta)</code> reports the running program&apos;s allocator. Native and WASI builds also write that identity in an <code>.allocator</code> file beside the output. See the <a href=\"https://usesigil.org/docs/guides/allocator/\">allocator guide</a> for configuration.</p><h2>Native clock units</h2><p>Native <code>current-jiffy</code> now counts microseconds, with 1,000,000 jiffies per second. Previously, the Unix clock could overflow the small-integer range after about 39 hours of machine uptime. The clock now stays exact, using a large integer when needed. Use <code>(jiffies-per-second)</code> to convert ticks to seconds instead of assuming a fixed rate.</p><h2>Fixes</h2><ul><li>Idle POSIX programs waiting on sockets, signals or timers woke every   100 ms even when nothing needed attention. They now sleep until   activity or the next deadline.</li><li>Connecting to a Unix socket with a full queue could block the whole   cooperative VM. <code>unix-connect</code> now parks only the caller and accepts   a <code>timeout-ms:</code> deadline, closing unfinished sockets on timeout or   exception.</li><li>Native <code>with-async</code> could return while external work was still pending.   It now waits for that work to finish.</li><li>Direct-WASM builds could reject module initializers whose branches   returned a mix of Boolean and numeric values. Those initializers now   compile.</li><li>Repeated imports could exhaust a module&apos;s import records, and imports   beyond the limit could go unrecorded. Equivalent imports now reuse   records; more than 128 distinct imports raises an error naming the   module and import, both when loading in the VM and when compiling.</li><li>Direct-WASM builds carried redundant copies and repeated cleanup code.   Measured Crash The Stack and Harkfell builds are now about 21% smaller   uncompressed, including their runtime companion.</li><li>Direct-WASM calls could miss updated bindings or providers registered   after the first lookup. Calls now see those changes.</li><li>Valid library graphs could be rejected because provider selection used   the wrong identity. Selection now uses the chosen provider&apos;s identity.</li><li>Supported UI calls in Direct-WASM builds could fail with a missing   native-leaf error. Those calls now compile.</li><li>Repeatedly adding the same library search directory could exhaust the   128-path limit. Adding it again now moves it to the front without   consuming another slot.</li><li>Browser initialization could fail to resolve native bridge calls.   Those calls now resolve through their module bindings.</li><li>Ordinary web apps could require a separate Direct-WASM host at startup.   They no longer export the companion-only entry points that caused it.</li><li>A bytecode <code>guard</code> could leave the VM in a broken return state when a   native callback raised an error. It now catches the error and resumes   its handler.</li><li>Identical WebAssembly runtime companion builds could produce different   bytes in different directories. Their output now matches.</li><li>Release downloads containing allocator sidecars could be rejected by   the old verifier. Verification now derives the expected files from the   checksum manifest and checks each binary&apos;s allocator identity.</li></ul>",
      "summary": "A memory and runtime release. Native and C-to-WebAssembly programs use less memory, idle programs wake less often, and a full Unix socket queue no longer stalls other fibers.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.17/",
      "url": "https://usesigil.org/releases/0.22.17/",
      "title": "Sigil 0.22.17",
      "date_published": "2026-10-15T00:00:00Z",
      "content_html": "<p>Sigil now manages its own memory allocator. Native programs built for static Linux x86_64 and C-to-WASM web builds use mimalloc 3.5.3 behind a new allocator interface, and the <code>sigil</code> command itself switches with them. Long-running programs hold much less memory, and web builds start smaller and grow less.</p><p><strong>Existing projects build without changes.</strong> The first build after upgrading rebuilds each target&apos;s allocator once, because the allocator is now part of a build&apos;s cache identity.</p><h2>Sigil manages its own allocator</h2><p>Memory requests from Sigil&apos;s runtime and from bundled C code such as SQLite now go through one allocator interface that Sigil owns, instead of whatever the C library provides. On static Linux x86_64 builds and C-to-WASM web builds, that interface is backed by a pinned copy of mimalloc 3.5.3.</p><p>In our measurements, the allocator was the difference:</p><ul><li>A Slate server running for 15 minutes ended at about 125 MiB of   resident memory, against 237 to 258 MiB with the previous allocator.   The program&apos;s own live data was the same in every run; the difference   was memory the old allocator kept but no longer used.</li><li>Harkfell&apos;s web build started at 85 MiB of WASM memory instead of 100,   and peaked at 177 to 201 MiB over a 15-minute session instead of 235 to<ol><li>WASM memory never shrinks, so a lower peak matters most on phones.</li></ol></li></ul><p>The shipped <code>sigil</code> command for Linux x86_64 is one of these static builds, so it uses mimalloc as well.</p><h2>Web builds get tuned settings automatically</h2><p>A default mimalloc reserves a large block of memory up front, which on the web made things worse, not better. C-to-WASM builds therefore get three compile-time settings automatically: <code>MI_FREE_USE_PAGEMAP=1</code>, <code>MI_DEFAULT_ARENA_RESERVE=4096</code> and <code>MI_ARENA_SLICE_SHIFT=14</code>. They&apos;re upstream&apos;s own options, and the first is upstream&apos;s recommendation for WASI. You don&apos;t need to set anything.</p><h2>Choosing the allocator</h2><p>To keep the previous allocator for a build, add the <code>allocator-stock</code> feature:</p><pre>(build\n  name: &apos;web-stock\n  target: &quot;wasm32-wasi&quot;\n  features: &apos;(release allocator-stock))</pre><p><code>allocator-mimalloc</code> requests mimalloc explicitly, and fails on a target where it isn&apos;t qualified yet. WASI builds can also pass typed <code>MI_*</code> settings; unknown names are rejected. Details are in the <a href=\"https://usesigil.org/docs/guides/allocator/\">allocator guide</a>.</p><p>The new <code>sigil-allocator</code> export from <code>(sigil meta)</code> reports which allocator a running program uses.</p><h2>What stays the same</h2><ul><li>Other native targets (Linux arm64, macOS, Windows, Android, the BSDs)   keep their current allocator until each is qualified.</li><li>Direct-WASM builds keep their own allocator.</li><li>Garbage collection sizing and trimming are unchanged.</li><li>The Zig SDK allocator fix from 0.22.16 stays in place wherever   mimalloc isn&apos;t used.</li></ul>",
      "summary": "Sigil now manages its own memory allocator. Native programs built for static Linux x86_64 and C-to-WASM web builds use mimalloc 3.5.3 behind a new allocator interface, and the sigil command itself switches with them. Long-running programs hold much less memory, and web builds start smaller and grow less.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.16/",
      "url": "https://usesigil.org/releases/0.22.16/",
      "title": "Sigil 0.22.16",
      "date_published": "2026-10-13T00:00:00Z",
      "content_html": "<p>A bug-fix release for compilation, long-running programs and audio. The compiler keeps its intermediate values alive through garbage collection, completed child processes release their backing memory, and async producers and consumers resume correctly. Cues and streams also share output protection in the browser.</p><p><strong>Existing projects build without changes.</strong></p><h2>Fixes</h2><ul><li>Garbage collection during compilation could leave stale values in the   compiler&apos;s intermediate representation and corrupt saved module hints.   Those values now stay rooted until the compiler finishes using them.</li><li>Completed child processes now release their backing memory as well as   their handles, preventing memory growth in programs that repeatedly   spawn children.</li><li>A producer or consumer passed to <code>call-with-values</code> can now yield and   resume with its returned values intact, including through a first-class   reference to <code>call-with-values</code>.</li><li>Sigil-managed Zig 0.16 SDKs now search the allocator&apos;s CPU partitions   for reusable slots before mapping more memory. Distro and user Zig   installations on PATH stay usable without changes to their SDKs.</li><li>Browser audio now limits the combined output after cues, streams and   one-shot sounds are mixed. A cue over a loud stream stays at or below   full scale; quiet output stays unchanged. The matching native fix is   in sigil-audio 0.13.1.</li></ul>",
      "summary": "A bug-fix release for compilation, long-running programs and audio. The compiler keeps its intermediate values alive through garbage collection, completed child processes release their backing memory, and async producers and consumers resume correctly. Cues and streams also share output protection in the browser.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.15/",
      "url": "https://usesigil.org/releases/0.22.15/",
      "title": "Sigil 0.22.15",
      "date_published": "2026-10-11T00:00:00Z",
      "content_html": "<p>A bug-fix release for closures and long-running programs. Capturing a conditional internal definition no longer breaks a closure, pipe readers keep moving on Linux static builds and Android, and short file reads no longer leave the garbage collector&apos;s allocation totals growing.</p><p><strong>Existing projects build without changes.</strong></p><h2>Conditional definitions and closures</h2><p>An internal <code>define</code> inside <code>when</code>, <code>unless</code>, <code>if</code>, or <code>cond</code> could fail with &quot;GET_CELL: expected upvalue cell&quot; when a lambda captured its binding. The binding now gets its cell before either branch runs, and redefining it updates the same cell so existing closures see the new value. Initializers still run only when their branch runs.</p><p>The <code>sigil/lint</code> tool now warns about this pattern. It works, but an explicit <code>let</code> binding or a definition before the conditional is easier to follow.</p><h2>Fixes</h2><ul><li>Pipe readers on Linux static builds and Android could stall after   reading one byte while more bytes waited in libc&apos;s buffer. Inherited   stdin and subprocess stdout and stderr now stay readable until all   available bytes have been read, even when the writer waits for a reply.</li><li>Short file reads could inflate GC allocation accounting and make   long-running programs keep using more memory. Bytevectors are now   accounted for by the bytes actually read.</li><li>Repeatedly adding the same library search path no longer consumes   another slot. Adding it again still moves it to the front, so lookup   priority stays the same.</li></ul>",
      "summary": "A bug-fix release for closures and long-running programs. Capturing a conditional internal definition no longer breaks a closure, pipe readers keep moving on Linux static builds and Android, and short file reads no longer leave the garbage collector's allocation totals growing.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.14/",
      "url": "https://usesigil.org/releases/0.22.14/",
      "title": "Sigil 0.22.14",
      "date_published": "2026-10-10T00:00:00Z",
      "content_html": "<p>A bug-fix release. <code>syntax-rules</code> now handles nested ellipses, <code>sigil build --watch</code> works again, and Sigil behaves better in scripts: no color codes in piped output, and clean version info.</p><p><strong>Existing projects build without changes.</strong></p><h2>Nested ellipses in <code>syntax-rules</code></h2><p>Patterns such as <code>(_ (name value ...) ...)</code> never matched, so macros in the R7RS style that repeat inside a repetition couldn&apos;t be written. They now match and expand, including empty inner lists, and a template that uses a pattern variable at the wrong depth gets a clear error.</p><h2>Fixes</h2><ul><li><code>sigil build --watch</code> stopped right after the first build with   &quot;unbound variable &apos;build-watch-loop&apos;&quot;. Watch mode works again.</li><li>Sigil no longer writes color codes when its output goes to a file or   a pipe, so scripts can read <code>sigil --version</code> cleanly. It also   honors <code>NO_COLOR</code>.</li><li>A CLI or native app built with gcc could show garbage in   <code>--version</code> and its app metadata.</li><li>Builds failed with &quot;cdr: expected pair&quot; when a <code>musl-gcc</code> wrapper was   on <code>PATH</code>.</li><li>Dev builds made with the managed Zig toolchain compiled Sigil&apos;s   runtime in debug mode with undefined-behavior checks, which made   them slower than they needed to be. The runtime is now built with   optimization in dev builds too (debug info is kept); your own code   still follows the config&apos;s <code>optimize</code> setting. If you want the   undefined-behavior checks back, put them in your config&apos;s <code>c-flags</code>   (for example <code>-O0 -fsanitize=undefined</code>); Sigil passes them through   unchanged.</li></ul>",
      "summary": "A bug-fix release. syntax-rules now handles nested ellipses, sigil build --watch works again, and Sigil behaves better in scripts: no color codes in piped output, and clean version info.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.13/",
      "url": "https://usesigil.org/releases/0.22.13/",
      "title": "Sigil 0.22.13",
      "date_published": "2026-10-06T00:00:00Z",
      "content_html": "<p>A bug-fix release. The headline: since 0.22.10, a non-ASCII character in any manifest string (an em-dash in a <code>description:</code>, say) broke <code>sigil deps</code> for every project that read it. That&apos;s fixed, along with a build hang, a few native string miscompiles and <code>--dry-run</code>.</p><p><strong>Existing projects build without changes.</strong> Crash The Stack, Harkfell and Phantom Burn build and pass their own checks on this release.</p><h2><code>write</code> and <code>read</code> agree on non-ASCII text</h2><p><code>write</code> escaped each byte of a non-ASCII character with no closing <code>;</code>, so <code>read</code> couldn&apos;t read its own output back. That&apos;s what broke manifests. <code>write</code> now prints printable non-ASCII text as itself (<code>(write &quot;café — λ&quot;)</code> prints <code>&quot;café — λ&quot;</code>), escapes control characters as valid <code>\\x&lt;hex&gt;;</code> escapes, and everything it prints reads back unchanged.</p><p>A lock file written by 0.22.13 with non-ASCII text in it won&apos;t pass 0.22.12&apos;s identity check, so upgrade every machine that shares one.</p><h2>Fixes</h2><ul><li><code>sigil build</code> could hang forever when a compiler or linker wrote a   lot of warnings to stderr. Builds now read both output streams   together.</li><li>Native and WebAssembly builds could compile some string constants   wrong: <code>&quot;a\\x0;12&quot;</code> came out as <code>&quot;a\\n&quot;</code>, and <code>&quot;\\x1B;7&quot;</code> didn&apos;t   compile. String constants now match the bytecode VM byte for byte.</li><li><code>sigil build --dry-run</code> actually built everything. It now only   reports what each step would do.</li><li>The store couldn&apos;t detect a changed item on macOS and BSD (it relied   on GNU <code>find</code>), so a modified item could be served as verified.</li><li><code>--no-bundle</code> native builds left out native package initializers,   so for example <code>sha256</code> from <code>(sigil crypto)</code> was unbound.</li><li>A <code>--no-bundle</code> web build of an app with a native dependency that   has no WebAssembly code (sigil-audio, for one) failed with &quot;Failed   to link dev runtime&quot;. Web builds now link only what was built for   them.</li><li>Managed Zig picked up C++ include paths when compiling C, which broke   builds inside Guix and Nix shells.</li><li>Web builds could write invalid JSON or HTML for names containing   control characters or quotes.</li><li><code>do</code> now accepts a variable with no step expression, as R7RS allows.</li><li>The bootstrap CLI&apos;s <code>sigil --version</code> printed garbage.</li></ul><h2>Known issues</h2><ul><li><code>syntax-rules</code> doesn&apos;t match nested ellipses yet: a pattern such as   <code>(_ (name value ...) ...)</code> never matches.</li></ul>",
      "summary": "A bug-fix release. The headline: since 0.22.10, a non-ASCII character in any manifest string (an em-dash in a description:, say) broke sigil deps for every project that read it. That's fixed, along with a build hang, a few native string miscompiles and --dry-run.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.12/",
      "url": "https://usesigil.org/releases/0.22.12/",
      "title": "Sigil 0.22.12",
      "date_published": "2026-10-01T00:00:00Z",
      "content_html": "<p>A correctness release. Sigil ignored a program&apos;s own binding of a builtin&apos;s name, such as a local <code>not</code> or an internal <code>(define (car x) ...)</code>, on every backend, and several older bugs mixed up local names with macro keywords. Both are fixed. Release configs also compile optimized C by default now.</p><p><strong>Existing projects build without changes.</strong> Code that doesn&apos;t rebind a builtin compiles exactly as before: the release and web builds of Crash The Stack, Harkfell and Phantom Burn are byte-identical to 0.22.11&apos;s.</p><h2>A binding of a builtin&apos;s name is used, not the builtin</h2><p>Sigil compiled a call to <code>car</code>, <code>not</code>, <code>+</code>, <code>list</code>, <code>raise</code>, <code>call/cc</code> and about sixty other builtins straight to the builtin whenever the operator was spelled that way, whatever the program had bound to the name. So <code>(let ((not (lambda (x) 42))) (not #f))</code> gave <code>#t</code>, an internal <code>(define (car x) 99)</code> was ignored, and <code>((lambda (car) (car 5)) (lambda (x) 77))</code> raised <code>car: expected pair</code>. The same happened with a library&apos;s own definitions, with names imported from a library other than <code>(sigil core)</code>, and in the macro expander, where a parameter named like a macro was expanded as the macro: <code>(define (g twice) (twice 3))</code> expanded a <code>twice</code> macro instead of calling its argument. The bytecode VM, native builds and WebAssembly all gave the same wrong answers. Now a binding the program introduces wins, apart from two cases involving a macro defined inside a procedure body (see Known issues), and code that doesn&apos;t rebind a builtin compiles exactly as before: every compiled module of the standard library and the CLI is byte-identical to 0.22.11&apos;s apart from the two below.</p><p>If your code relied on the old behavior, <code>SIGIL_DEBUG_PRIMITIVE_SHADOW=1</code> prints each call to a builtin&apos;s name that now goes to your own binding, with the reason. Two places in Sigil itself did: the MCP server&apos;s project overview used its own <code>keyword?</code> from before keywords were their own type, so it never showed a project&apos;s name, description, dependencies or packages (it does now), and <code>(sigil test)</code> had its own <code>list?</code> that never ran.</p><h2>Local bindings named like macro keywords</h2><p>Three older bugs in the same area are fixed. A macro whose expansion uses <code>when</code> failed with <code>syntax-rules: no matching pattern</code> when the caller had a local named <code>when</code>, as in <code>(let ((when 1)) (m))</code>: <code>syntax-rules</code> now ignores the keyword position of a pattern, as R7RS says. After <code>(let () (define foo ...) ...)</code>, a later <code>foo</code> in the same procedure read the let&apos;s variable and failed with <code>call: not a procedure</code>; it now refers to whatever <code>foo</code> means outside the let. And <code>with-exception-handler</code> passed as a value (through <code>apply</code>, or bound to another name) now installs a handler that <code>raise</code> and <code>raise-continuable</code> reach, exactly like a direct call.</p><h2>Release builds compile optimized C by default</h2><p>A config that leaves out <code>optimize:</code> now builds at level 2 when it is a release config: one named <code>release</code>, <code>release-static</code>, <code>prerelease</code>, <code>prerelease-static</code> or <code>web</code>, or one that says <code>debug?: #f</code>. Before, it compiled its C without <code>-O</code> and skipped the optimizer: a web app&apos;s runtime came out 6.6 MB and about four times slower than with <code>optimize: 2</code>. Any other config that leaves it out still builds at 0, and an explicit <code>optimize:</code> always wins. Every release config in the projects we checked already declares <code>optimize:</code>, so their builds don&apos;t change.</p><h2><code>write</code> prints compiled procedures as <code>#&lt;procedure&gt;</code></h2><p>0.22.11 made <code>format</code> print a compiled procedure as <code>#&lt;procedure&gt;</code>; <code>write</code> and <code>display</code> still printed <code>#&lt;object 0x...&gt;</code>. All three agree now. To tell compiled code from bytecode, use the new <code>(procedure-native? p)</code>: <code>#t</code> for a procedure compiled by the native backend or a builtin written in C, <code>#f</code> for a bytecode closure or anything that isn&apos;t a procedure.</p><h2><code>sgx</code> appears on the first upgrade</h2><p>Upgrading from 0.22.10 or earlier no longer needs a second <code>sigil cli use</code> to create <code>sgx</code>: the first command the new version runs writes <code>$SIGIL_HOME/bin/sgx</code> (and <code>bin/sigil</code>, if that is missing) and says so once on stderr.</p><h2>Fixes</h2><ul><li>Importing a macro through <code>only</code>, <code>rename</code> or <code>prefix</code>, or through a renamed re-export,   left it unbound under its new name: after   <code>(import (rename (only (demo source) allowed) (allowed selected)))</code>, <code>(selected ...)</code>   failed with <code>unbound variable &apos;selected&apos;</code>. Imported macros now follow the same selectors as   values.</li></ul><h2>Known issues</h2><ul><li>A macro that defines a builtin&apos;s name inside a top-level <code>(let () ...)</code> and calls it still gets   the builtin, and the definition leaks into the module (0.22.11 too).</li><li>In a built app (<code>sigil build</code>, native or WebAssembly), a macro defined inside a procedure body   whose template names one of that body&apos;s own defines fails when it runs, whatever the name:   <code>macro template refers to local &apos;helper&apos;, which a compiled app can&apos;t capture yet (a macro defined   inside a body)</code>. 0.22.11 failed the same way, as <code>unbound variable &apos;str&apos;</code>. <code>sigil run</code>, <code>eval</code>   and the REPL call the local.</li><li>A macro defined inside a procedure body whose template names a <code>let</code>-bound variable or a   parameter with a builtin&apos;s name still gets the builtin: in   <code>(let ((car (lambda (x) &apos;mine))) (define-syntax m (syntax-rules () ((_ x) (car x)))) (m &apos;(1 2)))</code>,   <code>(m &apos;(1 2))</code> gives <code>1</code>.</li><li>A body holds at most 64 local names for the purpose of telling a local from a macro: the 65th and   later parameter or definition named like a macro is still expanded as the macro.</li><li><code>(do ((x 1) (i 0 (+ i 1))) ...)</code>, a <code>do</code> variable with no step, fails with   <code>syntax-rules: no matching pattern</code>.</li></ul>",
      "summary": "A correctness release. Sigil ignored a program's own binding of a builtin's name, such as a local not or an internal (define (car x) ...), on every backend, and several older bugs mixed up local names with macro keywords. Both are fixed. Release configs also compile optimized C by default now.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.11/",
      "url": "https://usesigil.org/releases/0.22.11/",
      "title": "Sigil 0.22.11",
      "date_published": "2026-09-29T00:00:00Z",
      "content_html": "<p>A release in two parts. <code>sigil task</code> gives a project and its dependencies one way to share scripts, such as a game engine&apos;s publish tools. And a long list of memory-safety and correctness fixes, found by reading and testing the runtime closely: several were silent wrong answers or memory corruption that had been there for a long time.</p><p><strong>Existing projects build without changes.</strong> Run <code>sigil deps install</code> once in each project after upgrading, so it writes the new dependency index.</p><h2><code>sigil task</code> runs a project&apos;s scripts and its dependencies&apos;</h2><p><code>sigil task &lt;name&gt; [args...]</code> runs one of the project&apos;s tasks: a step task (a <code>tasks:</code> build pipeline, which takes no arguments) or a script task, a Sigil shell script that receives every argument after its name unchanged. A package declares script tasks as quoted data under <code>provides:</code>:</p><pre>provides: (list\n  script-tasks: &apos;((publish-web file: &quot;tasks/publish-web.sgl&quot;\n                               description: &quot;Publish a checked site tree&quot;)))</pre><p>A dependency&apos;s script tasks run only with its package prefix, as in <code>sigil task substratic:publish-web</code>, and only for dependencies listed in <code>dependencies:</code> or <code>dev-dependencies:</code>. A bare name is always the project&apos;s own task, so a dependency can&apos;t stand in for one, and <code>sigil build</code>, <code>sigil test</code> and <code>sigil deps install</code> never run a script task. Before a script task runs, one line on stderr says where it comes from, including the locked commit. <code>sigil task --list</code> lists every task.</p><p><code>sgx</code> is <code>sigil task</code> under a short name: <code>sigil cli install</code>, <code>use</code> and <code>upgrade</code> write <code>$SIGIL_HOME/bin/sgx</code>, and <code>sigil completions bash|zsh</code> prints a completion script for both. <code>sigil run -t &lt;task&gt;</code> still works, prints <code>Deprecated: use &apos;sigil task&apos; instead</code>, and goes away before 1.0.</p><p>Upgrading from 0.22.10 or earlier, the older CLI does the switch and doesn&apos;t know about <code>sgx</code>, so run <code>sigil cli use 0.22.11</code> once more after upgrading to create it.</p><h2><code>sigil run</code> passes your arguments through</h2><p>Everything after <code>sigil run</code>&apos;s own options now goes to the program unchanged: <code>sigil run app --dry-run -t x</code> passes <code>app --dry-run -t x</code> instead of running task <code>x</code>. The program reads them with <code>(command-line)</code>, as a built binary does.</p><h2>Scripts know their project and can keep secrets from child processes</h2><p>The shell prelude gains <code>(project-root)</code>, the root of the project the script was started in. <code>$</code>, <code>$?</code>, the captures, <code>spawn</code> and <code>pipe</code> stages accept <code>env-unset: (&quot;NAME&quot; ...)</code> to keep a variable such as a token out of a child&apos;s environment.</p><h2>Commands start faster, and installed dependencies can be checked</h2><p><code>sigil deps install</code> writes <code>.sigil/deps.index</code>, so commands no longer read every dependency&apos;s <code>package.sgl</code> each time they start: an empty script starts about 100 ms faster in a project with many dependencies. <code>sigil deps verify</code> re-hashes each installed dependency and reports anything changed since it was installed. Native builds no longer write into a dependency&apos;s shared copy; the generated <code>version_info.c</code> goes to the build&apos;s own output. A <code>package.sgl</code> whose <code>sigil:</code> floor is newer than the running Sigil is refused before anything in it runs: <code>this package needs sigil ≥ X; this is Y</code>.</p><h2>Large numbers, web builds and macros no longer corrupt memory</h2><ul><li><code>number-&gt;string</code> on a large integer in a radix below 10 wrote past the end of its buffer,   and <code>string-&gt;number</code> with a radix above 16 skipped the digits g to z and could abort.</li><li>On the web, <code>bytevector-copy!</code> and other range checks could wrap around and write out of   bounds, and <code>make-vector</code> or <code>make-bytevector</code> with a huge length quietly made a tiny one.   They raise an error now.</li><li><code>syntax-&gt;datum</code> could lose objects to the garbage collector under memory pressure, so   macro-heavy code could read freed memory.</li><li>The garbage collector didn&apos;t trace the recent-calls ring behind stack traces, so capturing a   stack trace after a collection could copy freed memory into the trace and corrupt the heap   later, typically as a crash at exit. The profiler&apos;s tables had the same gap under   <code>--profile</code>.</li><li>A path containing a NUL character was cut at the NUL, so the file checked and the file   opened could differ. It raises an error now.</li></ul><h2><code>let</code> no longer mistakes its body for a named <code>let</code></h2><p>A <code>let</code> whose body starts with a list of lists, such as <code>(let ((k 5)) ((f k)) 0)</code>, was read as a named <code>let</code> and referred to a global <code>k</code> instead of the binding, failing with <code>unbound variable &apos;k&apos;</code>. It affected every backend.</p><h2>Errors read the same on every backend</h2><p>Generic runtime errors now have the kind <code>runtime-error</code> everywhere (the bytecode VM used <code>vm-error</code>), and index errors say &quot;index out of range&quot;. Code that catches <code>vm-error</code> by name should catch <code>runtime-error</code> instead.</p><h2>Fixes</h2><ul><li><code>length</code> of an improper list returned -1 in compiled code, and <code>length</code> or <code>apply</code> on a   circular list aborted or hung. Both raise a catchable error now, and <code>list?</code> of a circular   list is <code>#f</code>.</li><li><code>apply</code> accepts continuations and parameter objects, as a direct call does.</li><li>Re-entering a multi-shot <code>call/cc</code> continuation could stop the bytecode VM with a GET_CELL   error, and <code>compile-file</code> on a file with several libraries folded them into the first.</li><li><code>(format &quot;~a&quot; proc)</code> printed compiled procedures as <code>#&lt;object&gt;</code>; it prints <code>#&lt;procedure&gt;</code>.</li><li><code>format</code>, <code>println</code> and <code>str</code> printed integers of 2^31 and above wrong on the web   (4294967297 came out as 1, 4294967295 as -1), and wrote a non-ASCII character as one broken   byte on every platform: <code>(format &quot;~a&quot; (integer-&gt;char 955))</code> now prints <code>λ</code>.</li><li>Web apps print non-ASCII text correctly (it came out as replacement characters), keep   stdout and stderr in order, and no longer slow down on long output lines.</li><li><code>sigil_make_bytevector</code> now returns zeroed memory, so C code that builds a bytevector,   including an embedder&apos;s natives, can&apos;t expose old heap contents.</li><li><code>string-repeat</code> could crash in desktop builds, and a corrupt <code>.sgb</code> file could corrupt memory   as it loaded. <code>make-srcloc</code> and <code>make-generated-srcloc</code> raise for positions past 2^31-1.</li><li>Compiling a library in a process that already had a library of the same name loaded bound   its exports in a different order, so the same source could compile to different bytecode.   Compiled output no longer depends on what else is loaded.</li></ul><h2>Desktop apps can compile their dependencies natively (opt-in)</h2><p>Set <code>SIGIL_NATIVE_DEPS=all</code> when you build, and a desktop app&apos;s release build compiles the dependency modules it imports to native code, not just its own modules; the rest stays bytecode. Calls into libraries such as sigil-geometry run 1.5 to 2 times faster and stop allocating per argument. It is off by default in this release while games try it. Binaries grow with the code they carry: 11% for Crash The Stack, 31% for Phantom Burn and 36% for Harkfell.</p><p>To try it, raise <code>sigil:</code> to 0.22.11, run <code>sigil deps update --upgrade-sigil</code>, and build with <code>SIGIL_NATIVE_DEPS=all</code>. A native app links the runtime its lock names, so the lock must carry 0.22.11&apos;s: it fixes a garbage-collector bug that native dependency code exposed.</p><h2>Known issues</h2><ul><li>Quoted list constants in compiled code are rebuilt each time they&apos;re evaluated, so two   evaluations of one quoted list aren&apos;t <code>eq?</code> as they are on the bytecode VM.</li><li>Dependency copies are not yet read-only: older Sigil versions still write into them, so   locking them down would break builds with an older CLI.</li><li><code>(apply apply k args)</code> with a bytecode continuation <code>k</code> doesn&apos;t work; <code>(apply k args)</code> does.</li><li><code>write</code> still prints a compiled procedure as <code>#&lt;object …&gt;</code> while <code>format &quot;~a&quot;</code> now prints   <code>#&lt;procedure&gt;</code>.</li></ul>",
      "summary": "A release in two parts. sigil task gives a project and its dependencies one way to share scripts, such as a game engine's publish tools. And a long list of memory-safety and correctness fixes, found by reading and testing the runtime closely: several were silent wrong answers or memory corruption that had been there for a long time.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.10/",
      "url": "https://usesigil.org/releases/0.22.10/",
      "title": "Sigil 0.22.10",
      "date_published": "2026-09-28T00:00:00Z",
      "content_html": "<p>A correctness and build-cache release. Optimized builds could compute wrong answers in two patterns, compiled code skipped argument-count checks, and Sigil&apos;s caches could run the wrong compiled code, ship stale native code, or throw away work they had already done. All of that is fixed, and an unchanged rebuild of a program now recompiles almost nothing.</p><p><strong>Relock projects that make native or web builds.</strong> Code compiled by 0.22.10 needs the 0.22.10 runtime, and linking it against an older one fails with <code>undefined reference to &apos;sigil_native_requires_sigil_lib_0_22_10_checked_calls&apos;</code>. Run <code>sigil deps update --upgrade-sigil</code> and rebuild. It moves the project&apos;s Sigil packages to the newest version its <code>sigil:</code> field allows, and it also re-resolves every other dependency, so expect those to move too. If <code>sigil:</code> pins a version below 0.22.10, raise it first.</p><h2>Scripts, tests and the REPL could run the wrong compiled library</h2><p><code>sigil &lt;file&gt;</code>, <code>sigil eval</code>, <code>sigil test</code> and the REPL cache compiled libraries. That cache found a library by its name and trusted its timestamp, so it could hand back the wrong compiled copy: two projects that lock different versions of one library ran each other&apos;s code, and so did a scratch copy with a newer timestamp, an in-place edit that kept the timestamp, or a change to <code>SIGIL_FEATURES</code>. Every compiled module is now stored under what actually shaped it: its source, the compiler, the settings that change output, its dependencies, its path and the host. The new cache lives in <code>$XDG_CACHE_HOME/sigil/compiled/</code>; the old <code>$XDG_CACHE_HOME/sigil/lib/&lt;commit&gt;+sgbN</code> directories are no longer used and can be deleted.</p><p>Scripts and tests also follow the project&apos;s redirects now: <code>sigil &lt;file&gt;</code>, <code>eval</code> and <code>test</code> take a redirected dependency&apos;s sources from the redirect target, and <code>sigil --redirects FILE ...</code> sets redirects for one command.</p><h2>Optimized builds computed wrong answers in two patterns</h2><p>Release and web builds use the optimizer (O1 and O2), and so does bytecode compiled at those levels. Two of its passes could treat a value that came from a conditional as if it always came from one branch:</p><ul><li>Calling a procedure chosen by a conditional could run the wrong one:</li></ul><pre lang=\"scheme\" class=\"highlighted\" tabindex=\"0\"><code>  <span class=\"tok-paren\">(</span><span class=\"tok-special\">define</span> <span class=\"tok-paren\">(</span>caller choose replacement<span class=\"tok-paren\">)</span>\n    <span class=\"tok-paren\">(</span><span class=\"tok-special\">let</span> <span class=\"tok-paren\">(</span><span class=\"tok-paren\">(</span>target <span class=\"tok-paren\">(</span><span class=\"tok-special\">if</span> choose <span class=\"tok-paren\">(</span><span class=\"tok-special\">lambda</span> <span class=\"tok-paren\">(</span>n<span class=\"tok-paren\">)</span> <span class=\"tok-paren\">(</span>+ n <span class=\"tok-number\">1</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span> replacement<span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span>\n      <span class=\"tok-paren\">(</span>+ <span class=\"tok-paren\">(</span>target <span class=\"tok-number\">7</span><span class=\"tok-paren\">)</span> <span class=\"tok-number\">1</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span></code></pre><p>  With <code>choose</code> false and a <code>replacement</code> that returns 41, this returned 9 instead of 42.</p><ul><li>A value chosen by a conditional could be mistaken for one of the branches. With <code>selected</code> set   to either an empty list or <code>(cons 1 &apos;())</code>, <code>(null? selected)</code> answered <code>#t</code> for the one-element   list.</li></ul><p>Both returned the wrong value silently, with no error. The optimizer now checks that a value has a single source before it relies on one. The fixes change no other generated code: the standard library and the packages compile to identical C, and so do both Flux Harmonic games for the second fix.</p><h2>Builds reuse more and never ship stale code</h2><ul><li><strong>An unchanged rebuild recompiles almost nothing.</strong> A package with a custom build step (C   sources, an archive, generated version info), or with a source the build can&apos;t model (an   <code>include</code>, a library condition in <code>cond-expand</code>), used to switch caching off for every module in   the package. Now only the files that need it compile fresh. Crash The Stack&apos;s unchanged rebuild   went from 170 recompiled modules to 3 in its dev configuration, and from 167 to 19 for the web.</li><li><strong>A build could ship old native code.</strong> After an edit that kept the file&apos;s size and timestamp,   <code>sigil build --no-cache</code>, and a build whose cache failed to start, reused the old compiled object   and printed the old behavior. Native objects are now reused only when their inputs match by   content.</li><li><strong>Macros that read files are handled safely.</strong> A module whose macros read a file, the   environment, the command line or a process while expanding is no longer cached at all: it   compiles fresh every build, so a data-file change always reaches the output. <code>--verbose</code> prints   an <code>UNCACHED</code> line for it.</li><li><strong>The web build&apos;s link, asyncify and wasm-opt steps are cached across builds,</strong> reused when the   tool, its arguments, its environment and its inputs all match by content.</li><li><strong>Package manifests are always evaluated in the top-level environment.</strong> On a release sigil, a   manifest loaded by a build step was evaluated inside the compiler&apos;s own module, which is why   release builds of apps that build sigil-lib stopped caching after sigil-lib.</li><li>A build step that defines or changes a library module in the build process (for example with   <code>eval-string</code>) turns caching off for the rest of that build, and <code>--verbose</code> says so:   <code>NO-CACHE a task step registered or redefined &lt;module&gt;</code>.</li><li>A REPL or <code>sigil build --watch</code> session that outlives a rebuild of Sigil itself stops caching,   instead of filing new entries under the new compiler.</li></ul><h2>Fixes</h2><ul><li>Compiled code now checks argument counts. Calling a procedure with too few arguments read   garbage in native code and an undefined or stale value on the web, and extra arguments were   silently dropped. Both now raise <code>arity-error</code> with the bytecode VM&apos;s message, for example   &quot;&lt;lambda&gt;: expected 2 argument(s), got 1&quot;. Code that caught the old <code>runtime-error</code> from the   native bridge should catch <code>arity-error</code> instead. Calling a delimited continuation with the   wrong number of values from compiled code now raises too, as on the bytecode VM.</li><li><code>apply</code> in tail position is now a real tail call in native builds. A loop that recursed through   <code>apply</code> a million times crashed with a segmentation fault; it now runs in constant stack, as on   the bytecode VM.</li></ul><h2>Known issues</h2><ul><li>A build step that re-<code>define</code>s or <code>set!</code>s an existing binding in a library module isn&apos;t detected   by the new step check.</li><li>A module that imports <code>(sigil user)</code>, or whose macros look up modules outside their own imports,   is never cached.</li><li>Web builds of Crash The Stack still recompile <code>case.sgl</code>, <code>regex.sgl</code> and <code>sigil/audio/sink.sgl</code>   on every build (as on 0.22.9).</li><li><code>(list? x)</code> on a circular list never returns.</li><li>The bytecode VM can stop with &quot;GET_CELL: expected upvalue cell&quot; when a multi-shot <code>call/cc</code>   continuation is re-entered, and with &quot;SET_CELL: expected upvalue cell&quot; on some module   bindings.</li><li>Release builds of desktop apps still run the standard library and dependency packages as   bytecode; only the app&apos;s own modules are native.</li></ul>",
      "summary": "A correctness and build-cache release. Optimized builds could compute wrong answers in two patterns, compiled code skipped argument-count checks, and Sigil's caches could run the wrong compiled code, ship stale native code, or throw away work they had already done. All of that is fixed, and an unchanged rebuild of a program now recompiles almost nothing.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.9/",
      "url": "https://usesigil.org/releases/0.22.9/",
      "title": "Sigil 0.22.9",
      "date_published": "2026-09-26T00:00:00Z",
      "content_html": "<p>A size release. Web builds of native-compiled apps are about 70% smaller, and call-heavy native code runs faster, because the compiler no longer spends most of each function&apos;s code on bookkeeping. Measured on Crash The Stack, Harkfell and Phantom Burn at the same commits.</p><p><strong>Existing projects build without changes.</strong> Rebuild to pick up the smaller output.</p><h2>Web builds are about 70% smaller</h2><p>A game&apos;s wasm is almost all code the compiler emits for its Sigil modules, and two thirds of that was per-function boilerplate: every local variable registered with the garbage collector by its own inlined call, and every call followed by its own copies of the error and exception exits. Locals are now kept in one array that is registered once, and a function&apos;s exits share one label.</p><table><thead><tr><th>Game</th><th>0.22.8</th><th>0.22.9</th><th>Change</th></tr></thead><tbody><tr><td>Crash The Stack</td><td>44.8 MB</td><td>12.0 MB</td><td>-73%</td></tr><tr><td>Harkfell</td><td>31.7 MB</td><td>9.0 MB</td><td>-72%</td></tr><tr><td>Phantom Burn</td><td>11.4 MB</td><td>3.7 MB</td><td>-68%</td></tr></tbody></table><p>Gzipped downloads shrink by 35 to 42%. Crash The Stack and Harkfell were over Cloudflare Pages&apos; 25 MiB file limit and are now well under it.</p><h2>Native code calls faster</h2><p>Native desktop builds use the same compiler, so a function now makes room for its garbage collector roots once instead of once per local. A call-heavy benchmark (<code>fib</code>, <code>tak</code>) runs in about half the time. In the browser, Crash The Stack&apos;s game tick went from 11.7 ms to 9.2 ms a frame, and its whole frame callback from 14.4 ms to 10.6 ms.</p><h2>Fixes</h2><ul><li><code>(remainder 7.0 2)</code> returned an undefined value in release and web builds, for every inexact argument. It now returns <code>1.0</code>, and a zero divisor raises an error, as on the bytecode VM.</li><li><code>apply</code> with an improper list as its last argument silently dropped the tail in native code: <code>(apply f 1 &apos;(2 . 3))</code> called <code>f</code> with <code>(1 2)</code>. It now raises the same error as the bytecode VM.</li><li><code>integer?</code> was wrong on both backends: <code>(integer? 1.0)</code> was <code>#f</code> on the bytecode VM, and <code>(integer? 3.5)</code> and <code>(integer? +inf.0)</code> were <code>#t</code> in native code. It now follows R7RS everywhere: exact integers and finite integral floats.</li><li>A <code>-0.0</code> literal lost its sign in release and web builds, and prints as <code>-0</code> on every backend now. <code>+inf.0</code>, <code>-inf.0</code> and NaN literals are also emitted exactly.</li><li>Forcing a promise whose body yielded inside <code>with-async</code> returned an undefined value on the bytecode VM and cached it, so every later <code>force</code> was wrong too. It now returns the body&apos;s value, as native code already did.</li><li>Release and web builds and the bytecode VM now agree on all of the above, checked by a new test that runs the same cases on both backends.</li></ul><h2>Known issues</h2><ul><li>Release builds of desktop apps still run the standard library and dependency   packages as bytecode; only the app&apos;s own modules are native. A proper fix is   coming.</li><li>In web builds, forcing a promise whose body yields inside <code>with-async</code> stops   the program, as it did in 0.22.8. The bytecode VM fix above doesn&apos;t cover the   web path yet.</li></ul>",
      "summary": "A size release. Web builds of native-compiled apps are about 70% smaller, and call-heavy native code runs faster, because the compiler no longer spends most of each function's code on bookkeeping. Measured on Crash The Stack, Harkfell and Phantom Burn at the same commits.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.8/",
      "url": "https://usesigil.org/releases/0.22.8/",
      "title": "Sigil 0.22.8",
      "date_published": "2026-09-25T00:00:00Z",
      "content_html": "<p>A correctness release. Release and web builds could drop the guard of a small helper procedure, comparisons with NaN answered true, and <code>sigil mcp serve</code> showed no tools in new Claude Code sessions. It also fixes a web app that doesn&apos;t import <code>(sigil core)</code>, a <code>from-path</code> dependency that was silently left out, macros that allocated on every use in native code, and modules reused after <code>SIGIL_FEATURES</code> changed. More of R7RS now works without an import.</p><p><strong>Existing projects build without changes.</strong> Rebuild your release and web builds to pick up the first fix.</p><h2>Release and web builds keep a helper&apos;s guard</h2><p>Since 0.16.1, release and web builds replaced a small helper with a fast path when its body <em>could</em> end in a struct field read, an index calculation <code>(+ x (* y (cols g)))</code> or a vector lookup <code>(vector-ref (items s) i)</code>, even when the helper did something else first. <code>(if (&lt; i 0) &apos;none (vector-ref (items s) i))</code> crashed on the guarded branch, a guarded index helper returned the unguarded sum, and a counter bumped before the lookup never moved. Dev builds were right, so tests passed. The fast path now applies only when the whole helper is that one expression.</p><h2>Comparisons with NaN are false</h2><p><code>(&lt;= nan 1.0)</code>, <code>(&gt;= nan 1.0)</code> and <code>(= nan nan)</code> answered <code>#t</code>, so a tolerance check like <code>(&lt;= (abs (- a b)) eps)</code> let a NaN through, and <code>zero?</code> said <code>#t</code> for NaN. Every comparison with NaN is now false, on every backend, and <code>min</code> and <code>max</code> return NaN when an argument is NaN.</p><h2>Sigil&apos;s MCP tools appear again in new Claude Code sessions</h2><p>Since 2026-09-25, Claude Code 2.1.282 negotiates MCP protocol revision 2026-07-28 with any server that offers it, and <code>sigil mcp serve</code> offered it without answering in that revision&apos;s shape. Claude Code checks that a tool listing says how long it may be cached (<code>ttlMs</code> and <code>cacheScope</code>). The listing from <code>sigil mcp serve</code> had neither, so the client retried a few times and then registered no tools at all, with no error shown. This release takes sigil-mcp 0.18.1, which adds both fields whenever the client speaks 2026-07-28, and older clients get exactly the answers they got before. Sessions that are already running keep the server they started with, so restart them after upgrading.</p><h2>A web app that doesn&apos;t import <code>(sigil core)</code> starts</h2><p>An app whose modules never import <code>(sigil core)</code> died in the browser before running anything, with <code>unbound variable &apos;%set-spec!&apos;</code>. The core library is now always set up first.</p><h2>A <code>from-path</code> dependency is installed and built</h2><p>A project&apos;s <code>(from-path dir: &quot;../lib&quot;)</code> without <code>package:</code> was left out: <code>sigil deps install</code> didn&apos;t link it and <code>sigil build</code> finished without it, with only a &quot;failed to load import&quot; warning. It&apos;s now linked and built like any other dependency, and one that doesn&apos;t resolve stops both commands with its directory named. When <code>--redirects</code> points a dependency elsewhere than its <code>.sigil/deps</code> link, the build says which tree it used.</p><h2>Macros cost nothing at run time in native code</h2><p>A <code>syntax-rules</code> macro whose expansion calls a procedure or reads a variable of the macro&apos;s own library looked the name up, and allocated, on every use in release and web builds: 27 allocations for one vector operation in sigil-geometry. Each use now finds the binding once, and still sees it change after a <code>set!</code>.</p><h2>Modules are recompiled when <code>SIGIL_FEATURES</code> changes</h2><p><code>sigil eval</code>, <code>sigil run</code> and <code>sigil test</code> keep compiled copies of the modules they build from source, and reused a copy compiled without a feature when that feature was set, so a <code>cond-expand</code> kept its old branch. Each feature set now has its own copies.</p><h2>More of R7RS works without an import</h2><p>A library that imports only <code>(sigil core)</code>, and every test file, now has these <code>(scheme base)</code> procedures:</p><ul><li>the math procedures <code>floor</code>, <code>ceiling</code>, <code>round</code>, <code>truncate</code>, <code>max</code>, <code>min</code>, <code>quotient</code>,   <code>gcd</code>, <code>lcm</code>, <code>expt</code>, <code>square</code>, <code>exact</code>, <code>inexact</code>, <code>exact?</code>, <code>inexact?</code>,   <code>exact-integer?</code> and the <code>floor/</code> and <code>truncate/</code> families, which needed   <code>(sigil math)</code>;</li><li><code>string&lt;?</code>, <code>string&gt;?</code>, <code>string&lt;=?</code> and <code>string&gt;=?</code>, which needed <code>(sigil string)</code> and   now take more than two strings;</li><li><code>list-tail</code>, <code>list-set!</code>, <code>list-copy</code>, <code>vector-append</code>, <code>boolean=?</code>, <code>symbol=?</code>,   <code>exact-integer-sqrt</code>, <code>input-port-open?</code>, <code>output-port-open?</code>, <code>rational?</code>, <code>real?</code>,   <code>complex?</code>, <code>numerator</code>, <code>denominator</code>, <code>rationalize</code>, <code>exact-&gt;inexact</code> and   <code>inexact-&gt;exact</code>, which didn&apos;t exist. Sigil has no exact fractions, so   <code>numerator</code>, <code>denominator</code> and <code>rationalize</code> return floats for a fractional   argument, and <code>rationalize</code> computes in floating point.</li></ul><p><code>(sigil math)</code> and <code>(sigil string)</code> still export the names they had. A library or program that defines one of these names itself keeps its own definition. <code>abs</code>, <code>modulo</code>, <code>remainder</code> and <code>call/cc</code> can now be passed as values: <code>(map abs xs)</code> raised &quot;unbound variable&quot;. Importing <code>(sigil string)</code> no longer swaps <code>string-upcase</code> and <code>string-downcase</code> for its own ASCII-only versions; they are core&apos;s.</p><p>The <a href=\"https://usesigil.org/docs/reference/r7rs-compatibility/\">R7RS compatibility</a> page now says which module each name is in and lists the gaps that remain.</p><h2>Smaller fixes</h2><ul><li><code>sigil cli use v0.22.7</code> works; the leading <code>v</code> of a tag name is accepted.</li><li><code>sigil deps install --help</code> lists the options, including <code>--redirects</code> and   <code>--upgrade-sigil</code>.</li><li><code>string-index</code> and <code>string-index-right</code> take a character as well as a predicate:   <code>(string-index &quot;a,b&quot; #\\,)</code> is 1.</li><li><code>sigil test</code> shows each file&apos;s time in its per-file line, and <code>--report</code> lists every   file with its outcome, counts and time in milliseconds.</li><li>A module can export 1024 names, up from 512. The web bridge packages (browser,   audio, WebGL, DOM, canvas, network) add their natives to <code>(sigil core)</code>, and a web   app that linked several of them could run core out of room. A module past the limit   now reports each name it can&apos;t export instead of dropping it silently.</li></ul><h2>Known issues</h2><p>Release builds of desktop apps run the standard library and dependency packages as bytecode; only the app&apos;s own modules are native. Web builds run everything native. <code>SIGIL_NATIVE_DEPS=1</code> makes a release build compile the standard library natively, at the cost of a much larger binary, since every standard library module is included. A proper fix is coming.</p><p><code>string-upcase</code> and <code>string-downcase</code> convert non-ASCII letters with the C library&apos;s case tables, so their results for non-ASCII text can differ between systems: the Linux release build turns <code>&quot;λ&quot;</code> into <code>&quot;Λ&quot;</code>, and a build against glibc may leave it unchanged. Before 0.22.8 a library that imported <code>(sigil string)</code> got ASCII-only versions whenever it passed these procedures as values.</p>",
      "summary": "A correctness release. Release and web builds could drop the guard of a small helper procedure, comparisons with NaN answered true, and sigil mcp serve showed no tools in new Claude Code sessions. It also fixes a web app that doesn't import (sigil core), a from-path dependency that was silently left out, macros that allocated on every use in native code, and modules reused after SIGIL_FEATURES changed. More of R7RS now works without an import.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.7/",
      "url": "https://usesigil.org/releases/0.22.7/",
      "title": "Sigil 0.22.7",
      "date_published": "2026-09-24T00:00:00Z",
      "content_html": "<p>A game audio and correctness release. Sound effects in the browser now play the moment they&apos;re triggered, a game&apos;s synth can drop to a lower rate on a weak phone, and <code>sigil</code> is ready to take upgrades from the signed registry. Three defects are fixed along the way: an error lost between nested <code>guard</code>s, a <code>-L</code> path that stopped <code>sigil</code> starting, and tests run against dependencies the lock doesn&apos;t name.</p><p><strong>Existing projects build without changes.</strong> The audio calls come with sigil-audio 0.13.0, released alongside.</p><h2>Sound effects play at once in the browser</h2><p>A sound effect pushed into the music stream was heard one buffer later, up to a quarter second on a phone, and the delay came and went. Load a sound once with <code>load-audio-cue</code> and play it with <code>play-audio-cue</code>: it&apos;s mixed into the very next audio block, about 3 ms, however much music is buffered. <code>audio-cue-lag</code> reports the wait for an on-device readout.</p><h2>A slow synth can render at a lower rate</h2><p><code>open-audio-stream</code> takes <code>rate:</code>, and <code>set-audio-stream-rate!</code> changes it mid-stream. The sink upsamples on the audio thread, so a game can render its music at 24000 on a 48000 device without a resampler of its own.</p><p><code>(sigil audio ladder)</code> decides when to step down. It&apos;s the policy Crash The Stack tuned on a phone: it watches the steady cost per frame, ignores the moments next to a sound effect or a tune starting, and never steps back up on its own. <code>rate-ladder-report</code> gives a readout line such as <code>48000/24000 BY LADDER@20.2S</code>.</p><p><code>audio-stream-underruns-sounding</code> counts only the dropouts a player could hear, apart from a stream with nothing to play running dry.</p><h2>Upgrades verified end to end</h2><p><code>sigil</code> can now take upgrades from the registry&apos;s signed release list, checking every step itself with no <code>minisign</code> needed: the key list against the root key built into <code>sigil</code>, the release list against a key from that list, and the binary against the hash, size and file name the list gives for your platform. An older list than one <code>sigil</code> has already seen is refused, so a stale copy can&apos;t roll you back.</p><p>The registry&apos;s signed list goes live with its first signing. Until then, and whenever the registry can&apos;t be reached, <code>sigil</code> says <code>Registry unavailable, using Codeberg.</code> and upgrades from Codeberg as before. If the registry answers but what it serves fails a check, <code>sigil</code> installs nothing and says why. <code>SIGIL_REGISTRY</code> points it at a mirror.</p><h2>An error passed on by an inner <code>guard</code> reaches the outer one</h2><p>0.22.6&apos;s known issue is fixed: when one <code>guard</code> sat in tail position of another&apos;s body, an error the inner one passed on skipped the outer handler. The workarounds are no longer needed.</p><p>One cost: a loop that calls itself from inside a <code>guard</code> body now keeps each iteration&apos;s handler until it returns, about 150 bytes each. For a long-running loop, put the recursive call after the <code>guard</code>.</p><h2><code>-L</code> can no longer stop <code>sigil</code> starting</h2><p>Pointing <code>-L</code> at another project&apos;s build output could replace the modules <code>sigil</code> itself is built from, and it failed before running anything. Those modules now always come from <code>sigil</code>&apos;s own copy. Separately released packages such as <code>(sigil json)</code> can still be replaced with <code>-L</code>.</p><h2>Build, test and run stop when dependencies don&apos;t match the lock</h2><p>After switching branches, <code>.sigil/deps</code> can still point at the old branch&apos;s dependencies, and tests ran against code the lock doesn&apos;t name. <code>sigil build</code>, <code>sigil test</code> and <code>sigil run</code> now stop with exit code 78 and name the package and both commits. Run <code>sigil deps install</code>, or set <code>SIGIL_ALLOW_STALE_DEPS=1</code> to go ahead anyway.</p><h2>A clean build from source</h2><p><code>make</code> no longer prints <code>Error in macro &apos;let&apos;</code> while bootstrapping, and the core library it builds is now checked to match a normal build.</p>",
      "summary": "A game audio and correctness release. Sound effects in the browser now play the moment they're triggered, a game's synth can drop to a lower rate on a weak phone, and sigil is ready to take upgrades from the signed registry. Three defects are fixed along the way: an error lost between nested guards, a -L path that stopped sigil starting, and tests run against dependencies the lock doesn't name.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.6/",
      "url": "https://usesigil.org/releases/0.22.6/",
      "title": "Sigil 0.22.6",
      "date_published": "2026-09-24T00:00:00Z",
      "content_html": "<p>A reliability release for scripts, plus the groundwork for <code>sigil env</code>. Two defects let a program lose data or an error and still exit 0, and the command line swallowed <code>-L</code> arguments meant for other programs. All three are fixed, and several errors that named nothing now say what went wrong.</p><p><strong>Existing projects build without changes.</strong> One behaviour change to know about: a <code>guard</code> that used to return <code>#f</code> for an error none of its clauses matched now passes the error on, so a script that seemed to succeed may now report it.</p><h2>Strings with a NUL byte are written in full</h2><p><code>write-file-string</code>, the shell&apos;s <code>write-text</code> and <code>display</code> stopped at the first NUL byte and reported success. A script saving a downloaded archive could end up with a 24-byte file and exit 0. Every byte of a string is now written, to files, pipes and ports alike.</p><h2><code>guard</code> passes on an error that no clause matches</h2><p>A <code>guard</code> with two or more clauses returned <code>#f</code> when none of them matched, so the error vanished and the script exited 0. It now propagates, as R7RS specifies.</p><p><strong>Known issue:</strong> when the inner guard sits in tail position of an outer guard&apos;s body, the error it passes on skips the outer handler and reaches the top level. Until it&apos;s fixed, call the inner code in non-tail position, for example <code>(let ((r (thunk))) r)</code>, or give the inner guard an <code>else</code> clause.</p><h2>Arguments after <code>--</code> reach your program</h2><p><code>sigil</code> treated <code>-L</code> as its own load-path flag wherever it appeared, even after <code>--</code>, so <code>sigil env shell -- tmux -L work</code> started tmux on the wrong server. Everything after <code>--</code> now passes through untouched.</p><h2>Errors that name what went wrong</h2><p>Struct, record and multimethod errors printed placeholders like <code>~a: unknown field ~a</code>; they now read like <code>point: unknown field z</code>. And when a module on a <code>-L</code> path stops <code>sigil</code> itself from starting, the error names the module, the file and the <code>-L</code> directory, and says to remove the <code>-L</code> or rebuild what it holds.</p><h2><code>sigil env</code>: one-time setup and a read-only store</h2><p><code>sigil env setup</code> offers to create the <code>/sigil</code> link that store programs need outside <code>sigil env shell</code>. It always asks first and refuses to run as root. The environment store now lives in <code>~/.local/share/sigil/store</code>, apart from the build cache, and its items are read-only: <code>sigil env store rm</code> removes them and <code>sigil env verify --full</code> checks every file.</p><p>A lock with nothing to build now takes about 2 seconds instead of 12, and shows its progress as it goes. The first public channel follows once its signing is done.</p>",
      "summary": "A reliability release for scripts, plus the groundwork for sigil env. Two defects let a program lose data or an error and still exit 0, and the command line swallowed -L arguments meant for other programs. All three are fixed, and several errors that named nothing now say what went wrong.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.5/",
      "url": "https://usesigil.org/releases/0.22.5/",
      "title": "Sigil 0.22.5",
      "date_published": "2026-09-23T00:00:00Z",
      "content_html": "<p>A regular expressions and web reliability release, plus Windows icons. Sigil now has regular expressions in the standard library. Web builds could also fail in ways nothing on the code path explained: a type error from nowhere, or text that parsed fine natively coming back as garbage. Crash The Stack hit both, including a menu that froze on phones.</p><p><strong>Existing projects build without changes.</strong> A native or wasm app picks up the fixes by rebuilding with this toolchain. <code>(sigil regex)</code> and <code>(sigil unicode case)</code> are new, and a Windows build config gains two optional keys.</p><h2>Regular expressions: <code>(sigil regex)</code></h2><p>Sigil now has regular expressions in the standard library, using the same syntax as RE2 and Go. <code>regex-match?</code>, <code>regex-search</code>, <code>regex-replace-all</code>, <code>regex-split</code> and the rest take either a compiled pattern or a plain string, so a one-off check is a single call.</p><p>Matching always runs in linear time, so no pattern can hang your program, even one you didn&apos;t write. The trade-off is that a pattern can&apos;t refer back to its own groups (backreferences like <code>\\1</code>) or use lookaround, and such patterns are rejected with an error pointing at the position. Replacements can still use captured groups: <code>$1</code> and <code>${name}</code> work in <code>regex-replace</code> and <code>regex-replace-all</code>.</p><p>The module is plain Sigil, so it behaves the same on bytecode, native and WebAssembly. Shell scripts get it without an import:</p><pre lang=\"scheme\" class=\"highlighted\" tabindex=\"0\"><code><span class=\"tok-paren\">(</span><span class=\"tok-special\">unless</span> <span class=\"tok-paren\">(</span>regex-match? <span class=\"tok-string\">&quot;^v[0-9]+\\\\.[0-9]+\\\\.[0-9]+$&quot;</span> tag<span class=\"tok-paren\">)</span>\n  <span class=\"tok-paren\">(</span>error <span class=\"tok-string\">&quot;not a release tag&quot;</span> tag<span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span></code></pre><h2>Tail calls keep their arguments alive</h2><p>A tail call into a built-in procedure could lose its arguments to the garbage collector while the built-in was still using them. Reading from a fresh string port was the usual victim: a large parse failed partway through with a type error or a reader error on text that was fine.</p><p>Only web builds showed it, and only when a collection happened to land mid-call, so the same code could work in one build and fail in the next. Rebuilding with 0.22.5 fixes it.</p><h2>Deep recursion no longer loses values</h2><p>Recursion more than 1024 native frames deep could leave the innermost frames unprotected from the garbage collector. Values still in use could be freed, and the next operation to touch them failed with an unrelated type error. The limit is gone: the frame stack now grows as needed.</p><h2>Catching memory errors in web builds</h2><p><code>SIGIL_GC_POISON_FREE=1</code> fills freed memory with a marker pattern, so a use-after-free fails right away instead of at some random later point. The GC&apos;s other environment variables, such as <code>SIGIL_GC_STRESS</code>, now reach a wasm module when its host supplies them.</p><h2>Case mapping that matches on every platform</h2><p><code>char-upcase</code> and <code>char-downcase</code> depend on the C library the runtime was linked against, so <code>é</code> could uppercase in one build and not in another. The new <code>(sigil unicode case)</code> carries the Unicode 15 case tables itself and gives the same answer everywhere. Case-insensitive regex matching uses it.</p><h2>Windows executables get an icon and version details</h2><p>A Windows <code>config</code> in your project&apos;s package file can now set the exe icon for <code>sigil build</code> with <code>icon:</code>, pointing at a <code>.ico</code> file in your project. The exe also gets its Properties → Details tab filled in from your package&apos;s <code>version:</code> and author, and the new optional <code>product-name:</code>.</p><pre lang=\"scheme\" class=\"highlighted\" tabindex=\"0\"><code><span class=\"tok-paren\">(</span>config\n  <span class=\"tok-keyword\">name:</span> <span class=\"tok-quote\">&apos;</span>windows-amd64\n  <span class=\"tok-keyword\">toolchain:</span> <span class=\"tok-quote\">&apos;</span>zig\n  <span class=\"tok-keyword\">target:</span> <span class=\"tok-string\">&quot;x86_64-windows-gnu&quot;</span>\n  <span class=\"tok-keyword\">backend:</span> <span class=\"tok-quote\">&apos;</span>native\n  <span class=\"tok-keyword\">icon:</span> <span class=\"tok-string\">&quot;windows/app.ico&quot;</span>\n  <span class=\"tok-keyword\">product-name:</span> <span class=\"tok-string\">&quot;My Game&quot;</span><span class=\"tok-paren\">)</span></code></pre><p>A missing or invalid icon fails the build with a clear message instead of quietly shipping without one. Configs that don&apos;t use these keys build exactly as before.</p>",
      "summary": "A regular expressions and web reliability release, plus Windows icons. Sigil now has regular expressions in the standard library. Web builds could also fail in ways nothing on the code path explained: a type error from nowhere, or text that parsed fine natively coming back as garbage. Crash The Stack hit both, including a menu that froze on phones.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.4/",
      "url": "https://usesigil.org/releases/0.22.4/",
      "title": "Sigil 0.22.4",
      "date_published": "2026-09-22T00:00:00Z",
      "content_html": "<p>A web audio release. A page that is not cross-origin isolated no longer mixes its sound on the main thread, so a long game frame no longer pops the audio. Found by Crash The Stack&apos;s live soundtrack on a phone over a plain http host.</p><p><strong>Existing projects build without changes.</strong> The <code>(sigil audio sink)</code> facade is the same; a project picks the new path up by rebuilding with this toolchain.</p><h2>The worklet drains a page that cannot share memory</h2><p>The bridge used an <code>AudioWorkletNode</code> only when the page could share memory with it (cross-origin isolated) and fell back to a <code>ScriptProcessorNode</code> on the main thread otherwise, where any frame that ran long starved the ring. A third mode now sits between them: the worklet is fed the frames by message and drains them on the audio thread, with the depth and underrun readings lagging by at most one report. <code>SigilWasmAudio.sinkMode(id)</code> says which path a sink is on (<code>worklet-sab</code>, <code>worklet-msg</code> or <code>scriptprocessor</code>). A browser without a worklet keeps the old path. <code>test/integration/test-wasm-audio-worklet-msg.mjs</code> drives the processor against the real producer and bounds the lag with the main thread stalled.</p>",
      "summary": "A web audio release. A page that is not cross-origin isolated no longer mixes its sound on the main thread, so a long game frame no longer pops the audio. Found by Crash The Stack's live soundtrack on a phone over a plain http host.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.3/",
      "url": "https://usesigil.org/releases/0.22.3/",
      "title": "Sigil 0.22.3",
      "date_published": "2026-09-21T00:00:00Z",
      "content_html": "<p>A native-codegen correctness release. Two defects found by Crash The Stack in its first week of play, both invisible to the bytecode VM and to every in-process test suite: a release binary that could crash at startup depending on where it was built, and a web build that leaked memory on every frame.</p><p><strong>Existing projects build without changes.</strong> A declaration such as <code>sigil: &quot;0.22&quot;</code> already admits 0.22.3. Nothing in the language or the build surface changed; a project picks the fixes up by rebuilding with this toolchain.</p><h2>Quoted literals are safe under the collector</h2><p>A native binary rebuilds every quoted list or vector at module init, and that reconstruction was not GC-safe: a collection landing in the middle of a large literal could leave later elements dangling, and the crash came much later, in the first major collection. Whether it happened depended on the length of the checkout&apos;s path, so the same commit could run in one worktree and segfault in another.</p><p>Literals are now rooted and barriered while they are rebuilt, on both the native and the wasm targets. <code>test/integration/test-native-literal-gc.sh</code> forces the timing and is red on 0.22.2, green here.</p><h2>Tail calls with more than eight arguments no longer leak</h2><p>A tail call between native-codegen procedures with nine or more arguments carried its arguments in a heap buffer that was never freed. A game&apos;s draw path can make thousands of such calls a frame: the published web build grew by about 90 KB per frame and hit <code>FATAL: Out of memory</code> after an hour or two on a board, and the native release binary grew from 283 MB to 907 MB in three and a half minutes.</p><p>Both trampoline loops now free the buffer once the callee returns. <code>test/integration/test-native-tramp-args.sh</code> bounces five million nine-argument calls and caps the resident set&apos;s growth at 8 MB; on 0.22.2 it grows by 390 MB, here by zero.</p><h2>Reading a crash in a release binary</h2><p>Release binaries are stripped. To get a symbolized frame, relink the objects under <code>build/release/native/</code> without <code>-s</code> and re-append the bundle trailer. To catch a missed barrier directly, run with <code>SIGIL_GC_AUDIT_BARRIERS=1</code> and force the timing with <code>SIGIL_GC_ADAPTIVE_YOUNG=0 SIGIL_YOUNG_THRESHOLD=&lt;n&gt;</code>; <code>SIGIL_GC_STRESS=1</code> is blind to this class, since its collections are all major.</p>",
      "summary": "A native-codegen correctness release. Two defects found by Crash The Stack in its first week of play, both invisible to the bytecode VM and to every in-process test suite: a release binary that could crash at startup depending on where it was built, and a web build that leaked memory on every frame.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.2/",
      "url": "https://usesigil.org/releases/0.22.2/",
      "title": "Sigil 0.22.2",
      "date_published": "2026-09-18T00:00:00Z",
      "content_html": "<p>A patch for the seams a game project hit in its first two days: between the two backends, between a game and the web loader, between what a docstring says and what a procedure does. It also repairs a 0.22.1 regression that stopped script-style source files from building. Everything here came from Crash The Stack&apos;s toolchain notes, each item with the measurement that found it.</p><p><strong>Existing projects build without changes.</strong> A declaration such as <code>sigil: &quot;0.22&quot;</code> already admits 0.22.2. The one behaviour a page might notice: <code>sigil_wasm_dispatch_event</code> now reports a handler&apos;s positive integer instead of 0, so a page that treated any non-zero return as failure should treat only a negative one that way.</p><h2>Script-style source files build again</h2><p>0.22.1 rejected any source whose declaration forms the bytecode-input identity extractor could not model, and that included a script-style file under <code>src/</code>: a top-level <code>(import ...)</code> followed by expressions and no <code>define-library</code>, loaded by the wasm runtime at startup. <code>sigil build</code> died with <code>unsupported dependency declaration or feature condition</code> on a repository 0.22.0 built.</p><p>A script&apos;s top-level imports are now modeled declaration positions: they are dependency edges, so the script compiles after the modules it imports, and its cache identity is complete. A form the extractor still cannot model (an import inside a procedure body, an <code>include</code>, a <code>(library ...)</code> feature test) no longer fails the build: the file compiles fresh, the build prints one <code>UNCACHED</code> line naming the form, and the package declines cache reuse until the form is gone. The contract is in the <a href=\"https://usesigil.org/docs/reference/build-cache/\">build cache reference</a>.</p><h2>The two backends answer alike</h2><p><code>(map f a b)</code> with two lists raised <code>map: expected 2 argument(s), got 3</code> in the bytecode VM and ran under native codegen, which silently dropped the extra list; one source tree gave a green browser build and a native game that drew nothing. Core <code>map</code> and <code>for-each</code> now take any number of lists and stop at the shortest, as R7RS says.</p><p>A <code>define</code> inside a <code>cond-expand</code> clause in a library or procedure body is now a define of that body on every backend. Before, the bytecode compiler raised <code>GET_CELL: expected upvalue cell</code> at the call and native codegen resolved the name to an immediate, so a module that differed per target had to cond-expand inside every body.</p><p><code>write</code> prints a symbol as <code>|...|</code> when the reader would not read the bare name back as that symbol: <code>|1|</code> (a keyboard&apos;s digit key), <code>|a b|</code>, <code>|foo:|</code>. Written data round-trips through <code>read</code> again; <code>display</code> is unchanged.</p><p>A conformance fixture now runs a list of stdlib forms through the differential harness on bytecode, native ELF and native wasm and fails on any disagreement; <code>make test-conformance</code> runs the bytecode and native rungs and is part of <code>make test</code>. A second fixture records a known gap for its own ticket: native codegen does not check call arity, so a two-argument procedure called with three runs as if called with two.</p><h2>The web loader keeps early dispatches and answers questions</h2><p>A dispatch made from <code>SigilWasmBridges.setInstance</code>, where a page first learns the wasm instance, reached a runtime with no VM and was dropped with a success-shaped return. The loader now publishes <code>globalThis.SigilWebApp</code>: a dispatch made before <code>sigil_wasm_start</code> is queued and delivered, in order, right after it; <code>SigilWebApp.ready</code> resolves then, and <code>document</code> receives a <code>sigil-web-app-ready</code> event for scripts that ran before the loader.</p><p><code>SigilWebApp.dispatch()</code> returns the handler&apos;s integer, so a page can ask the app a question through the return value instead of polling <code>localStorage</code>. A <code>pointerdown</code> on a canvas is forwarded as <code>(&quot;pointerdown&quot;, &quot;x,y,mods&quot;)</code> in canvas-buffer pixels with a modifier bitmask, unless the page&apos;s own listener handled it, so a game page no longer re-implements pointer forwarding.</p><h2>Structs: update through a procedure, compare by fields</h2><p><code>(struct-update s field: proc ...)</code> copies a struct with each named field passed through its procedure, replacing the read-apply-rebuild idiom <code>(session s trace: (trace-hint (session-trace s)))</code>. <code>(struct-equal? a b)</code> compares two instances of one type field by field. <code>equal?</code> on instances is still identity in this release.</p><h2>One package, native and web configs</h2><p>Three sigil-build gaps met by a package that builds both a native binary and a browser application:</p><ul><li>A dev-dependency is built only when tests are built (<code>--tests</code>, <code>sigil test</code>). A   plain <code>sigil build --config web</code> used to compile the test runner&apos;s whole closure for   wasm and fail in mbedtls two and a half minutes in.</li><li>A task&apos;s <code>configs:</code> gate is honoured. <code>build</code> can be declared once for the wasm   config, linking a web application, while the native configs keep the default task.</li><li>A dependency library&apos;s <code>native-init:</code> is called in a wasm build: the build generates   a weak <code>sigil_wasm_app_register</code> that calls each one, so no hand-written   <code>app-register.c</code> is needed, and a package that still ships its own keeps winning.</li></ul><h2>Docstrings say what raises</h2><p>Procedures in <code>(sigil fs)</code> and <code>(sigil io)</code> that can raise carry a <code>raises:</code> line, and those that answer <code>#f</code> say so; the browser storage facades document that they never raise and what a missing value looks like. <code>read-file-bytes</code> takes <code>missing:</code> like <code>read-file-string</code>, and <code>with-current-directory</code> raises when it cannot enter the directory instead of running its thunk in the old one.</p>",
      "summary": "A patch for the seams a game project hit in its first two days: between the two backends, between a game and the web loader, between what a docstring says and what a procedure does. It also repairs a 0.22.1 regression that stopped script-style source files from building. Everything here came from Crash The Stack's toolchain notes, each item with the measurement that found it.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.1/",
      "url": "https://usesigil.org/releases/0.22.1/",
      "title": "Sigil 0.22.1",
      "date_published": "2026-09-16T00:00:00Z",
      "content_html": "<p>A cache-correctness and test-reliability release. The package cache now proves what it reuses: it hashes the bytes of every input and output it depends on, binds the compiler&apos;s own identity into the cache key, and declines to reuse anything it cannot fully account for. The test runner stops counting other checkouts&apos; tests as your own, and its file summary can finally report a shortfall. An opt-in evidence journal records what a test run actually loaded and executed, for qualifying a release rather than trusting a green suite.</p><p><strong>Existing projects build without changes.</strong> No dependency-wide version bumps are required; a declaration such as <code>sigil: &quot;0.22&quot;</code> already admits 0.22.1. The first build after upgrading rebuilds packages whose manifests predate the new schema, and later builds reuse as before. No store reset is needed.</p><h2>The package cache proves what it reuses</h2><p>Package reuse used to trust a manifest whose inputs matched by size and modification time. Sigil now hashes the bytes of every input and every required output before reusing a package, so a same-size edit with a restored timestamp, a broken link, or a missing documentation sidecar rebuilds instead of restoring a stale result. Older manifests miss safely and are replaced after the next complete build.</p><p>The compiler&apos;s own identity is part of the cache key too: the executable&apos;s bytes and its startup libraries, not the version it reports. A rebuilt toolchain that still says 0.22.0 no longer reuses bytecode from the previous one. Full contract in the <a href=\"https://usesigil.org/docs/reference/build-cache/\">build cache reference</a>.</p><h2>Test discovery stays inside your checkout</h2><p>In a repository without a <code>test/</code> directory, discovery walks the whole tree and used to descend into other checkouts underneath it. One repository counted 117 of 319 per-file results from <code>.worktrees/</code> and <code>.claude/worktrees/</code>, including a test that passed in the real tree and failed in a stale copy. Discovery now stops at any directory containing a <code>.git</code> and prints a SKIP line saying so.</p><p>The <code>Files:</code> summary line could not report a shortfall: a run in which nothing executed still printed <code>Files: 4 of 4 run</code>. It now reads <code>X of Y loaded</code>, so <code>0 of 1 loaded, 1 failed to load</code> beside <code>Tests: 1 passed</code> tells you exactly what happened.</p><h2>Opt-in test execution evidence</h2><p>Set <code>SIGIL_TEST_EVIDENCE</code> to a new JSONL path and <code>SIGIL_TEST_RUN_ID</code> to a run token before starting Sigil, and the runtime journals every input buffer it acquired (with its SHA-256), every module initialization, and the runner&apos;s selection and completion records. <code>scripts/observe-test-evidence.py</code> runs one <code>sigil test --sgl</code> invocation under an external observer and reconciles the journal against the expected files and module bytes; it exits 2 for incomplete evidence, and a missing or truncated journal never qualifies as a pass. Inconsistent evidence is rejected, partial-load fail-fast is honored, and large journals stream during validation. This is POSIX-only and experimental; Windows and browser builds refuse the opt-in with exit 74. See <a href=\"https://usesigil.org/docs/reference/runtime-evidence/\">runtime evidence</a> and <a href=\"https://usesigil.org/docs/reference/test-evidence/\">test execution evidence</a>.</p><h2>Runtime and process fixes</h2><p>Async readers and writers are preserved when the opposite direction becomes ready on the same socket or descriptor, so duplex connections no longer freeze under backpressure; the visible symptom was terminal scrollback replay stalling on reconnect. Native closures stay rooted across allocating metadata initialization, closing a collection window that WebAssembly builds could hit. PTY children on macOS and BSD now receive the requested environment, so a <code>TERM</code> override reaches the shell as it already did on Linux. The build store retries a content lock that its previous owner releases mid-contention instead of reporting an unwritable store. Compiler summary export lookups are indexed, development CLI entrypoints read startup library roots the same way the bundled CLI does, and the release Scheme observation timeout is configurable.</p>",
      "summary": "A cache-correctness and test-reliability release. The package cache now proves what it reuses: it hashes the bytes of every input and output it depends on, binds the compiler's own identity into the cache key, and declines to reuse anything it cannot fully account for. The test runner stops counting other checkouts' tests as your own, and its file summary can finally report a shortfall. An opt-in evidence journal records what a test run actually loaded and executed, for qualifying a release rather than trusting a green suite.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.22.0/",
      "url": "https://usesigil.org/releases/0.22.0/",
      "title": "Sigil 0.22.0",
      "date_published": "2026-09-07T00:00:00Z",
      "content_html": "<p>A scripting and reliability release. Sigil is more convenient for operational scripts, the bundled client supports TLS 1.3, and builds and sockets recover more reliably from cache and resource problems. Direct file execution now provides the scripting prelude automatically; ordinary evaluation remains available through <code>sigil eval -f</code>.</p><p>No dependency-wide version bumps are required. A declaration such as <code>sigil: &quot;0.21&quot;</code> already admits 0.22.0, but <code>sigil deps install</code> preserves a valid locked runtime. Upgrading the CLI does not rewrite project lockfiles. Projects that start using the new runtime or standard-library APIs should raise their minimum to <code>sigil: &quot;0.22&quot;</code> and run <code>sigil deps install</code>. To refresh dependencies and explicitly move the runtime within the existing compatibility window, use <code>sigil deps update --upgrade-sigil</code>; this also updates ordinary dependencies.</p><h2>TLS 1.3 and connection diagnostics</h2><p>The bundled TLS client now supports TLS 1.3 alongside TLS 1.2, using Mbed TLS 3.6.7. Certificate and hostname verification remain enabled. New <code>tls-connect/details</code> and <code>tls-upgrade/details</code> procedures expose per-attempt handshake failures and negotiated protocol details; existing connection APIs keep their behavior.</p><h2>A scripting prelude and reliable command capture</h2><p><code>sigil script.sgl</code> now loads <code>(sigil shell prelude)</code>, including short text/path names, positional arguments, iteration, polling, and temporary-directory scopes. <code>sigil eval -f file.sgl</code> keeps ordinary explicit imports; <code>sigil eval --quiet</code> suppresses implicit expression results. See the <a href=\"https://usesigil.org/docs/guides/shell-scripting/\">shell scripting guide</a>.</p><p>Unix command capture drains both output streams while feeding stdin, fixing stalls when output fills a pipe. Background commands can write directly to log files and explicitly detach from the launching script. Unsupported options now fail before spawning. File/directory scopes clean up when their body raises, and printing to a closed port raises instead of crashing.</p><p>Shebang scripts work with formatter verification and strict test classification. Reader errors retain the source filename. Concurrent auto-compilation uses unique staging files, and failed cache refreshes stop execution instead of loading stale bytecode.</p><p>Script and eval errors now show concise diagnostics on stderr, with expanded call history available through <code>--error-history</code>. The guide includes a complete HTTP harness with child cleanup and retained failure evidence, and explains how polling deadlines interact with blocking readiness probes.</p><h2>Shared text and iteration helpers</h2><p><code>(sigil string)</code> adds <code>string-truncate</code>, with a configurable truncation marker, and <code>string-contains-any?</code> for literal alternatives. <code>(sigil iteration)</code> adds <code>each</code> for lists and arrays, and <code>(sigil time)</code> adds <code>wait-until</code> for readiness polling. These helpers are also available in the scripting prelude.</p><p><code>read-file-string</code>, also exposed as <code>read-text</code> in scripts, reads complete UTF-8 files without truncating multibyte text. Its new <code>missing:</code> and <code>trim:</code> options cover optional state files and whitespace trimming without hiding permission or other read errors.</p><h2>Cached builds retain API documentation</h2><p>Restoring compiled modules from the build store now restores their documentation metadata too, keeping help, editor navigation, and MCP documentation available after a cache hit. When a second checkout reuses that cache, source locations are updated to point into the current checkout. Missing or unusable metadata causes recompilation instead of leaving an incomplete cache result in place.</p><h2>Socket polling under descriptor exhaustion</h2><p>On systems using epoll or kqueue, socket polling falls back to <code>poll</code> when the process or system cannot allocate another file descriptor. Applications can continue draining and closing existing sockets, including high-numbered descriptors, so they can recover from that condition.</p><p>The async scheduler rounds future fractional-millisecond deadlines up when polling, avoiding a busy loop while waiting for the timer to expire.</p><h2>WebSocket event notifications for browser hosts</h2><p>The WebAssembly network bridge accepts an optional <code>onEvent(socketId, eventType)</code> callback when creating imports or opening a WebSocket directly. Hosts can wake their event loop when a connection, message, error, or close event is queued. The existing event queue remains available, and a callback exception does not discard the event.</p><h2>Updated bundled libraries</h2><p>The CLI now bundles HTTP 0.18.7, log 0.17.0, nREPL 0.17.2, MCP 0.18.0, SQLite 0.16.3, and LSP 0.16.3 alongside crypto 0.16.6 and TLS 0.16.7. The MCP transition pin is replaced by its published release. The refreshed dependency graph no longer locks the retired standalone socket, docs, and ANSI providers.</p><p>Text log fields now quote and escape unsafe values; JSON log output is unchanged. Requesting dependency-subcommand help no longer starts an install or upgrade. The updated HTTP library also includes byte-preserving uploads, streamed responses, and multipart file parsing.</p>",
      "summary": "A scripting and reliability release. Sigil is more convenient for operational scripts, the bundled client supports TLS 1.3, and builds and sockets recover more reliably from cache and resource problems. Direct file execution now provides the scripting prelude automatically; ordinary evaluation remains available through sigil eval -f.",
      "tags": [
        "release"
      ]
    },
    {
      "id": "https://usesigil.org/releases/0.21.0/",
      "url": "https://usesigil.org/releases/0.21.0/",
      "title": "Sigil 0.21.0",
      "date_published": "2026-09-02T00:00:00Z",
      "content_html": "<p>A test-reporting honesty release, and the first one that can turn a previously green suite red. <code>sigil test</code> could report success while running fewer tests than it appeared to, or none at all, and nothing in its output said so. Both cases now fail the run.</p><p>It also adds <code>(sigil grant)</code>, a standard library type for capability grants and the algebra for narrowing them. Nothing enforces a grant yet; this is the shared vocabulary, not a sandbox.</p><p><strong>This is a breaking change.</strong> If your <code>test/</code> directory holds a <code>.sgl</code> file that registers tests and is not named <code>test-*.sgl</code> or <code>*-test.sgl</code>, <code>sigil test</code> now exits 1 and names it. That file has never run. See &quot;If this release turns your build red&quot; below.</p><p><strong>It reaches you without any change on your part.</strong> The <code>sigil:</code> field is a floor with an implicit 1.0.0 ceiling, so a project declaring <code>0.19</code>, <code>0.20</code>, <code>0.20.3</code> or <code>^0.20</code> admits 0.21.0 and picks it up on its next <code>sigil deps install</code>. There is no pre-1.0 version you can be pinned to that holds this back. The minor bump is a signal, not a barrier.</p><h2><code>sigil test</code> always reports how many files it ran</h2><p>The summary&apos;s <code>Files:</code> line used to print only when discovery selected at least one file. A package whose tests are a native binary could lose its entire Sigil suite to a rename and still print <code>Tests: 0 passed (0 total)</code> under a green <code>All tests passed!</code>, with no <code>Files:</code> line at all, and exit 0. Zero tests run and every test passing looked identical. The line now always prints, including <code>Files: 0 of 0 run</code>.</p><h2>A run that selects no Sigil test files fails</h2><p>If discovery selects nothing while a <code>.sgl</code> file sits in <code>test/</code> or <code>tests/</code>, <code>sigil test</code> exits 1 and names the files it skipped. A package with genuinely no Sigil tests has no <code>.sgl</code> under <code>test/</code> and is unaffected.</p><h2>Strict discovery is on by default</h2><p>Discovery selects <code>test-*.sgl</code> and <code>*-test.sgl</code> only, so a file named anything else sits beside the suite and never runs. The summary cannot show this: a file that never matched is upstream of every count it reports, which is how a repository can publish <code>Files: 5 of 5 run</code> with a sixth file of real tests unrun beside the five.</p><p><code>sigil test</code> now fails when a <code>.sgl</code> file that is a direct child of a test directory imports <code>(sigil test)</code>, registers at least one test, and was not selected. It reads the file rather than running it, so a file that starts a server or calls <code>exit</code> is examined safely, and a file that mentions <code>(sigil test)</code> only in its comments is not flagged.</p><p>Files in subdirectories of <code>test/</code> are never considered, because that is where fixtures, integration drivers and helper modules live. Across 175 checkouts holding 5193 test <code>.sgl</code> files, 1850 are not selected by discovery and 1647 of those are nested. The rule flags 28 files in four projects, and every one is a real test file.</p><h2>If this release turns your build red</h2><p>The message names the file:</p><pre>✗ STRICT: 1 .sgl file(s) in a test directory register tests and were\n  not selected by discovery. They have never run.\n    - ./test/parser-regression.sgl</pre><p>That file has never run, so the question the red is asking you is whether it should have. Both answers are a one line change.</p><p><strong>If it should be running</strong>, rename it to <code>test-parser-regression.sgl</code> (or <code>parser-regression-test.sgl</code>). It joins the suite on the next run, and you get back however many tests it holds.</p><p><strong>If it should not</strong>, because it is a live gate or a driver invoked by its own script, add it to <code>.testignore</code> in your project root. Globs work, so one line covers a family:</p><pre>test/parser-regression.sgl\npackages/myapp/test/*-live.sgl</pre><p><code>.testignore</code> is the same file discovery already honours, not a new mechanism.</p><p>If you cannot deal with it now, <code>sigil test --no-strict</code> restores the previous behaviour and you can come back to it. It is worth knowing that this is the larger of the changes available to you: it means editing your CI config, where the two fixes above are a rename or a single line, and it leaves the file unrun in the meantime.</p><h2><code>(sigil grant)</code>: a shared vocabulary for capability grants</h2><p>A new standard library module holding an immutable capability grant and the algebra for comparing and narrowing one. A grant is a set of facets, each naming a kind (<code>fs</code>, <code>exec</code>, <code>net</code>, <code>import</code>, <code>eval</code>, <code>ffi</code>, <code>pty</code>), a mode, and a resource pattern. A facet is a permission, and absence is denial:</p><pre lang=\"scheme\" class=\"highlighted\" tabindex=\"0\"><code><span class=\"tok-paren\">(</span><span class=\"tok-special\">import</span> <span class=\"tok-paren\">(</span>sigil grant<span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span>\n\n<span class=\"tok-paren\">(</span><span class=\"tok-special\">define</span> g <span class=\"tok-paren\">(</span>grant <span class=\"tok-keyword\">fs:</span> <span class=\"tok-quote\">&apos;</span><span class=\"tok-paren\">(</span>rw <span class=\"tok-string\">&quot;/work&quot;</span><span class=\"tok-paren\">)</span> <span class=\"tok-keyword\">exec:</span> <span class=\"tok-quote\">&apos;</span><span class=\"tok-paren\">(</span>allowlist <span class=\"tok-string\">&quot;git&quot;</span> <span class=\"tok-string\">&quot;sigil&quot;</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span>\n<span class=\"tok-paren\">(</span>grant-covers? g <span class=\"tok-paren\">(</span>grant-from-specs <span class=\"tok-quote\">&apos;</span><span class=\"tok-paren\">(</span><span class=\"tok-string\">&quot;fs:ro:/work/src&quot;</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span>   <span class=\"tok-comment\">; =&gt; #t\n</span><span class=\"tok-paren\">(</span>grant-covers? g <span class=\"tok-paren\">(</span>grant-from-specs <span class=\"tok-quote\">&apos;</span><span class=\"tok-paren\">(</span><span class=\"tok-string\">&quot;fs:rw:/etc&quot;</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span><span class=\"tok-paren\">)</span>        <span class=\"tok-comment\">; =&gt; #f</span></code></pre><p><code>grant-covers?</code> is the partial order, &quot;has at least as much authority as&quot;. <code>attenuate</code> narrows. <code>empty-grant</code> permits nothing and <code>grant-allow-all</code> permits everything. <code>grant-facets</code> returns the facets for a consent prompt or an audit line, and <code>grant=?</code> compares authority rather than spelling, so two grants written differently that permit the same things compare equal.</p><p>Several tools in the Sigil ecosystem had each grown a grant table of their own, with their own idea of what a filesystem root or an allowed command meant. This is the type they can share, so a narrowing computed in one place means the same thing in another.</p><h2>Narrowing composes, and cannot widen</h2><p><code>(grant-covers? g (attenuate g narrowing))</code> holds for every pair of grants, so attenuating can only ever remove authority. <code>attenuate</code> also returns the <em>greatest</em> grant covered by both of its inputs rather than merely some grant covered by both, which is what makes narrowings safe to compose: applying two restrictions in either order gives the same authority, and a grant narrowed twice is exactly the intersection of the two, not something smaller that happens to be under both.</p><p>That distinction is easy to state and easy to get wrong, so the module ships 90 tests that check these as properties over a generated corpus of grants rather than as examples, along with the component boundary that keeps <code>/a/bc</code> from counting as under <code>/a/b</code>, and the argv[0] match that lets an allowlist entry <code>git</code> permit <code>git</code> and refuse <code>/tmp/evil/git</code>.</p><h2>Grants serialize without changing what they permit</h2><p><code>grant-&gt;specs</code> renders a grant to strings like <code>fs:rw:/work</code> and <code>exec:allowlist:git,sigil</code>; <code>grant-from-specs</code> parses them back. Every spec string <code>(sigil system grant)</code> accepts today parses to the same authority here.</p><p>A grant round-trips through that surface preserving what it permits, and that holds by construction rather than by testing: a resource the grammar cannot carry unambiguously is refused when the grant is built, so the value that would survive the round trip as something broader cannot be created in the first place. The cost is worth knowing up front. An exec command cannot contain a comma, because the comma is what separates commands in <code>exec:allowlist:</code>. Filesystem paths may contain commas and colons, and both round-trip.</p><h2>What this does not do</h2><p>This release ships the value and the algebra, and nothing else. Nothing in Sigil consults a grant yet: there is no enforcement, no sandbox, and no VM involvement. Importing <code>(sigil grant)</code> does not restrict what your code can do.</p><p>It is useful now to any tool that already decides what it will allow and wants to express that decision in a form another tool can read, compare and narrow. The checking layer is later work.</p>",
      "summary": "A test-reporting honesty release, and the first one that can turn a previously green suite red. sigil test could report success while running fewer tests than it appeared to, or none at all, and nothing in its output said so. Both cases now fail the run.",
      "tags": [
        "release"
      ]
    }
  ]
}
