Changelog
This file starts at v1.0.0. Before it there was no compatibility promise to describe — neither for
the language nor for the .lyrbc format — and a changelog written under those conditions records
churn rather than change. The pre-1.0 releases carry their notes in their annotated tags.
Versions follow vMAJOR.MINOR.PATCH, as described in README. Each entry
lists what changed for someone using the toolchain: the language, the standard library, the
bytecode format, the command line and the embedding API. Compiler internals are in git log.
v1.6.0 — 2026-08-18
Attributes. A program can say things about itself that a tool outside it can read — which functions a host should call, what a script-declared type looks like, what a module is. The bytecode format goes 3.1 → 3.2; both new sections are skippable, so a 1.5.0 runtime loads a 1.6.0 module and runs it unchanged.
Added
Attributes, on a function, a type and the module header. An attribute is a struct type; where it may sit is the marker interface it declares —
OnModule,OnTypeorOnFunction, all new instd.core:import std.core { OnType, OnFunction }; pub struct Component :: [OnType] { } pub struct System :: [OnFunction] { order: int = 0 } @Component pub struct Health { value: int, max: int } @System { order = 10 } pub fn damageTick(dt: float): void { }Conformance decides, not the name — no struct becomes an attribute by accident, the same nominal rule the operators follow. The arguments are the struct initializer restricted to literals; a field the use does not write carries the field's literal default, and a field with neither is an error at the use site, not a hole in the metadata.
An attribute describes; it does nothing. No attribute in this release is read by the compiler, and a runtime that ignores them runs the program unchanged.
Bytecode format 3.2. Section 11 holds the rows — target, attribute type, one value per field in field order, always complete. Section 12 holds field names, ONLY for types a row references: everywhere else the rule stands that field names are not in the bytecode, but a host reading
@Component struct Healthneedsvalueandmax, or it has learned a shape it cannot name.An attributed function survives dead-code elimination: the row is a promise that the index is valid, and the host is a caller the reachability analysis cannot see — the same standing as the entry point.
The embedding API reads the rows.
ScriptModule.Attributesanswers beforeInstantiate— for foreign bytes, the module row is how a host decides whether to load at all. A hit is a call handle:instance.CallVoid(use, …)calls by the index the row carries, so a typo in a script is a compile error instead of a function nobody finds.FieldsOfyields the named, typed shape of an attributed type.The tools show them.
lyric disasmprints each row with its field names,lyrvm infocounts them, and hovering@Systemin an editor answers with the struct.
Fixed
- A duplicate field in a struct initializer crashed the compiler.
P { x = 1, x = 2 }passed the type checker and died in the lowering with an internal exception instead of an error message. It is a diagnostic now (LYR-SEM0070), reported at each repeated field, in struct and class initializers and in enum struct-variant initializers alike. Found while building the attribute checks, which validate their arguments the same way.
Changed
@nameat declaration position is no longer "attributes arrive later". It parses; what the name resolves to is the sema's question, so@testis nowunknown type 'test'instead ofLYR-PAR0038. That code stays on parameters, where attributes remain rejected, with a message that no longer promises the future. The reserved expression form@name(args)leaves the grammar;LYR-SEM0053now says an attribute is not an expression.
Not in this release
- Attributes on parameters, fields and members — top-level declarations and the module header only.
- Attributes the compiler reads (
@Deprecated,@MustUse,@Inline): the moment one attribute changes compilation, the attribute set becomes part of the language contract and the stability promise. That is a separate decision, deliberately not smuggled in here. - Runtime application, Python-decorator style: there is no mechanism by which an attribute wraps or replaces its target.
- Qualified attribute names: names are the bytecode's type names and therefore unqualified. An SDK owns its attribute names the way it owns its native names.
- Completion after
@.
v1.5.0 — 2026-08-18
Operators on your types. Everything resolves through the one mechanism this language has for
polymorphism — the interface a type declares — so there is no operator declaration syntax, no new
opcode, and the .lyrbc format stays 3.1.
Added
==and!=on every type conforming toEquatable<T>. The operator is the method:a == bcallsa.equals(b), anda != bnegates it.struct Point :: [Equatable<Point>] { x: int, fn equals(other: Point): bool { return this.x == other.x; } } let same = a == b;Conformance is required, not the method alone: a type with an
equalsnobody declared asEquatablestays rejected, so no method becomes an operator by accident of its name.<,<=,>and>=on every type conforming toOrdered<T>— onecomparemethod, negative/zero/positive, and all four operators read its sign.string < stringworks, through the conformance the standard library has carried since v1.0; its rejection had promised exactly this change.+,-,*and/on types conforming toAdd<T>,Sub<T>,Mul<T>andDiv<T>— four newstd.coreinterfaces, one method each, homogeneous:T op TgivesT. The built-in numerics conform,stringtoAddalone, so a generic function constrained onAdd<T>serves anint, astringand your vector type in one program.asbeyond the numerics converts throughInto<T>.x as Tisx.into()where the operand's type declares the conformance. Explicit only, one target per type, total conversions only — a conversion that can fail belongs in a named function returning an optional. The numeric casts keep their opcodes and are not overridable.s *= 3andxs *= 2work. The repetition overloads of*had no compound form — an accident of how compounds were checked, recorded as a limit. The compound check rework below delivered them.
Fixed
- A compound assignment never checked its operator.
p += pon a struct passed the compiler and produced an integer addition of two references at runtime — thes += "x"class of bug fixed in v1.1.0, one type over.s &= sandf <<= fpassed the same way. A compound is now typed as the binary it carries: whatevera = a + bsays,a += bsays too.
Not in this release
- Heterogeneous operands (
Vec2 * float): needs a two-parameter interface and a rule for multiple conformances to one generic interface. - Compound assignment through the operator interfaces (
v += won aVec2): the compound lowering evaluates the target's address once and cannot yet route through a call. The diagnostic says to writev = v + w. %on user types, and unary-: no interface exists for either, deliberately.- A conversion out of a builtin (
extend int :: [Into<Cents>]): the orphan rule stops it, and the rule does not look into type arguments. A named function takes its place. - Method overloading, considered and rejected: constraints plus generics are this language's overloading, and the standard library says so itself.
v1.4.0 — 2026-08-17
Completion, and a standard library that says what it does. The language, the command line, the
embedding API and the .lyrbc format are untouched; the format stays 3.1.
Added
Completion. After a
.the members of what stands before it; anywhere else the names in scope.let p = Point { x = 1 }; p. // x, y, and every method, extension and interface default the type hasThe member list is the one the compiler would accept, not an approximation of it: extension methods and interface default methods are in it, which matters because every string method of this standard library is an extension — a list without them would be empty on a
string.In scope: locals, parameters, type parameters, what the module declares and imports, and the builtins. Inner names shadow outer ones, a binding is not offered inside its own initializer, and a loop variable is not offered in the loop head.
Each item carries its kind and, when the declaration has one, the
///block above it.It works while the file does not parse, which is the state it is asked in. The trigger character is
.; everything else is the editor asking on its own.The standard library documents itself where it did not.
std.io.console,std.coreandstd.optionheld 33 public declarations and no documentation at all, so hoveringprintlnshowed a signature and nothing else. All three are written now, interface members included.
Fixed
A struct initializer is a reference to its type. Asking for the definition of
Pointinlet p = Point { x = 1 };used to answer nothing, and find-references did not list it. Both do now, and hovering it reports the type.v1.3.0 listed this under Not in this release because recording it made the type checker read
Pair<int> { a = 6 }.aas a static member access. The receiver question is answered from the expression's type now rather than from that table, so the two no longer collide.
Not in this release
- Completion does not offer keywords.
if,returnand the rest are not symbols; an editor gets them from the grammar it highlights with. - Completion after
importdoes not offer module paths. That is a different source — the file system — and not the scope. - A field reference still marks the whole member access: asking for references to
xmarksp.x. Use sites carry no span for their name alone. - References and completion stop at the compilation. The server compiles the file you are in, so another file of your project that imports it is not searched.
v1.3.1 — 2026-08-17
Fixed
Eight diagnostics pointed at a document that does not exist. Five named
Sprache.md, which has beendocs/Grammar.mdfor some time, and the sections they cited were wrong as well — §10 and §11 of a document that has seven. Following either reference led nowhere twice:attributes are not part of v1 (Sprache.md §10); '@test' and 'lyric test' arrive after v1.0attributes are not part of v1; '@test' and 'lyric test' arrive laterRather than repair the citations, they are gone. A diagnostic names what is wrong, not where to read about it — a citation ages in two ways at once, and both had already happened here. Where a reference carried information (
§11 allows none or one 'string[]'), the message now says it outright.The affected codes are
LYR-PAR0038,LYR-PAR0039,LYR-SEM0053, the lowering's main check, the bytecode reader's global check and two entry-point findings of the IR verifier. No code changed and no behaviour changed — only the wording, so a program that compiled still compiles and one that did not still fails, with the same code on the same span.
v1.3.0 — 2026-08-17
Everything in this release is the language server. The language, the standard library, the command
line and the .lyrbc format are untouched; the format stays 3.1.
Added
Hover shows the documentation you wrote. A
///block above a declaration appears under its signature, for declarations in the file you are editing and in every module it reads:fn cpuCount() -> int How many cores the machine has, for programs that split their work. The VM itself is single-threaded.The text goes through unchanged — there is no doc-comment vocabulary in the grammar, so nothing is interpreted, and nothing is composed from the signature. A declaration without a block is shown exactly as before.
An editor can show the outline of a file (
textDocument/documentSymbol). Types carry their fields, methods, variants and static constants as children; imports, parameters and locals are left out, because an outline says what a file offers.It reads the syntax and resolves nothing, which is why a file with type errors still has an outline — the moment you most want one.
Only the nested form is produced. An editor that does not announce
hierarchicalDocumentSymbolSupportgets no outline rather than the deprecated flat one.Find all references (
textDocument/references), with or without the declaration itself. The answer covers the program reachable from the file you are in, so a call into the standard library is found in the module that declares it.
Changed
- Go to definition selects the NAME of a declaration, not the start of it. Previously the cursor
landed on the first character of the whole declaration — on
forfor a loop variable, oncatchfor a catch binding. An editor that announceslinkSupportnow also receives the full extent of the declaration beside the name, so a peek window shows the declaration and puts the cursor on its name.
Fixed
- Go to definition on a struct initializer no longer jumps somewhere else. In
let p = Point { x = 1 };, asking aboutPointused to land onp— the enclosing binding, which is not what the cursor was on. It now answers with nothing. What it cannot yet do is answer withPoint; see below.
Not in this release
- A struct initializer is not a reference to its type.
Point { … }is bound to no symbol, so neither find-references nor go-to-definition sees it. An annotation (let p: Point) is found. - A field reference marks the whole member access: asking for references to
xmarksp.x, not thexin it. Use sites carry no span for their name alone. - References stop at the compilation. The server compiles the file you are in; another file of your project that imports it is not part of that compile, and its uses are therefore not listed.
- No completion. It is the first question asked at a position where the text does not parse, which is a compiler topic rather than another editor feature.
v1.2.0 — 2026-08-17
Changed
Only a field needs the
,that separates members of a struct or class. A bodiless method used to need a semicolon and a comma in a row:class Builder { fn addExecutable(entry: string, output: string): Artifact;, // no longer fn addTest(entry: string): Artifact; }The rule is strictly more permissive, so every file that was valid stays valid — the comma is still accepted where it is now optional. Two fields still need one, or
a: int b: intwould read as a single field of a type nobody wrote.
Added
lyric newwrites a project that builds.lyric new myapp # lyric.json, build.lyr, .gitignore, src/main.lyr lyric new mylib --lib # lyric.json, src/mylib.lyr — nothing to buildTwo shapes and two flags rather than a template system: with two variants a discovery mechanism is more machinery than content. The name becomes a module name, so it has to be one, and an existing directory that holds something is refused rather than merged into.
The templates are embedded in the binary, so nothing can go missing beside it — and they are real
.lyrfiles in the repository, which the test suite compiles.__name__is a valid Lyric identifier, so a template is compilable Lyric rather than text with holes in it.It is the one command the driver runs itself: it writes files and compiles nothing.
A project may be built by a script,
build.lyr.lyric buildwithout a file argument runs it and compiles what it declares:import std.build { addExecutable }; pub fn build() { let app = addExecutable("src/main.lyr", "out/app.lyrbc"); app.sourceMap(false); addExecutable("tools/mktex.lyr", "out/mktex.lyrbc"); }Every artifact is compiled whole, from its entry file; there is no link step and nothing is shared between two of them but the source on disk.
lyric buildwith a file still means "compile this file" and is unchanged.Nothing is compiled while the script runs — it collects, and the compiles happen once
buildhas returned. That is why an option set on the following line still applies, and why a source file the script generates is finished before anything reads it.It is a Lyric program with the whole standard library and every capability, so it may write files and start processes. Relative paths in it resolve against the directory holding
build.lyr, not against the directory the build was started from.lyric buildin a repository you did not write runs code you did not write, asmakeandcmakedo.New binary
lyrbuild, the second afterlyrreplthat holds both the front end and the runtime: a build script has to run, and what it collects has to be compiled afterwards.A project may say where its modules are, in a
lyric.json.{ // where our own modules live "sourceRoot": "src", "nativeRoots": { "engine": "sdk" }, }sourceRootreplaces "the directory of the entry file" as the module root, andnativeRootsmaps a module path segment to a directory whose modules may declare functions without a body. The file is searched for upwards from the file being compiled, and comments and trailing commas are allowed in it.This closes the gap v1.1.0 shipped with:
lyric checkandlyric buildnow see the native roots a host declares, so a script written against an SDK no longer compiles in the host and fails on the command line.Both keys are optional and without the file nothing changes — that is what makes it an addition rather than a new requirement. A key nobody knows is a warning rather than an error, so a file written for a later version still loads.
It is read and never executed, which is what lets an editor learn a project's layout without running anything from it.
The language server follows a program across its files. Editing a module now refreshes the diagnostics of every open file that imports it, and a dependency is read from the editor's buffer rather than from its last save. Both halves are needed: an overlay nobody re-reads shows nothing, and a cascade over stale text refreshes to the same answer.
What a file depends on is taken from the compilation itself, not from the imports in its text — the resolver already followed them, transitively and through the project's roots, and a second answer to that question would be the one that is wrong.
The cascade goes one level. Two modules may import each other, which is a diagnostic rather than a crash, so a transitive one would not terminate.
CompilerOptions.SourceOverlayis the seam, and it is not editor-specific: it says "compile as if these files held this text", which a host embedding the compiler can use for the same reason.The language server reads
lyric.json. An import of a host SDK no longer shows as an unknown module in an editor while the same script runs correctly in the host — the second half of what v1.1.0 listed as not in it.A broken project file does not stop the analysis. The editor keeps getting diagnostics, resolved by the plain rules, and the reason goes to the client's log; publishing nothing would leave an earlier state on screen with no hint that anything happened. The message names the project file rather than appearing as an error inside the file being edited, and it is said once per change rather than once per keystroke.
Still not there: editing a module does not refresh the diagnostics of the file that imports it. The server analyses one buffer at a time.
v1.1.0 — 2026-08-15
Bytecode format 3.1. A minor of the format may only add skippable sections, so a 1.0 runtime reads a module built by this release and a 1.1 runtime reads one built by 1.0 — with one caveat below.
Added
A host may ship its API as
.lyrfiles instead of generating it.HostOptions.NativeRootsnames directories whose modules may declare functions without a body, keyed by the module path segment they own, andLangVm.RegisterNativesupplies the implementations under the same qualified names. Until now every host function went throughRegisterFunction, which derives the declaration from the delegate — right for a handful, and for an SDK it means the same signature lives in the C# call and in whatever documents the API.Whether a module may declare a native follows the ROOT it came from, never its content, so naming a file well enough is not a way into the host. A module in such a root may hold ordinary Lyric code beside its declarations.
A program may consist of several files. A module path becomes a file path under the directory of the entry file:
import shapes.circlereadsshapes/circle.lyrbeside it. Until now only the standard library could be imported, so every program was one file.Three rules come with it. A file must agree with the path it was loaded from, or the header is an error — previously such a file registered under the name its header claimed and the import that pulled it in reported cannot find module about a file it had just read.
stdresolves against the standard library alone, so nothing beside your program can take its place. And only standard library modules declare functions without a body; in your own modules a missing body is a compiler error rather than a failure at load time.Everything still compiles into one
.lyrbc. There is no separate compilation step per file.A panic names the line it happened on, not just the function:
panic [LYR-VM0002]: division by zero in main.divide (app.lyr:3) in main.main (app.lyr:8)The innermost frame points at the instruction that failed, every frame below it at the call it was waiting on.
The SourceMap section of the bytecode format now has a payload. It was reserved and named in 3.0 and never written. It maps a byte offset in a function's code to a file and a line, one row per position change.
lyrc build --no-source-mapleaves the section out. Without it the file is byte for byte what the same build produced before the section existed, so stripping costs nothing else. The section is written by default: the moment a line number is wanted is the moment nobody planned for it.Paths are stored relative to the entry file's directory, and a file outside it — the standard library sits beside the toolchain — keeps its bare name. Nothing absolute reaches the file, so a module does not carry the directory layout of the machine that built it.
Fixed
s += "x"on a string silently produced the empty string.+on astringis a call tostd.string.concatand on an array anarrcatinstruction, but the compound forms emitted a bareaddwith the operand type next to it. Nothing rejected that in a release build, and the runtime read the two strings as integers, so the variable ended up empty and the program kept running:var line = ""; line += "0F "; // line was "" afterwards, not "0F "Affected were a local, a captured variable and a coroutine local. On an array the same instruction produced a value with no reference, and the next access to it ended the process with a host exception instead of a panic. A field (
obj.s += "x") and an array element (xs[0] += "x") were reported asLYR-IR0001rather than miscompiled, and now work as well.s = s + "x"was correct throughout and is unchanged. Existing.lyrbcfiles are unaffected: the format and its specification were right, the compiler was not.s *= 3andxs *= 2stay rejected — a separate rule in the type checker demands that the right operand be assignable to the left, which does not hold for repetition.s = s * 3works.A runtime accepted an arithmetic opcode with a type it cannot compute on. §5 of the format says
addthroughremrequire a numeric type; the reader checked indices only and never the type tag, so a module carryingadd stringpassedlyrvm verifyand ran. That is why the bug above could reach an output at all — the IR verifier that does catch it runs in debug builds only. Such a module is now rejected at load time withLYR-BC0005.A reader rejected a section id it did not know, with
LYR-BC0003, instead of skipping it. That is the mechanism the format's forward compatibility rests on, and it had never run, because nothing had ever written an unknown section.This is the caveat above: a 1.0.1 runtime cannot read a module carrying a SourceMap, even though the format says it must. Building with
--no-source-mapproduces a module those runtimes accept.
Not in this release
- The command line does not know native roots.
HostOptions.NativeRootsreaches the compiler through the embedding API alone, solyric checkand the language server report an unknown module for an import a host resolves at runtime. Scripts written against an SDK run correctly and look wrong in an editor. - The language server does not know multi-file programs. It compiles the buffer it was given, so
editing
util.lyrdoes not refresh the diagnostics of theapp.lyrthat imports it. Reopening or editing the importing file does.
Both need a place where a project says what it consists of, and putting that on the command line would make a third place where the layout is written down.
v1.0.1 — 2026-08-14
Fixed
A module with both a module-level
letand atry/catchcompiled to a file that would not load. The compiler wrote the Globals section (id 10) ahead of the Handlers section (id 9), and section ids must ascend strictly, solyric runandlyrvm verifyrejected the compiler's own output withLYR-BC0005.lyric checkandlyric buildreported success beforehand, which is what made it look like a runtime problem rather than an emitter one.Only a module carrying both sections was affected; either one on its own was written correctly and is unchanged. No
.lyrbcfile that used to be valid changes — the format and its specification were already right and the writer was not, so the bytecode format stays 3.0.
v1.0.0 — 2026-08-14
The first release with a compatibility promise. Everything below describes the state it ships, not a change against v0.9.0: there is no earlier entry to compare against.
From here on the .lyrbc format and the language carry the promise the versioning describes: a
minor may add, a major may break.
Language
The whole grammar in docs/Grammar.md compiles and runs: functions, structs and
classes, enums with match, interfaces with default methods and :: conformance, generics with
constraints, optionals, exceptions with throws and defer, closures, coroutines, modules, and
extend blocks on own and primitive types.
Fixed against v0.9.0, each of them a case that used to be refused or to fail late:
- An argument position now carries an expected type, so
f(Opt.Some(5))names its instance instead of requiringf(Opt<int>.Some(5)). - A generic struct initializer takes its instance from the surrounding type:
let p: P<int> = P { v = 1 }. Written type arguments still win, and there is still no inference from the field values. - A
typealias works in every position — as a return type and a field type too, not only as a parameter type and a local annotation. static fnis allowed in an enum and anextendbody. An interface member stays non-static: it is reached through a vtable slot, which takes a receiver.- A cyclic type alias (
type A = B; type B = A;) is a diagnostic instead of ending the compiler process.
Toolchain
lyric(driver),lyrc(compiler),lyrvm(runtime),lyrrepl(interactive prompt), andlyrembed.dllfor a C# host.lyric checkanswers the same question aslyric build. It used to stop after type checking and reportokfor programs the backend could not express.- Releases ship a self-contained archive per platform (
win-x64,linux-x64,osx-arm64) that runs without a .NET install.
Documentation
- A static documentation site generated by
tools/DocGen: the guide, both specifications, and a standard library reference generated from the.lyrsignatures. One frozen directory per version.
Not in this release
- No interface inheritance; require both interfaces side by side.
- No operator overloading, so
==and<on user types stay ordinary methods. - No attributes (
@testand the rest). - The source map section of the bytecode format is reserved but not written, so a panic names the function rather than the line.