Lyric v2.12.0

Lyric

A statically typed application language with a bytecode VM — standalone, and embeddable in a host with a capability-based sandbox.

What changed in v2.12.0 — 2026-08-22

Bytecode format 3.6. The interpreter runs the same programs in a third of the instructions, and the release is about why that is the number that mattered.

The measurement it started from

An instruction on this VM costs ~6 ns — and costs it regardless of what it does. A br, an add f64 and an and i64 lie within twenty percent of each other. That is the normal price of a switch-dispatch stack machine, not a slow interpreter: the dispatch is the whole bill. So nothing that makes an instruction cheaper moves a program's time, and the only lever is executing fewer.

The loop test of every while in the language was four instructions, of which three existed to move a value onto the operand stack and off it again.

Added

Measured

Same harness, same machine, same session; the baseline is a build with selection switched off and the old nextInt. Two iteration counts differenced, so nothing that happens once is in the figure, and the instruction counts are read from an ExecutionBudget rather than counted by hand.

case instr ns/iter instr ns/iter
2.11 2.11 2.12 2.12
a counting loop 9 44.3 3 18.2 2.4×
an integer accumulator 13 59.2 4 27.6 2.1×
a float accumulator 13 68.7 4 24.8 2.8×
a masked accumulator 15 88.7 9 61.2 1.4×
a native call 14 82.4 8 56.9 1.4×
Random.nextFloat() 70 395.4 42 199.2 2.0×
an array read 19 91.6 13 65.8 1.4×

The time fell WITH the count rather than beside it, which is the claim the release rests on. What rose is the average price per instruction (≈4.9 → ≈6.3 ns), and that is the healthy direction: the instructions the fusion removed were the cheapest ones, the moves.

The rows that gain least say where the remaining work is: a nested expression keeps its intermediate on the stack, and neither fused form can reach a value that is not in a slot.

Format

3.6 against 3.5: four new opcodes. A producer may emit the unfused sequences instead, and one that never emits a fused form writes a module any 3.5 runtime accepts; a module that uses one needs a 3.6 runtime, named at load time.

The §Versioning rule was corrected in the same round. It read "a minor version may only add skippable sections", which 3.4 was not — the note beside that version said as much, so the document had contradicted itself since the day it shipped. It now states the compatibility the format has actually delivered: per MODULE, not per version.

The full changelog →