Lyric
A statically typed application language with a bytecode VM — standalone, and embeddable in a host with a capability-based sandbox.
What changed in v3.4.1 — 2026-08-24
A ground-up bug sweep through the pipeline. Every fix carries a failing test written first, and none changes the behaviour of a correct, documented program — what they change is that malformed input, whether hand-written source or crafted bytecode, is now refused with a message instead of crashing, corrupting, or slipping through.
Fixed
A Unicode escape past the last scalar no longer crashes the compiler.
"\u{80000000}"— eight hex digits with the high bit set — wrapped to a negative number and took the compiler down with an unhandled exception instead of reportingLYR-LEX0007. A surrogate escape (\u{D800}) passed the lexer too and was then dropped from the string. Both are refused now.An oversized array is a panic, not a process crash.
[0] * nand array concatenation with a result too large to fit an array reached the allocator as an overflowed length and aborted the whole runtime — the escape aCapability.Nonesandbox exists to prevent. They panic (LYR-VM0006) now, on the interpreter and the JIT alike.A module that under-declares its capabilities is refused. Binding trusted the capabilities a module declared: one that called
std.io.filewhile declaring nothing was handed the native and ran it, even underCapability.None. A gated native can no longer be bound without its capability declared (LYR-CAP0001), so a host loading foreign bytes is protected as the capability model promises.The bytecode loader closes four gaps in what it rejects. A function whose code ends without a terminator, a
retin a valued function or aretvalin a void one, athrownaming a type outside the table, an entry point whose signature is neither empty nor a singlestring[], a reference index out of range inside a function-type signature, and an attribute value naming an enum's payload variant or an out-of-rangechar— each passed validation before and could crash or misexecute; each is refused now.lyrvm verifycatches them without running a byte.An array type carries no length.
int[3]parsed into a type nothing could produce; the parser reportsLYR-PAR0043with the advice to build the array with[x] * n.A pattern literal must be a literal.
-yand3..yparsed with an identifier where the grammar has a literal — and the lowering evaluated it, a pattern quietly matching a runtime value. Both are refused (LYR-PAR0033).A selective import names at least one item.
import a { };was accepted as an import of nothing; it reports now.An f-string format spec runs to its matching brace.
{x:a{b}c}stopped at the first}, leaking the tail into the surrounding text; nested braces, parentheses and brackets are tracked as the grammar has always said.A supplementary char crosses the host boundary without truncation. A Lyric
charis a full Unicode scalar; a .NETcharis UTF-16. The embedding boundary silently masked an astral code point to sixteen bits. It now crosses as its code-point integer — a host asking forchargets a checked error, one asking forintgets the whole code point.
Docs
- The guide called
intan alias forint64; both are distinct types of the same width, as the specification says. A literal adapts to either, a variable crosses withas.
Added
A chain can be started from an array, a range, or a table with holes in it.
std.itergains four entrances:over(xs).map(f) // an array range(1, 4) // what '1..4' walks in a loop rangeInclusive(1, 4) // what '1..=4' walks compact(slots).map(f) // the non-null values of a '(?T)[]'The adapters have been methods since 2.17, but the three built-in forms could not start a pipeline: an array and a string have no declaration to hang an
Iterableconformance on, and a range is not a value at all —a..bis a loop head, so(a..b).iter()is not something that could exist. Naming the machinery by hand was the alternative, and a class literal with two fields reads worse than the loop it replaces.rangeInclusiveis its own function rather thanrange(low, high + 1), for the reason the two adapters behind it are separate: at the type's maximum that+ 1wraps and the chain yields nothing.compacttakes an ARRAY, not an iterator, and that is the whole design. AnIterator<?T>cannot exist —next()answers?Tand spendsnullon "the end", so an optional element would need??T, and?does not nest. Reading the slots directly is what makes the distinction available. It is lazy like every other adapter.