Lyric v1.6.0

Lyric .lyrbc Bytecode Format 3.2

This document is normative. The C# serializer implements it; it does not define it. A disassembler or a second runtime can be written from this document alone.

Before Lyric v1.0 the format may change incompatibly with a major version bump and without a migration path. A stability promise begins at v1.0.

Format version 3.2 covers: scalars, locals, module-internal and native calls, structured control flow, classes, arrays, optionals, enums, interfaces with vtable dispatch, structs with value semantics, exceptions, global constants, closures, host objects, source positions, and attributes.

3.2 against 3.1: two new sections — Attributes (id 11) and Names (id 12). Both are skippable: no other section refers to either, and a runtime that ignores them runs the program unchanged, because an attribute describes and does nothing. A 3.1 reader therefore loads a 3.2 module; the only difference is whether a host can read the attributes.

3.1 against 3.0: the SourceMap section (id 6) has a payload. It is skippable, so a 3.0 reader loads a 3.1 module and a 3.1 reader loads a 3.0 module; the only difference is whether a panic can name a line.


1. Encoding

Item Rule
Fixed-width integers little-endian, explicitly — not host byte order
Variable integers LEB128, unsigned, at most 10 groups (64 bits)
Strings byte length as LEB128, then UTF-8 without BOM and without terminator
Floats IEEE-754 bit pattern, little-endian (4 or 8 bytes)
Bool one byte, 0x00 is false, anything else is true

LEB128 (unsigned): seven payload bits per byte, least significant first. Bit 7 (0x80) set means another byte follows. A reader that consumes more than ten bytes must reject the file.

Determinism: the same compiler input produces byte-identical output.


2. File layout

magic            4 bytes   'L' 'Y' 'R' 'B'  (0x4C 0x59 0x52 0x42)
version.major    u16       little-endian
version.minor    u16       little-endian
sections         *         any number, see below

A section:

id               u8
byteLength       uleb128   length of the payload, excluding id and this field
payload          byteLength bytes

A reader must skip a section with an unknown id and must reject a file whose section ids do not strictly ascend.

Versioning: an unknown major version is rejected. An unknown minor version is tolerated; a new minor version may only add skippable sections.

Section ids

Id Name Required Contents
1 Capabilities no uleb128 bitset
2 Strings no constant pool, strings only
3 Types no layouts of composite types
4 Imports no host and native functions
5 Functions no defined functions with their code
6 SourceMap no strippable: byte offset to file and line
7 Start no entry point: uleb128 function index
8 Impls no interface implementations (vtables)
9 Handlers no protected regions per function
10 Globals no global slots and their initializer
11 Attributes no attribute rows: which struct describes which target, with literal values
12 Names no field names, only for types an Attributes row references

A missing section counts as empty.

Types (Id 3)

uleb128 count, then per type:

nameIndex        uleb128   index into the string pool
kind             u8        0 = layout, 1 = enum, 2 = interface, 3 = struct

kind 0 — layout (class, struct, and each individual enum variant):

fieldCount       uleb128
fieldTypes       fieldCount × type (§3), in declaration order

kind 1 — enum: names its variants, each of which is its own layout entry.

variantCount     uleb128
variantTypes     variantCount × uleb128   index into this table

Slot 0 of every variant is its tag: the i64 index of the variant in its enum's variantTypes list, zero-based in declaration order. Payload fields start at slot 1.

kind 3 — struct: the same field layout as kind 0, with value semantics.

fieldCount       uleb128
fieldTypes       fieldCount × type (§3), in declaration order

A struct value is the same slot sequence as a class object at runtime; ldfld and stfld operate on it unchanged. Value semantics live entirely in structcopy (§5).

A struct must not contain itself as a field, directly or indirectly. Recursion through a class, an array or an interface is permitted; those are references.

kind 2 — interface: names its method slots instead of fields.

slotCount        uleb128   at least 1
slotNames        slotCount × string

The index into this list is the slot that callvirt addresses. slotCount must be at least 1. An interface must not appear on the left of an Impls row.

The field index is the position in the field list; field names do not appear in the bytecode. The type name is present for diagnostics and disassembly.

A reader must reject: a field type of void, a type index outside the table, and an enum whose variant is not a layout. A type may name itself as a field type, including forward.

Capabilities (Id 1)

A uleb128 bitset stating what the module requires.

Bit Value Capability Module
0 0x1 fileAccess std.io.file
1 0x2 networkAccess std.io.net
2 0x4 osAccess std.os
3 0x8 hostAccess reserved

0 means the module requires nothing. Submodules inherit: std.os.env requires osAccess.

Enforcement happens at load time. A module that requires more than the runtime grants is rejected (LYR-CAP0001) before any instruction runs.

Strings (Id 2)

count            uleb128
values           count × string

Imports (Id 4)

count            uleb128
entries          count × {
                   name         string
                   paramCount   uleb128
                   paramTypes   paramCount × type tag
                   returnType   type tag
                 }

Host functions are referenced symbolically by name and signature. Binding to implementations happens at load time.

Functions (Id 5)

count            uleb128
entries          count × {
                   nameIndex    uleb128   index into the string pool
                   paramCount   uleb128
                   returnType   type tag
                   slotCount    uleb128
                   slotTypes    slotCount × type tag
                   maxStack     uleb128
                   blockCount   uleb128
                   blockOffsets blockCount × uleb128   byte offset into 'code'
                   codeLength   uleb128
                   code         codeLength bytes
                 }

SourceMap (Id 6)

Byte offset to source position, one table per function.

fileCount        uleb128
files            fileCount × uleb128   index into the string pool
functionCount    uleb128               must equal the count in Functions (Id 5)
functions        functionCount × {
                   rowCount   uleb128
                   rows       rowCount × {
                     offsetDelta  uleb128   bytes since the previous row of this function
                     fileIndex    uleb128   index into the file table above
                     line         uleb128   1-based; 0 means no line is known
                   }
                 }

Strippable. No other section refers to this one, so removing it leaves a valid module. A module without it is valid too; the only consequence is that a panic names a function instead of a line.

The offsets are byte offsets into that function's code, the same coordinate blockOffsets uses. The first row of a function carries its offset outright; every later row carries the difference to the one before. The offsets therefore ascend, and only the first row may have a delta of 0.

A row states the position from its own offset up to the next row's. To resolve an offset, take the last row whose offset is less than or equal to it; before the first row there is no position.

A row is written only where the position changes. A loop body is dozens of instructions across a handful of lines, and a row per instruction would make this the largest section in the file.

A function may carry rowCount 0. Its code then has no position at all, which is what a runtime reports for it.

Why byte offsets and not instruction indices. An instruction index is not a notion of this format: it presupposes that a runtime decodes the code into an array before running it. A runtime that walks the bytes directly would have to count instructions to answer a question the format can answer in its own coordinates.

The row carries no column. A minor version may only add skippable sections, so the shape of this one is fixed until a major: a column cannot be added here later, and adding it beside this section would be a second mechanism for the same thing.

A reader must reject: a functionCount that differs from the Functions section, a file index outside the file table, a string index outside the pool, an offset that does not lie inside that function's code — a row marks where an instruction begins, so offset == codeLength is already outside — and a delta of 0 on any row but the first.

Start (Id 7)

functionIndex    uleb128   into the shared index space: imports first, then functions

The entry point. A runtime calls it without arguments unless its signature takes string[] (§8); its return value is the process exit code, masked with & 0xFF.

Without this section the module is a library and has no entry point.

Impls (Id 8)

The vtables: which function fills which method slot of which interface for which type.

implCount        uleb128
implCount × {
  typeIndex      uleb128   the implementing type (class or enum)
  interfaceIndex uleb128   an interface entry
  methodCount    uleb128   must equal the interface's slotCount
  methods        methodCount × uleb128, each an index into the shared call
                 index space (imports first, then functions)
}

One row per (type, interface) pair; the same pair twice is an error. A type that is neither class nor enum must not appear on the left.

All implementations of the same slot share a signature. A runtime may derive the argument count from any row of the interface, and must, because callvirt takes its receiver off the stack before the target function is known.

Handlers (Id 9)

The protected regions: which block range of a function is covered by which handler.

handlerCount     uleb128
handlerCount × {
  function       uleb128   index into the Functions section
  startBlock     uleb128   first protected block
  endBlock       uleb128   first block no longer protected
  kind           u8        0 = catch, 1 = finally
  catchType      uleb128   0 = catches everything; otherwise type index + 1
  handlerBlock   uleb128   where control transfers
  slot           uleb128   0 = binds nothing; otherwise slot index + 1
}

Ranges are block indices, not byte ranges.

The order is the contract: innermost region first. While unwinding, a runtime takes the first entry whose range covers the fault site and whose type matches.

The caught value goes into a slot, not onto the stack. When entering a handler the operand stack is cleared.

A runtime must reject: a range outside the block count, startBlock >= endBlock, a handler block inside its own range, a type or slot index outside its table, and a finally region carrying a type or a slot.

Globals (Id 10)

globalCount      uleb128
globalTypes      globalCount × type (§3)
initFunction     uleb128   0 = none; otherwise index + 1 into the shared call
                 index space (imports first, then functions)

A runtime must call the initializer before the entry point. It takes no arguments, returns nothing, and leaves the slots filled. The order within its body is the initialization order.

A reader must reject: a global type of void, an init index outside the call space, and a non-empty global list without an init function.

A slot of type string starts as the empty string, not as an empty reference.

Attributes (Id 11)

New in 3.2. Which struct type describes which function, type or the module, with literal values.

count            uleb128
rows             count × {
                   targetKind   u8        0 = function, 1 = type, 2 = module
                   target       uleb128   into Functions (kind 0) or Types (kind 1); 0 for kind 2
                   type         uleb128   the attribute's struct type: an index into Types
                   valueCount   uleb128   must equal the struct's fieldCount
                   values       valueCount × ConstValue
                 }

ConstValue       = tag u8, then by tag:
                   integers, char     uleb128    two's complement, widened to 64 bits
                   f32 / f64          4 / 8 bytes IEEE-754 bit pattern, little-endian
                   bool               u8
                   string             uleb128    index into the string pool

A row is complete: one value per field of the attribute type, in field declaration order — a field the source did not write carries the field's literal default, filled in by the compiler. The position IS the field index, which is why no index is stored, and each value's tag must equal the tag of the field at its position.

The rows stand in declaration order: modules in compilation order, targets in source order, the attributes of one target in the order written.

Skippable, and inert by design. No other section refers to this one, and a runtime that ignores it runs the program unchanged: an attribute describes its target, it does nothing. What a host makes of a row is the host's business.

A compiler must keep an attributed function alive: 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.

A reader must reject: an unknown target kind, a target or type index outside its table, a nonzero target for a module row, a type that is not a struct, the same (targetKind, target, type) triple twice, a value count differing from the struct's field count, a value tag differing from the field tag at its position, a string index outside the pool, and a value of a non-literal type.

Names (Id 12)

New in 3.2. Field names, ONLY for types an Attributes row references — the attribute types themselves and the attributed type targets. Everywhere else the rule of the Types section stands: field names are not in the bytecode.

count            uleb128
entries          count × {
                   type         uleb128   index into Types
                   nameCount    uleb128   must equal that type's fieldCount
                   names        nameCount × string
                 }

The names stand in field order, so names[i] names the field fieldTypes[i] describes — and for an attribute type, the value at position i of every row that references it. Entries ascend by type index; a referenced type with zero fields has no entry.

Without this section a host reading @Component struct Health { value, max } would learn a shape it cannot name: the layout gives field types and the row gives values, but which field means what exists only in the source.

A reader must reject: a type index outside the table, the same type twice, and a name count differing from that type's field count.


3. Type tags

One byte.

Tag Type Tag Type
0x01 i8 0x08 u64
0x02 i16 0x09 f32
0x03 i32 0x0A f64
0x04 i64 0x0B bool
0x05 u8 0x0C char
0x06 u16 0x0D string
0x07 u32 0x0E void

Composite types from 0x40:

Tag Meaning Followed by
0x40 reference to a Types entry uleb128 type index
0x41 array the element type, again as a type (§3)
0x42 optional (?T) the inner type, again as a type (§3)
0x43 enum uleb128 index of an enum entry
0x44 interface uleb128 index of an interface entry
0x45 struct (value semantics) uleb128 index of a struct entry
0x46 function value uleb128 parameter count, the parameter types, then the return type, all inline
0x47 host object the registered type name, inline as a length-prefixed UTF-8 string

An array's element type is inline rather than a table index: int[][] is 0x41 0x41 0x04. An array type cannot be recursive, so it needs no table entry.

void is valid only as a return type, never as a slot, field or value type.

A value tagged 0x40 is a reference: assignment copies the reference. A value tagged 0x45 is a value type: the same slot sequence, but every binding copies.

Lyric's int, uint and float are aliases for i64, u64 and f64 and appear as those.


4. Execution model

A stack machine with two separate stores per call:

The invariant

The operand stack is empty at every block boundary.

Values that cross blocks travel through local slots. It follows that stack depth at any point is statically determined without data-flow analysis, that a reader can verify it at load time and omit every runtime check afterwards, and that jumps need no stack adjustment.

Jump targets

Jumps name block indices, not byte offsets. The function header carries the offset table. A target is checked with index < blockCount.


5. Instructions

Every instruction starts with an opcode byte. T denotes a type tag byte (§3). The type travels as a tag next to the opcode, not encoded into it; the tag is in the instruction stream, not in the runtime value, so dispatch stays static.

Values and slots

Opcode Mnemonic Operands Stack Effect
0x01 const T, immediate +1 load a constant, see below
0x02 ldloc uleb128 slot +1 read a slot
0x03 stloc uleb128 slot −1 store the top value into the slot
0x04 pop −1 discard the top value

const immediate by tag:

Tag Immediate
i8i64, u8u64 uleb128 of the two's-complement bit pattern, zero-extended to 64 bits
f32 4 bytes IEEE-754, little-endian
f64 8 bytes IEEE-754, little-endian
bool 1 byte
char uleb128 Unicode code point
string uleb128 index into the string pool

The value must fit the width of the tag. const void is invalid.

Arithmetic and bit operations

Each takes two values of the same type and leaves one of that type (−2 +1). The tag names the operand type.

Opcode Mnemonic Opcode Mnemonic
0x10 add T 0x15 shl T
0x11 sub T 0x16 shr T
0x12 mul T 0x17 and T
0x13 div T 0x18 or T
0x14 rem T 0x19 xor T

add through rem require a numeric type, shl through xor an integer type. Signed and unsigned are distinct operations. There is no string concatenation instruction; it lowers to a call.

Comparisons

Two values of the same type produce a bool (−2 +1). The tag names the operand type, not the result type.

Opcode Mnemonic Opcode Mnemonic
0x20 lt T 0x23 ge T
0x21 le T 0x24 eq T
0x22 gt T 0x25 ne T

lt, le, gt and ge require a numeric type. eq and ne are additionally valid on bool, char and string.

Unary operations and conversion

Opcode Mnemonic Operands Stack Effect
0x30 neg T −1 +1 negation, numeric
0x31 not −1 +1 logical not
0x32 bitnot T −1 +1 bitwise not, integer
0x33 conv T_from, T_to −1 +1 numeric conversion

not carries no type tag; only bool is valid. It is the only exception to the tag rule.

conv is valid between numeric types only, with T_from ≠ T_to.

Calls and control flow

Opcode Mnemonic Operands Stack Effect
0x40 call uleb128 index −n [+1] call, see below
0x41 ret 0 return without a value
0x42 retval −1 return the top value
0x43 br uleb128 block 0 unconditional jump
0x44 condbr uleb128 ifTrue, uleb128 ifFalse −1 branch on the top bool
0x45 unreachable 0 must never be reached

call takes paramCount values off the stack, the first parameter lowest, and leaves one value exactly when the return type is not void.

The index addresses a shared index space: all imports first, then all defined functions.

ret and retval must match the function's return type. Every block ends with exactly one of ret, retval, br, condbr, unreachable, throw or endfinally.

Objects

Opcode Mnemonic Operands Stack Effect
0x50 newobj uleb128 type +1 allocate an instance, fields at their zero value
0x51 ldfld uleb128 type, uleb128 field −1 +1 replace the reference with the field value
0x52 stfld uleb128 type, uleb128 field −2 store the field

Stack order for stfld: the reference lies below the value. Push the reference first, then the value.

The type index accompanies ldfld and stfld so a reader can check the field index against a layout at load time without data-flow analysis.

Zero value of a field: numbers 0, bool false, char U+0000, string the empty string, references the null reference. No field is ever uninitialized.

Arrays

Opcode Mnemonic Operands Stack Effect
0x58 newarr element type (§3), uleb128 count −n +1 take n values off the stack and build an array
0x59 ldelem −2 +1 array, index → element
0x5A stelem −3 array, index, value
0x5B arrlen −1 +1 length as i64
0x5C arrcat −2 +1 concatenate two arrays into a new one
0x5D arrrep −2 +1 array, count → new array, repeated

newarr takes the element count as an immediate after the element type, then that many values off the stack, the first element lowest.

An index violation is a panic, not undefined behaviour. This applies to ldelem and stelem. An element index is a runtime value and is not checkable at load time.

An array does not grow; its length is fixed at creation. arrcat and arrrep each produce a new array and leave their operands unchanged. arrrep with count 0 yields an empty array; a negative count is a panic.

Optionals

Opcode Mnemonic Operands Stack Effect
0x60 optnone inner type (§3) +1 push "no value"
0x61 optsome inner type (§3) −1 +1 wrap the top value
0x62 optissome −1 +1 bool: is a value present?
0x63 optget −1 +1 unwrap; panics when there is no value

An optional does not nest. A reader must reject an inner type tagged 0x42.

Representation. A value is "no value" exactly when its reference is empty. For ?string, ?T[] and ?class this coincides with the natural representation. For ?int, ?bool and ?char there is no free bit pattern, so a runtime must carry a marker distinguishable from the payload and hold the number beside it. A runtime must not reserve a bit pattern as null: ?int must carry all 2^64 int values.

Enums

Opcode Mnemonic Operands Stack Effect
0x68 newvariant uleb128 variantType −n +1 allocate a variant; n is its payload field count
0x69 enumtag −1 +1 the variant's tag as i64
0x6A enumas uleb128 variantType −1 +1 narrow to a variant; panics on a mismatched tag

newvariant takes the payload fields off the stack, the first lowest, and sets slot 0 to the tag itself.

match has no opcode. It reads the tag with enumtag and branches on it; after the branch enumas produces a value of the variant's type, and field access is an ordinary ldfld against the variant's layout.

Interfaces

Opcode Mnemonic Operands Stack Effect
0x70 mkiface uleb128 concreteType, uleb128 interfaceType −1 +1 lift an object reference to its interface type
0x71 callvirt uleb128 interfaceType, uleb128 slot −n +0/1 call the slot's implementation for the receiver's concrete type

mkiface carries both indices so a loader can check the implementation relation against the Impls section without data-flow analysis.

callvirt expects the receiver as argument 0, lowest on the stack. n is the slot's argument count including the receiver.

There is no downcast; an interface value cannot be narrowed back to its class.

Representation. A value tagged 0x44 carries two things: the object reference and the index of its concrete type in the Types section. How a runtime stores that is its own choice. Reading the concrete type out of the object is not permitted — an object carries no type tag in this format, and mkiface is the only point at which the concrete type is known.

Structs

Opcode Mnemonic Operands Stack Effect
0x72 structcopy uleb128 structType −1 +1 produce an independent copy of a struct value

There is no newstruct; a struct value is created with newobj.

The copy is recursive across nested structs and shallow across everything else. A field of class, array or interface type carries a reference, and that reference is shared. A field of struct type is itself a value and is copied. The recursion terminates without cycle detection because a struct cannot contain itself.

A compiler must emit structcopy wherever a struct value is bound into a new location: initialization, assignment, argument, return, field and element assignment. A freshly created value — from newobj or as the result of a call — does not need one.

A reader must reject structcopy on a type that is not a struct entry.

Exceptions

Opcode Mnemonic Operands Stack Effect
0x73 throw uleb128 concreteType −1 begin unwinding; terminator
0x74 endfinally 0 end of a finally region; terminator

throw carries the concrete type of the thrown value as type index + 1, or 0 when the type is known only at runtime — the value is then interface-typed and carries its type with it.

The type comparison while unwinding is equality, not a subtype test: the language has no inheritance, so the type at the throw site is the type a catch compares against.

endfinally returns control to the point where unwinding was interrupted; the search continues at the next handler of the same function with the same origin block. A finally region is entered only while unwinding; endfinally without an active unwind is an error.

An exception that leaves the entry point uncaught aborts the runtime like a panic.

Globals

Opcode Mnemonic Operands Stack Effect
0x75 ldglobal uleb128 index +1 read a global slot
0x76 stglobal uleb128 index −1 write a global slot

As ldloc and stloc, but module-wide instead of frame-wide. The index is checked at load time and the access is unchecked at runtime.

Closures

Opcode Mnemonic Operands Stack Effect
0x77 mkclosure uleb128 target≪1 | hasEnv −(0/1) +1 build a function value
0x78 callind uleb128 argc≪1 | retval −(1+argc) +(0/1) call a function value

A function value is a pair of environment reference and function index. A closure without captures carries no reference and costs no allocation.

mkclosure takes its environment off the stack; callind passes it as argument 0, the position a receiver occupies for a method.

Both operands carry a flag in the lowest bit, with the value from bit 1 upward, so that the stack effect of each instruction is known at load time: a closure either has an environment or not, and a function value does not carry its signature in the instruction stream.

The function index is stored incremented by one, so that a closure over function 0 without an environment is distinguishable from "no value".

A reader must reject a mkclosure target index outside the call space.


6. Load-time validation

A runtime must validate a module completely before executing it, and may then run it without safety checks.

Code Reason
LYR-BC0001 magic missing — not a .lyrbc file
LYR-BC0002 unknown major version
LYR-BC0003 file ends inside a structure; section length does not match its contents
LYR-BC0004 index out of range: string pool, function, block, slot, type or field
LYR-BC0005 unknown opcode, unknown type tag, sections not ascending
LYR-BC0006 stack discipline: underflow, depth ≠ 0 at a block boundary, depth > maxStack

For newobj, ldfld and stfld, load-time checking means: the type index lies within the Types section, and for ldfld and stfld the field index lies within the field count of that exact type. Field access at runtime is then an unchecked array access.

The reader stops at the first finding.


7. Example

Source:

fn add(a: int, b: int): int {
    return a + b;
}

Disassembly:

fn main.add -> i64 {
  params: 2
  maxstack: 2
  slots:
    l0: i64
    l1: i64
  bb0:
    ldloc 0
    ldloc 1
    add i64
    retval
}

A minimal module containing only that function, 46 bytes. The compiler writes a SourceMap by default; this is the stripped form, which is why section 6 is absent:

4C 59 52 42                  magic "LYRB"
03 00                        version.major = 3
02 00                        version.minor = 2

01                           § section 1 — Capabilities
01                             byteLength = 1
00                             bitset = 0

02                           § section 2 — Strings
0A                             byteLength = 10
01                             count = 1
08                             [0] length = 8 bytes
6D 61 69 6E 2E 61 64 64          "main.add"

04                           § section 4 — Imports
01                             byteLength = 1
00                             count = 0

05                           § section 5 — Functions
12                             byteLength = 18
01                             count = 1
00                             nameIndex = 0            -> "main.add"
02                             paramCount = 2
04                             returnType = i64
02                             slotCount = 2
04 04                          slotTypes = i64, i64
02                             maxStack = 2
01                             blockCount = 1
00                             blockOffsets[0] = 0
07                             codeLength = 7
02 00                            ldloc 0
02 01                            ldloc 1
10 04                            add i64
42                               retval

The slot table has two entries: intermediate values stay on the operand stack and need no slot. An emitter that writes every intermediate into a slot is also conformant.


8. Runner contract

This section is normative for any runtime used as lyric --vm <path>.

8.1 Invocation

<vm> run <file.lyrbc> [-- <program-args>]

The first parameter is literally run. Everything after the first -- belongs to the Lyric program. A runtime may offer further commands.

8.2 Exit codes

Code Meaning
0255 return value of main, masked with & 0xFF
101 panic
1 load, validation or IO error — the program never started
2 invocation error: missing argument, unknown command, wrong file kind

101, 1 and 2 collide with a program returning those values. Callers that need the distinction read stderr.

8.3 Streams

No mixing in either direction.

8.4 Version output

<vm> --version

produces free-form text on stdout and exit code 0. The driver passes it through and does not interpret it.

8.5 Program arguments

Everything after the first -- belongs to the Lyric program. A runtime delivers it to the entry point when its signature calls for it:

Entry point Behaviour
fn main(): int arguments are ignored
fn main(args: string[]): int the runtime builds a string[] into parameter slot 0

Which form is present is read from the signature in the function table.

A reader must reject an entry point with more than one parameter, and one whose single parameter is not string[].

8.6 Modules without an entry point

A module without a Start section is a library: valid bytecode, not a program. run on it is an error (LYR-VM0001); verify and info are not.

8.7 Conformance

For a given module,

lyrvm verify <file.lyrbc>

answers whether this runtime would accept it — format validation (§6) and import binding, without executing an instruction.