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.7.0 — 2026-08-25
M35 — the reason. The gap std.io.file has carried since 2.14 — "a missing file and a
permission denied look the same" — closes, and it closes as a LIBRARY DOCTRINE rather than a
one-module patch (spec §9.0, lyric-spec#22): a value answers whether, a throw answers why.
Everything is additive — no silent form changes, no format change (bytecode stays 3.6) — and
every twin derives from the same implementation as its silent sibling, so the two can never
drift.
Added
std.io.error—IoErrorandIoErrorKind, the reason an I/O operation failed: which kind (NotFound,PermissionDenied,InvalidPath,Other— a matchable enum behind a carrier class, so the list can grow without breaking anyone'smatch), on which path, and what the platform said. Capability-free: knowing the type costs nothing, producing one takes the capability of the module whose operation failed.std.io.file— anOrThrowtwin for every silent form.textOrThrow,bytesOrThrow,linesOrThrow,sizeOrThrow,entriesOrThrowanswer the value or throw the reason; the ten operation twins (writeTextOrThrow…removeDirOrThrow) arevoidwhere the silent form answersbool, throwing where it answersfalse. The runtime classifies the last failed operation; the twins read the classification immediately — the VM is single-threaded, so nothing runs in between. A module that binds the new natives needs a 3.7 runtime (theparseFloatforward path); one that does not loads everywhere it always did.entries(path): ?string[]completes the 2.14 linelistDirmissed:nullwhen the directory cannot be read, an empty array when it is empty.listDir— which answers an empty array to both, with "askisDirectoryfirst" as its documented race — carries@Deprecated { until = "4.0" }.std.json.parseOrThrow— aJsonErrornaming the line, the column (1-based, code points), the offset and what the parser expected there. One parser answers both forms: the leaf that refuses records the FIRST place,parseignores the note,parseOrThrowreads it.std.encoding.hexDecodeOrThrow/base64DecodeOrThrow— anEncodingErrornaming the character offset and what a canonical string would have had there.std.string.utf8DecodeOrThrow— aUtf8Errornaming the byte that broke the sequence.
Deliberately unchanged
parseInt and parseFloat stay ?T alone — the reason of a five-character input is the input.
std.os stays silent throughout: an unset variable, an unanswered hostName — null IS the
whole truth there. Map.get, List and the option helpers keep absence as a value, and panics
keep programming errors.
Fixed
- An import mentioned only in a PATTERN warned as unused (
LYR-SEM0072).match (e.kind)over an imported enum — the exact shape the new convention recommends — left the import "unused", which under--deny-warningsfails the build. The 3.3.0 type-path fix, one node kind further: a pattern's path is strings, and its qualifier is recorded now.
Documentation
- Guide 10 gains "Whether, or why" — both shapes side by side,
??for the fallback andmatch (e.kind)for the reason. Guide 13 listsstd.io.error, the twin convention, and thelistDirdeprecation.