Lyric
A statically typed application language with a bytecode VM — standalone, and embeddable in a host with a capability-based sandbox.
What changed in v4.5.0 — 2026-09-22
The project system. A compile is one of two named profiles and the default is debug; a
build script names what it builds; a project builds, runs, packs, checks and tests without
anybody naming a file; and lyric.json says what the project is called, what it depends on and
which toolchain it needs.
Added
Profiles. A compile is one of two named shapes.
debugruns no IR optimizations and keeps the source map and the debug info;releaseruns them and drops the debug info, keeping the source map.lyrc,lyrbuildandlyrtesttake--profile <name>,--releaseand--debug, andlyric run,lyric buildandlyric packhand them through. Every field of a profile stays overridable, and a field flag wins:--optimize/--no-optimize,--source-map/--no-source-map,--debug-info/--no-debug-info.LYRIC_PROFILEnames the default for a whole process, theLYRIC_JITshape; a flag beats it.HostOptions.Profilein the embedding API. A host that ships setsProfile.ReleasebesideCompile = true; left unset, a VM compiles the process default.Four diagnostic switches on
lyrc, part of no profile:--no-inline,--no-scalar-replacement,--no-devirtualizeand--no-fusion. Each takes one pass or the fused instruction forms out of an optimized build, for bisecting a finding down to the pass.lyrvm run --jit, the engineHostOptions.Compileopts into, from the command line.lyric run app.lyr --jithands it through to the runtime.lyric.jsonlearns three keys.dependenciesmaps a module path segment to the directory of another project; that project's own source root is where the segment's modules come from, itsnativeRootsanddependenciescome along, and the table is flat: one segment, one directory, for the whole program — two dependencies disagreeing are refused with both named, and your own entry pins the segment for everybody under you.nameis the project's name, a module name.toolchainis the oldest toolchain that may build the project; an older one refuses with both numbers (LYR-CLI0018) instead of compiling against a language the project was not written for. All three are optional; a file without them means what it meant.HostOptions.DependencyRoots, the same table for a host compiling a project's scripts.std.buildnames what it builds.executable(name, entry)declares a program that lands asout/<profile>/<name>.lyrbc;library(name, root)checks every module under a root and writes nothing;packed(app)declares the packed form of an executable, landing asout/<profile>/<name>with the platform's suffix. An artifact is an ordinary object: its fields are the profile's four (optimize,sourceMap,debugInfo,denyWarnings), set one by one or all at once withuse(Profile.release()), andoutputnames where it lands outright.Profile.selected(),Profile.debug()andProfile.release()are the toolchain's table, asked rather than copied.A build script takes options.
option(name, help)answers-D name=value(nullwhen not given),flag(name, help)answers-D name;lyric build --helpin a project lists them. A-Dthe script never asked about is refused with the ones it did (LYR-CLI0003).--only <name>builds one artifact; a name nobody declared is refused with the ones that were (LYR-CLI0019). Apub fn after()in the script runs once every artifact was written, and not otherwise.The verbs take no file in a project.
lyric runbuilds the default artifact — the first executable the script declares — and runs it;lyric run mktexruns that one;lyric packbuilds it in the release profile and packs it. An argument that is on disk or carries.lyr/.lyrbcis a file and goes to the compiler as before, and anything else is an artifact's name.lyric checkchecks a whole project. Without a file, or with a directory, the source root is read as one compilation and the test root as a second one that imports it — a test calling a function that was renamed is a question no per-file check can answer.lyrc checktakes the same directory.lyric test --filter <text>runs the tests whosemodule.functioncontains the text. A filter that matches nothing is an error, because a green run of nothing is worse.A project builds without a script.
lyric buildin a directory without abuild.lyrcompilesmain.lyrunder the source root intoout/<profile>/<name>.lyrbc, named after the project (lyric.json'sname, or the directory); a source root without amain.lyris a library and is checked as a whole. Neither isLYR-CLI0011, naming both ways.
Changed
lyric newwrites atests/directory with one test in it, and the app template gains a module besidemain.lyrfor that test to import: a scaffold whose shape is the shape a project keeps.lyric testruns it straight away.Build artifacts land under
out/<profile>/.lyric new's app builds toout/debug/<name>.lyrbc, andlyric build --releasetoout/release/, so the two shapes never overwrite each other. A script that wants the old layout setsoutputon the artifact.addExecutable(entry, output)is deprecated (LYR-SEM0076, kept until 5.0). It still builds, named after the output's stem; the new spelling isexecutable(name, entry)withoutputset when the derivation is not wanted. The old arguments in the new call are refused at the call with a message saying which way round they go. Its one setter,app.sourceMap(false), is the field now —app.sourceMap = false— and a script calling the method gets a compile error at the call: a field and a method cannot share the name, and the field is the one the other three options have.lyric build,lyric run,lyric check,lyric test, the REPL and the embedding API compile the debug profile by default. Before, every compile ran the optimizations and kept the names, the shape--release --debug-infoproduces now.lyric packcompiles the release profile by default. The measurement behind the change, on this interpreter (tools/Bench, Release configuration, both shapes in one session):shape release debug struct built and added per iteration ( construct_call)55 ns 141 ns for-inover a range73 ns 117 ns for-inover an array154 ns 140 ns Set.iter()loop409 ns 351 ns one call per iteration 64 ns 65 ns the standard library's test suite 3.96 s 3.45 s A backtrace from a default build names every frame, small callees included; the release profile still splices them away.
Every tool refuses an option it does not know (
LYR-CLI0003, exit code 2).lyrcandlyrvmused to overlook one in silence, solyric run app.lyr --grant nonesent--grantto the compiler, which ignored it, and the program ran with every capability.
Fixed
The standard library's own tests are checked for warnings again. The repository holds every Lyric file it tracks to complete diagnostic silence, and the sixteen files under
stdlib-tests/were exempt by accident for as long as the rule has existed — the check that routes a standard library module through an importing probe matched them on their path's prefix. Three findings fell out of it at once, all cleaned: two unused imports and an unused loop variable.lyric testcould not findlyrtestin a source build: the test runner was the one tool the driver's output directory did not get a copy of. The release archive always carried it, so the failure was a developer's, which is why nothing caught it.