Sigil 0.17.10
A live-redefinition and interactive-correctness release. Native-compiled programs can now pick up redefinitions while they run, your editor can attach to an already-running program and evaluate straight into its modules, and a cluster of macro, module-resolution, and exception-cleanup bugs that surfaced during live-coding are fixed. Existing projects keep building without changes.
Redefine code in a running native binary
Native-compiled code can now be redefined while it runs. Until now, a define-library body compiled its defines to lexical cells, but the bindings table kept a separate snapshot cell per binding, so evaluation (through the REPL or nREPL) and compiled code operated on different storage. Redefining a binding in a running native binary had no effect, at any optimization level. Those cells are now unified: a captured define installs its lexical cell directly as the binding cell, so an evaluated redefinition reaches every compiled call site on its next call. This is the foundation for live-coding into a running native program, a game loop or a web server, and having your edits take effect without a restart.
This also closes a native-backend use-after-free that shipped in 0.17.8: the generated code cached a binding's storage location inline, and after a redefinition replaced and freed the old cell, a cached pointer could dereference freed memory. A version counter now stamps every native binding cache and bumps on each redefinition, so a stale entry re-resolves instead of reading a freed cell. The same counter is what lets a redefinition propagate to native call sites at all, bringing the native backend to parity with the register VM.
Attach your editor to a running program
sigil-nrepl-attach connects Emacs to an nREPL server that is already running, one embedded in your game or web server, without spawning a new one. Previously the editor could only drive a server it had started itself. Alongside it, sigil-send-region and sigil-send-definition now evaluate in the source buffer's own module rather than the session's default, so a redefinition lands where the running program can see it, across module boundaries, with no manual module switch. Paired with the native live-redefinition work above, you can edit a definition in your buffer and watch a running native program pick it up.
Live-coding correctness
Four fixes for bugs that surfaced while live-coding during streams:
- Nested evaluation no longer crashes or hangs the host. A preemptive yield (added in 0.17.9) could fire in the middle of a nested
eval, compile, or macro-expansion and abort into C code that was not prepared to be unwound, taking down a long-running server. Because the process kept running elsewhere, it looked like a hang rather than a crash. Preemptive yields are now held behind a barrier so these nested extents stay atomic and yield only at a safe point. syntax-local-valueresolves against the caller's module. Broken since 0.15.0: once module initialization had finished, looking up a macro binding withsyntax-local-valuefrom the module that defined it found nothing, because resolution only consulted a context that is set during expansion. It now falls back to the calling frame's module, so the lookup works at ordinary runtime.macroexpandresolves against the caller's module. The same staleness, one function over.(macroexpand '(my-macro ...))called at runtime from the module that definesmy-macronow expands it instead of returning it untouched.dynamic-windcleanup runs through an intervening prompt. Since the 0.17.5 exception-cleanup hardening, adynamic-windafterthunk was skipped when an exception unwound through acall-with-promptthat was not the raise's target. The scheduler wraps every task in a prompt, so this silently skipped cleanup in async code whenever an exception crossed adynamic-wind. Cleanup now runs for every winder between the raise and the handler that catches it, regardless of a delimited prompt in between.
A busy connection no longer starves other sockets
Sigil's async runtime is a single-threaded cooperative scheduler, and it serviced socket and file-descriptor I/O only when its run queue had nothing else ready. That left a gap: a task that stays perpetually ready, for example an HTTP server's connection-drain loop while a browser holds a long-lived connection open, kept the queue busy, so the first read on every other socket waited until the busy task finally let the queue drain. Timer and channel work kept flowing, which is why the symptom was so confusing: a live dashboard's periodic updates ticked along normally while a chat client's first message, an OBS websocket's connect, or an audio stream's startup could hang for thirty seconds to several minutes before their socket was ever polled.
This release adds await-readable-any, a single waiter over a set of sockets that wakes once as soon as any one of them is readable, so a server can wait on its whole socket set through the scheduler in one suspension instead of busy-polling. Paired with the sigil-http release that adopts it (its serve loop now suspends on the socket set rather than spinning), an application that holds a long-lived connection open no longer starves its other sockets: first reads land in milliseconds instead of minutes.