MuIR serialization format
MuIR is the canonical textual persistence and interchange format for portable MuLang IR. MuIR files use the .muir extension and UTF-8 encoding. The format version is independent from the MuLang language version and language-profile fingerprint.
MuIR reconstructs an IrProgram; it does not prove that the program is semantically valid for a host environment. A host MUST apply the following lifecycle:
- read the MuIR document;
- reject the document when reading reports an error;
- select the expected provider environment;
- validate the reconstructed program through
IrValidator; - export or execute the program only when validation succeeds.
1. Lexical structure
A MuIR document consists of Unicode scalar values decoded from UTF-8. Invalid UTF-8 MUST be rejected.
Spaces, horizontal tabs, CRLF line endings, LF line endings, and blank lines are insignificant between tokens. A # outside a string begins a comment that continues to the next line ending or end of document.
Keywords and symbolic tokens are case-sensitive. An atom is a non-empty sequence delimited by whitespace, a comment, a string delimiter, or one of:
{ } [ ] , :
Strings are delimited by ". They support:
| Escape | Value |
|---|---|
\" |
quotation mark |
\\ |
reverse solidus |
\n |
line feed |
\r |
carriage return |
\t |
horizontal tab |
\u{H...} |
one Unicode scalar encoded by one through six hexadecimal digits |
Raw line endings, unpaired surrogates, invalid scalar values, and other escapes MUST be rejected.
Non-negative integers use invariant decimal notation without a sign. Signed integer constants may use one leading -.
2. Document grammar
The following grammar describes MuIR version 2. Repetition counts are declared immediately before the repeated production and MUST match the number of following declarations.
document ::=
"muir" "2"
"mode" compilation-mode
"environment" string
"profile" string
"types" integer type-definition*
entry-function
"users" integer user-function*
"attributes" integer "{"
attribute*
"}"
"end"
compilation-mode ::= "expression" | "program"
entry-function ::= function-header("entry")
user-function ::= function-header("user")
function-header(kind) ::=
kind string type-ref block-ref
"slots" integer
"blocks" integer
"lifetime-regions" integer
"exception-regions" integer
"{"
lifetime-region*
exception-region*
slot*
block*
"}"
lifetime-region ::=
"lifetime-region" lifetime-region-ref
"parent" (lifetime-region-ref | "none")
"entry" block-ref
exception-region ::=
"exception-region" exception-region-ref
"parent" (exception-region-ref | "none")
"parent-part" ("protected" | "handler" | "cleanup" | "none")
"protected" block-ref block-list
"handler" ("none" | block-ref slot-ref block-list)
"cleanup" ("none" | block-ref block-list)
slot ::=
"slot" slot-ref slot-kind type-ref slot-mutability
"region" lifetime-region-ref
slot-kind ::= "parameter" | "local" | "temporary"
slot-mutability ::= "mutable" | "readonly"
block ::=
"block" block-ref "region" lifetime-region-ref
"instructions" integer "{"
instruction*
terminator
"}"
instruction ::= "ins" opcode span operands
terminator ::= "term" terminator-kind span operands
span ::= integer integer
type-ref ::= "t" integer
slot-ref ::= "%" integer
block-ref ::= "bb" integer
lifetime-region-ref ::= "lr" integer
exception-region-ref ::= "er" integer
block-list ::= "[" (block-ref ("," block-ref)*)? "]"
attribute ::=
"attribute" attribute-target attribute-name
"{" attribute-payload-token* "}"
attribute-target ::=
"program"
| "function" function-target
| "slot" function-target slot-ref
| "type" type-ref
function-target ::= "entry" | "user" string
attribute-name ::= atom
The two integers in a span are the source start and length. Both MUST fit in a signed 32-bit integer, and their checked sum MUST also fit.
The document MUST contain exactly one entry function. User-function, slot, block, instruction, element, and argument order is preserved. Object properties are normalized by ordinal property name.
Lifetime-region and exception-region identifiers are independently contiguous
and zero-based. Every function serializes lifetime root region lr0.
Parameter slots MUST be readonly, belong to lr0, are implicitly defined at
function entry, and cannot be instruction destinations. Temporary slots MUST
be mutable. Read-only local validation is path-sensitive within each
activation of its owning lifetime region.
Exception component block lists have set semantics and canonical output sorts
them by block identifier. A handler declaration includes its mandatory
read-only error slot.
3. Type table
Type identifiers are contiguous and zero-based. A definition MUST use the next identifier in sequence. Composite definitions may refer to any declared type identifier, including a later definition, so finite object-type graphs can be self-recursive or mutually recursive.
type-definition ::=
"type" type-ref "intrinsic" intrinsic-type
| "type" type-ref "nullable" type-ref
| "type" type-ref "array" array-capability type-ref
| "type" type-ref "object" openness integer
"[" object-property-list? "]"
intrinsic-type ::=
"bool" | "int" | "float" | "number" | "string"
| "primitive" | "error" | "unknown" | "object" | "void" | "null"
array-capability ::= "mutable" | "readonly"
openness ::= "open" | "closed"
object-property-list ::= object-property ("," object-property)*
object-property ::=
string type-ref
("required" | "optional")
("mutable" | "readonly")
The compiler error-recovery type is not representable. A writer MUST fail explicitly when that type occurs.
The reader parses the complete type table before materialization and publishes no partially initialized type. Recursive cycles consisting only of nullable or array wrappers, with no structured-object node, are invalid.
Canonical type identity is the complete wire definition. The writer:
- ignores source
TypeSymbolreference sharing; - sorts object properties by name with ordinal comparison;
- computes recursive structural identity coinductively;
- merges bisimilar finite recursive type graphs;
- orders acyclic dependencies before dependants;
- permits forward references within a recursive strongly connected component.
Openness, property name, optionality, mutability, property type, and array capability remain identity-bearing. Property declaration order does not.
Version 1 object properties omit the mutability token and are read as mutable. Version 2 writers always emit both independent capability tokens.
Provider type IDs, language-facing object type names, and named-versus-anonymous origin are not represented. The environment fingerprint identifies the required provider schema, while IR validation compares the reconstructed object type structurally.
Source-declared and provider type names do not participate in the type table.
Reachable names may be represented by source-type-names diagnostic
attributes. Unused source declarations remain absent, and changing only a
diagnostic alias does not change canonical executable type identity.
4. Well-known attributes
MuIR 2 defines a closed catalog of optional diagnostic attributes:
| Attribute | Target | Payload | Cardinality |
|---|---|---|---|
source-document |
program | one absolute URI string | zero or one |
source-line-map |
program | source length, line count, and line-start offsets | zero or one |
provider-global-name |
program | provider ID and language name | zero or more, unique by provider ID |
provider-function-name |
program | provider ID and language name | zero or more, unique by provider ID |
source-name |
function or slot | one string | zero or one per target |
qualified-source-name |
function | one string | zero or one |
declaration-span |
function or slot | one span | zero or one per target |
source-type-names |
type | a non-empty string list | zero or one |
Known payloads are:
source-document-payload ::= string
source-line-map-payload ::= integer integer integer*
provider-symbol-name-payload ::= string string
source-name-payload ::= string
qualified-name-payload ::= string
declaration-span-payload ::= span
source-type-names-payload ::= integer "[" (string ("," string)*)? "]"
Source document URIs MUST be absolute and are serialized using their original string representation. Names and provider IDs MUST NOT be empty or whitespace. Source type names MUST be unique and ordinally sorted, and their declared count MUST match the list. The first integer in a source line map is the source length in Unicode scalar values. The second integer is the line count and MUST match the number of following line-start offsets. A line map MUST contain at least one line start; the first MUST be zero, and subsequent offsets MUST be strictly increasing and no greater than the source length. Instruction, terminator, and declaration spans MUST remain within the mapped source length. Unknown attribute names on known targets are skipped and discarded through their balanced-brace payload. Unknown target kinds, malformed targets, unbalanced payloads, invalid known payloads, duplicate singleton attributes, and conflicting slot names are errors.
Attributes do not affect executable semantics. Unknown attributes accepted by an older reader are not preserved when that program is rewritten.
5. Constants
Constant operands encode runtime representation explicitly:
constant ::=
"null"
| "bool" ("true" | "false")
| "int" signed-integer
| "float" float-value
| "string" string
float-value ::=
finite-binary64
| "positive-infinity"
| "negative-infinity"
| "nan"
| "negative-zero"
Finite binary64 values use invariant round-trip notation. negative-zero preserves the negative-zero bit pattern. nan represents the MuLang NaN value; payload and sign bits are not preserved.
Arbitrary host objects are not portable constants and MUST be rejected by the writer.
A constant whose static type is primitive uses the token for its concrete
runtime representation. No separate primitive constant token exists.
6. Instructions
Every instruction starts with its opcode and source span. Operand order is fixed:
| Opcode | Operands after span |
|---|---|
constant |
destination, type, constant |
copy |
destination, source |
load-global |
destination, global ID string |
unary |
destination, unary operator, operand |
binary |
destination, binary operator, left, right |
convert |
destination, source, target type, conversion kind |
truthiness |
destination, source |
type-test |
destination, source, tested type |
is-null |
destination, source |
has-property |
destination, target, key |
create-array |
destination, array type, slot list |
create-object |
destination, object type, property-value list |
get-property |
destination, target, name string, optional flag, array-length flag |
set-property |
target, name string, value |
remove-property |
target, name string |
get-element |
destination, target, index, object-access flag, optional flag |
set-element |
target, index, value, object-access flag |
remove-element-property |
target, key |
provider-call |
destination or none, function ID string, return type, argument list |
user-call |
destination or none, function ID string, return type, argument list |
A slot list is [ followed by comma-separated slot references and ].
A property-value list is [ followed by comma-separated pairs of a string, :, and a slot reference, then ].
Boolean flags use true and false.
Unary operator tokens are:
| Token | IR operator |
|---|---|
identity |
identity |
negate |
numeric negation |
logical-not |
logical negation |
bitwise-not |
bitwise complement |
Binary operator tokens are:
add subtract multiply divide remainder
left-shift right-shift
less-than less-than-or-equal greater-than greater-than-or-equal
structural-equal structural-not-equal
identity-equal identity-not-equal
bitwise-and bitwise-xor bitwise-or
Conversion-kind tokens are value and checked.
Unknown opcodes and operator tokens MUST be rejected. A reader MUST NOT ignore an executable operation it does not understand.
7. Terminators
Terminator operands after the source span are:
| Token | Operands |
|---|---|
jump |
target block |
branch |
condition slot, true block, false block |
return |
value slot or none |
throw |
error-value slot |
resume |
no operands |
Every block contains exactly one terminator after its declared instructions.
resume is valid only in a cleanup component. A cleanup cannot return.
Ordinary control-flow edges cannot enter handlers or cleanup components.
8. Canonical form
The canonical writer MUST:
- emit UTF-8 without a byte-order mark when writing a stream;
- use LF line endings;
- use two spaces per indentation level;
- emit one space between ordinary tokens;
- emit no comments or trailing whitespace;
- emit one final line feed;
- preserve function, slot, block, instruction, element, and argument order;
- emit the MuIR 2
attributessection even when empty; - order targets as program, function, slot, then type;
- order functions with entry first and users in document order;
- order slots by function and slot ID and types by type-table ID;
- order attributes for one target by attribute name;
- order provider mappings by provider ID and type aliases ordinally;
- emit object properties by ordinal name;
- assign type identifiers deterministically, with acyclic dependencies before dependants and stable ordering inside recursive components;
- use the tokens and operand orders defined by this section.
Serializing wire-equivalent IR graphs MUST produce byte-identical output regardless of equivalent type-instance sharing or object-property declaration order. Reading canonical MuIR and writing the reconstructed program MUST reproduce the same bytes.
The default writer options emit the complete canonical document. Writers MAY
offer an explicit diagnostic-metadata projection that omits the
source-line-map attribute or all diagnostic attributes. Such output remains
canonical for the projected metadata set but is intentionally lossy: reading
and rewriting it cannot restore omitted attributes. The MuIR 2 attributes
section remains present even when all attributes are omitted.
MuIrWriterOptions.WriteAttributes controls whether any diagnostic attributes
are emitted. MuIrWriterOptions.WriteSourceLineMap independently controls the
source-line-map attribute when attribute writing is enabled.
Readers MAY accept non-canonical whitespace and comments, but writers MUST emit only canonical form.
9. Reading, diagnostics, and limits
MuIR reading distinguishes structural reconstruction from semantic IR validation. A successful read guarantees that the document is lexically and structurally representable by the public IR model. It does not guarantee valid control flow, definite assignment, provider identifiers, type compatibility, or environment/profile fingerprints.
Reading failure returns no partial program and at least one error diagnostic. A diagnostic contains:
- a stable
MUIRcode; - severity;
- zero-based document offset and length;
- one-based line and column;
- a message.
The reader MUST reject unsupported format versions, invalid references, invalid type definitions, malformed instructions and terminators, source-span overflow, trailing content, and constructor failures.
Hosts MAY configure positive limits for document, string, token, type-nesting, type, function, slot, block, instruction, list-element, diagnostic, total attribute, per-target attribute, skipped unknown-payload token, source-type name, and provider-symbol mapping counts. A limit violation MUST fail reading without returning a partial program.
10. Compatibility
MuIR version 2 adds the primitive intrinsic-type token, structured-object
property mutability, lifetime and exception regions, and well-known
attributes. The reader continues to accept version 1 documents, reads their
properties as mutable, and converts positional slot names into source-name
attributes while preserving the obsolete compatibility property.
Earlier prerelease MuIR 2 layouts are not compatibility formats and MUST be
rejected. A MuIR 2 document therefore always contains the attributes section
and has no positional slot-name operand. The canonical writer emits version 2.
MuIR compatibility is defined by the wire contract, not by .NET enum names, record names, or numeric enum values.
Changing an existing token, operand order, required field, or semantic interpretation requires a new MuIR format version. Adding an instruction, terminator, type form, or other required executable construct also requires a new version.
A reader MUST reject unknown required constructs and unsupported versions rather than guessing. Version 1 fixtures remain reader-compatibility artifacts; rewriting version 1 input with the current writer upgrades it to version 2.