Sigil 0.17.15
A string and character performance release. Text operations that used to crawl over long buffers are now native and linear: indexing an ASCII string is roughly 230 times faster, scanning mixed text has a new native cursor, and the bulk string, vector, and bytevector operations are single-pass native code. It also picks up shebang support for executable scripts and two runtime fixes from earlier in the series. Existing projects build without changes.
Indexing ASCII text is fast again
string-ref and substring now take an O(1) fast path when a string is all ASCII, in both the native builtins and the bytecode VM. A scan that touches every character of a 160,000-character ASCII string dropped from 157.7 seconds to 0.68 seconds, about 230 times faster. Syntax highlighters, lexers, and any code that walks a string by index get this with no changes. One caveat: a single non-ASCII character anywhere in a string sends the whole string back to the linear-decode path, so use the new cursor (below) for mixed-content scanning.
A native string cursor for scanning mixed text
(string->cursor str [start [end]]) returns a cursor you can walk with cursor-peek, cursor-next!, cursor-eof?, and cursor-pos. Each step is amortized O(1), decoding one UTF-8 character forward, so a lexer over a buffer full of non-ASCII text stays linear without copying substrings. A scan of an 80,000-character mixed string fell from 35.1 seconds to 0.32 seconds. string-for-each and string-map now iterate through the cursor, so they are linear too (a 40,000-character mixed string-for-each went from 19.9 seconds to 0.58 seconds).
Bulk string, vector, and bytevector operations are native
string-copy, string-copy!, string-fill!, string-join, vector-copy, bytevector-copy, bytevector-copy!, and the list/vector converters are now single-pass native code. string-join in particular was quadratic (joining 16,000 parts took 8.5 seconds); it is now a native single-pass builder. string->vector on a one-million-character ASCII string dropped from 2.36 seconds to 0.13 seconds.
Two correctness improvements come with the sweep: string-copy! and bytevector-copy! now have memmove overlap semantics, so a self-copy that shifts a region to the right no longer corrupts the data (the old forward loops did). And string-copy! validates the whole destination range before it writes, so a bad range fails cleanly instead of writing part of the data and then erroring mid-loop.
Native vector mutation
vector-copy!, vector-fill!, list->vector, and vector->list are now native, continuing the bulk-operation work from the string sweep on the vector side.
Async continuations survive suspension in nested native frames
A suspending operation reached through a frame that a native procedure had called could lose its continuation, so the work after the suspension silently never ran. The VM now preserves the continuation across that boundary, so async code behaves the same whether or not a native frame sits in the call stack.
Scripts can start with a shebang line
A leading #! line is now skipped when Sigil reads a whole file (file evaluation, load, and eval-string), so you can make a Sigil script executable with #!/usr/bin/env sigil at the top. A #! anywhere other than the first line is still a syntax error, and source locations stay correct (the content reports its real line numbers).