Sigil 0.22.10
A correctness and build-cache release. Optimized builds could compute wrong answers in two patterns, compiled code skipped argument-count checks, and Sigil's caches could run the wrong compiled code, ship stale native code, or throw away work they had already done. All of that is fixed, and an unchanged rebuild of a program now recompiles almost nothing.
Relock projects that make native or web builds. Code compiled by 0.22.10 needs the 0.22.10 runtime, and linking it against an older one fails with undefined reference to 'sigil_native_requires_sigil_lib_0_22_10_checked_calls'. Run sigil deps update --upgrade-sigil and rebuild. It moves the project's Sigil packages to the newest version its sigil: field allows, and it also re-resolves every other dependency, so expect those to move too. If sigil: pins a version below 0.22.10, raise it first.
Scripts, tests and the REPL could run the wrong compiled library
sigil <file>, sigil eval, sigil test and the REPL cache compiled libraries. That cache found a library by its name and trusted its timestamp, so it could hand back the wrong compiled copy: two projects that lock different versions of one library ran each other's code, and so did a scratch copy with a newer timestamp, an in-place edit that kept the timestamp, or a change to SIGIL_FEATURES. Every compiled module is now stored under what actually shaped it: its source, the compiler, the settings that change output, its dependencies, its path and the host. The new cache lives in $XDG_CACHE_HOME/sigil/compiled/; the old $XDG_CACHE_HOME/sigil/lib/<commit>+sgbN directories are no longer used and can be deleted.
Scripts and tests also follow the project's redirects now: sigil <file>, eval and test take a redirected dependency's sources from the redirect target, and sigil --redirects FILE ... sets redirects for one command.
Optimized builds computed wrong answers in two patterns
Release and web builds use the optimizer (O1 and O2), and so does bytecode compiled at those levels. Two of its passes could treat a value that came from a conditional as if it always came from one branch:
- Calling a procedure chosen by a conditional could run the wrong one:
(define (caller choose replacement)
(let ((target (if choose (lambda (n) (+ n 1)) replacement)))
(+ (target 7) 1))) With choose false and a replacement that returns 41, this returned 9 instead of 42.
- A value chosen by a conditional could be mistaken for one of the branches. With
selectedset to either an empty list or(cons 1 '()),(null? selected)answered#tfor the one-element list.
Both returned the wrong value silently, with no error. The optimizer now checks that a value has a single source before it relies on one. The fixes change no other generated code: the standard library and the packages compile to identical C, and so do both Flux Harmonic games for the second fix.
Builds reuse more and never ship stale code
- An unchanged rebuild recompiles almost nothing. A package with a custom build step (C sources, an archive, generated version info), or with a source the build can't model (an
include, a library condition incond-expand), used to switch caching off for every module in the package. Now only the files that need it compile fresh. Crash The Stack's unchanged rebuild went from 170 recompiled modules to 3 in its dev configuration, and from 167 to 19 for the web. - A build could ship old native code. After an edit that kept the file's size and timestamp,
sigil build --no-cache, and a build whose cache failed to start, reused the old compiled object and printed the old behavior. Native objects are now reused only when their inputs match by content. - Macros that read files are handled safely. A module whose macros read a file, the environment, the command line or a process while expanding is no longer cached at all: it compiles fresh every build, so a data-file change always reaches the output.
--verboseprints anUNCACHEDline for it. - The web build's link, asyncify and wasm-opt steps are cached across builds, reused when the tool, its arguments, its environment and its inputs all match by content.
- Package manifests are always evaluated in the top-level environment. On a release sigil, a manifest loaded by a build step was evaluated inside the compiler's own module, which is why release builds of apps that build sigil-lib stopped caching after sigil-lib.
- A build step that defines or changes a library module in the build process (for example with
eval-string) turns caching off for the rest of that build, and--verbosesays so:NO-CACHE a task step registered or redefined <module>. - A REPL or
sigil build --watchsession that outlives a rebuild of Sigil itself stops caching, instead of filing new entries under the new compiler.
Fixes
- Compiled code now checks argument counts. Calling a procedure with too few arguments read garbage in native code and an undefined or stale value on the web, and extra arguments were silently dropped. Both now raise
arity-errorwith the bytecode VM's message, for example "<lambda>: expected 2 argument(s), got 1". Code that caught the oldruntime-errorfrom the native bridge should catcharity-errorinstead. Calling a delimited continuation with the wrong number of values from compiled code now raises too, as on the bytecode VM. applyin tail position is now a real tail call in native builds. A loop that recursed throughapplya million times crashed with a segmentation fault; it now runs in constant stack, as on the bytecode VM.
Known issues
- A build step that re-
defines orset!s an existing binding in a library module isn't detected by the new step check. - A module that imports
(sigil user), or whose macros look up modules outside their own imports, is never cached. - Web builds of Crash The Stack still recompile
case.sgl,regex.sglandsigil/audio/sink.sglon every build (as on 0.22.9). (list? x)on a circular list never returns.- The bytecode VM can stop with "GET_CELL: expected upvalue cell" when a multi-shot
call/cccontinuation is re-entered, and with "SET_CELL: expected upvalue cell" on some module bindings. - Release builds of desktop apps still run the standard library and dependency packages as bytecode; only the app's own modules are native.