Sigil 0.18.5

release

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

Download 0.18.5Get started

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

A stray brace no longer hangs the compiler

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

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

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

A misplaced paren no longer silently guts a function

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

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

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

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

list-ref tells you what went wrong

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

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

base64url-encode works on inputs over ~192 bytes

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

Stack traces stop pretending to be stacks

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

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

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

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

Shutdown stops reading freed memory

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

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

Behaviour changes

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

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

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

Known limitations

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