l
GitHub ↗

The l Programming Language Specification

This document is the normative specification of the l programming language. It defines source syntax, program organization, types, evaluation, runtime behavior, and the language ABI. A program is valid only when it satisfies every applicable rule in this document.

Except where stated otherwise, declarative text, grammar productions, and tables are normative. must and must not state requirements; may states permission. Invalid and error require rejection before executable code is produced. Implementation-defined behavior is valid behavior whose choice must be documented. Undefined behavior imposes no requirements after the operation occurs. No defined result means that an operation produces no result value while all other execution requirements continue to apply. A term is defined where it appears in bold.

Examples are non-normative. They illustrate rules but neither add exceptions nor define behavior. This specification describes the language, not compiler commands, options, artifact formats, or implementation architecture beyond the required compilation boundaries.

Compilation Steps

The compilation order is fixed:

Source File(s) -> Lexer -> Token Stream -> Parser -> HIR -> Reducer -> MIR -> Checker -> LIR -> Emitter -> Target Output

Target output may be object code, LLVM IR (LLVM’s external intermediate representation), or another representation. LIR is always an internal language representation and must never be identified with LLVM IR.

Stage contracts

Each compilation stage is defined by its contract:

Stage Input → output Sole responsibilities
lexer source files → token stream Validate UTF-8 lexical structure and produce tokens with exact lexemes and origins.
parser token stream → HIR Validate HIR grammar and build one package tree; it performs no reduction or type checking.
reducer HIR → MIR Expand macros, eliminate every syntactic sugar, and validate rules needed specifically for those reductions.
checker MIR → LIR Resolve names and semantic identities, check and infer types, and enforce all target-independent core-language rules. It performs no syntactic reduction.
emitter LIR → target output Apply the selected ABI, layout, and target representability rules. It receives only checked input.

Macro expansion is the sole exception to the general query boundary: the reducer may resolve only the names, aliases, types, and ABI facts required by a predefined macro. Those queries neither check unrelated HIR nor produce checked MIR.

The parser leaves each use without its interface; the implementation supplies every interface into HIR before reduction, as specified under Libraries.

Each stage may rely on every invariant guaranteed by an earlier stage. A later stage must not reinterpret an earlier stage’s grammar, repeat its validation, or construct an error that cannot occur for valid input to that stage.

The relation among language levels is a construct-subset relation:

LIR ⊆ MIR ⊆ HIR

No later level introduces a language construct absent from an earlier level. The reducer may generate nodes only from constructs already available in HIR. Resolution targets, inferred types, source provenance, and other annotations are metadata, not language constructs. The relation permits MIR and LIR to have the same tree shape; LIR is still the subset that has passed checking and carries checked facts, including every type the checker infers under Literal typing.

Language representations

HIR, the high-level intermediate representation, is the parsed form of a complete package and contains every source-language construct. It preserves source forms, item origins, and source ranges, but not whitespace or comments. It may contain macros, Self, anonymous functions, and omitted/defaulted forms listed under Syntactic Sugars and Macros.

MIR, the mid-level intermediate representation, is desugared HIR. It contains only the core language: the construct set shared by MIR and LIR after all syntactic reduction. Every function result, jump operand, block result, branch, loop alternative, and enum ID that HIR permits to be implicit is explicit in MIR. No macro invocation, Self, or anonymous function reaches MIR. Type-dependent expressions—including eager operators other than && and ||, compound assignments, applications that may be calls or conversions, contextual aggregate construction, loops, switches, and unsafe—are core constructs rather than checker rewrites.

LIR, the low-level intermediate representation, is checked and inferred MIR. It uses only MIR constructs and records their resolved symbols, types, operation meanings, control-flow targets, and ABI-relevant facts. In LIR, every let has a type and every numeric literal has the type given under Literal typing. The checker may reject or annotate a node but must not replace it with different syntax.

Diagnostics

An invalid program is rejected by the first applicable stage boundary, independent of how many diagnostics an implementation chooses to report:

Stage Error class
lexer Invalid encoding, token spelling, literal spelling, or unrecognized character.
parser Input outside the HIR grammar.
reducer Unknown or invalid macro, expansion-limit violation, invalid Self, mixed omitted and written enum IDs, an anonymous function in a static initializer, or another impossible HIR-to-MIR reduction.
checker Invalid identifiers surviving expansion, duplicate or unresolved names, invalid types, operations, control flow, visibility, or static semantics.
emitter A checked value, layout, symbol, or target-width constant not representable under the selected ABI, or an item whose symbol conflicts under Linkage.

An array length, written enum ID, and tuple index accept a numeric literal syntactically; positivity, integrality, range, and bounds restrictions belong to the checker, except target-width representability belongs to the emitter. The reducer rejects mixed omitted and written enum IDs before inserting omitted IDs.

Every diagnostic must identify the originating source file and range when one is known. A generated node inherits the range of the source construct reduced into it. A macro-generated node uses the complete invocation range; a surviving macro argument retains its own range. Expanded text need not be displayed. Successful checking is not memory-safety validation.

Modules and Packages

Packages and modules

A package is the complete compilation and distribution unit. It contains exactly one root module and can be used as a library. A public root main additionally gives it an entry point. A module is one item scope; the root and each namespace are modules. A namespace is a named module nested in another module. Package metadata, including an optional package name, is external to source text; source cannot determine its own package name or revision.

A source file is one named unit of source text belonging to a package. A source entry is either one source file supplying the root module, or a directory interpreted recursively:

  • Every .l file directly in a directory contributes items to that directory’s one module. Files do not introduce names, and private items are shared across those files.
  • A subdirectory containing an .l file at any depth introduces a namespace named after the subdirectory and recursively contains its files and child namespaces. A subdirectory with no .l descendant is ignored.
  • A directory namespace name must be a valid non-keyword, non-predefined identifier. A source file name is not a language name; only its .l extension is significant.
  • Source ordering does not affect name visibility or runtime initialization.

A root source-entry directory containing no .l file at any depth is invalid.

namespace Name { items } declares an inline namespace. A namespace is declared exactly once: repeated inline declarations and an inline declaration colliding with a directory namespace are duplicate items. Every module item may appear in a namespace body, including another namespace.

Directory composition joins source files; paths name visible items directly. Another package is reached only through use; see Libraries.

An optional package name has one or more slash-separated segments. Each segment starts with an ASCII letter, continues with ASCII letters, digits, or interior -, and ends with an ASCII letter or digit. It surrounds the root module for linkage without changing source name resolution. Package naming is specified further under Linkage.

Libraries

A library is a package another package uses, named by a one-segment package name. use name; declares that the package depends on the library name and declares in the enclosing module a namespace name holding the library’s interface. use appears only before the items of a source file or inline namespace body. The namespace shares the module’s item scope, so a second use of one name in a module, even from another file, is a duplicate item.

The interface of a library is the part a package using it sees: its root module reduced to its aliases, its uses, its public function and static declarations, its public function and static definitions as the same declarations without body or initializer, the public functions of its impl blocks reduced likewise, and each namespace whose own interface is nonempty. Every other item, and every impl block left empty, is omitted. The interface of each use inside it is supplied the same way. Every use of one library reaches one interface, so its impl blocks place their functions once.

The interface namespace is the root module of the library’s own paths: an absolute path written inside it starts at that namespace, and the ancestor rule of Names and scope keeps it from selecting items outside it. Its items link as the library’s package; see Linkage. Every library a package uses, directly or through another library, is a link dependency of the package.

Locating a library’s source is implementation-defined. Before reduction, the implementation rejects a use whose library it cannot locate, whose name is not a package name segment, or that reaches its own library through the uses of the libraries it names.

Names and scope

Functions, statics, type aliases, and namespaces share one item scope in each module. No item may repeat, including a second declaration followed by a definition. An impl block introduces no module-scope name; its functions occupy their target type’s namespace.

Items are visible throughout their module regardless of source order. Functions may recurse mutually, aliases may refer forward, and static initializers may contain valid forward symbol relocations. Visibility is checked after path resolution and is independent of mut and volatile.

Namespaces and type aliases are path-visible without pub. Functions and statics require pub when selected from another module.

Form Lookup
Unqualified value Lexical parameters and bindings, inner to outer, then current-module statics and functions.
One-segment named type Current module, then predefined names.
Relative item path Current module; its first segment is a child namespace, except a one-segment item.
Absolute item path Root module of the enclosing package, per Libraries; lexical names never shadow it.
Enum constructor path Resolve the prefix as a type, then the final segment as its variant before associated-function lookup.
Associated expression path Resolve the prefix as a type, then find the function globally on that type, subject to visibility.

A child module cannot select an item owned by an ancestor, even with an absolute path; it may use an absolute path through the root to a non-ancestor module. A path prefix that is not the required namespace or type is unresolved.

A local binding may shadow another binding, a parameter, or an unqualified module item without changing it. A binding is not visible in its own initializer. Lexical names do not shadow qualified enum constructors or associated paths. Named and anonymous functions require unique parameter names; labels in function-reference types need not be unique.

An identifier surviving macro expansion is invalid if it contains adjacent underscores. This applies to item, associated-function, parameter, function-type label, binding, field, variant, path-segment, and macro names. A macro argument consumed during expansion need not survive for this check.

Module items

A declaration introduces an entity’s name and type or signature. A definition is a declaration that also supplies a function body or static storage. A function signature ending in ; is a declaration. A signature followed by a body is a definition. A static without an initializer is a declaration of externally supplied storage; a static with an initializer is a definition. Omission never implies zero initialization. extern may mark a declaration outside an impl block, which selects its symbol under Linkage; a definition is never extern.

pub grants cross-module source visibility. On a definition it also requests exported linkage; a private definition has internal linkage. A declaration has external linkage whether or not it is public, because another object supplies its entity. These rules apply equally to functions and statics.

A function body’s result and every return operand must be compatible with its declared result type.

Associated functions

An associated function is an ordinary function in a type’s namespace, with no receiver or method-call syntax. Statics and aliases cannot be associated.

impl T { ... } may appear at module or namespace level. The block takes no pub; each contained function is a declaration or definition with ordinary visibility. Its target may be any type expression after macro expansion and alias resolution except void or a mut- or volatile-qualified type. A recursive alias is valid.

Each resolved structural type owns one global associated namespace, independent of packages and modules. Implementations for aliases and inline spellings of the same type merge. Duplicate function names are invalid, and an enum variant and an associated function cannot share a name. T::f requires T to be a predefined name or visible alias path and denotes an ordinary function value. A public associated function is callable from any module; a private one only from the module containing its impl. An associated function named main is not an entry point.

Statics

A static name has exactly its annotated resolved type; no pointer is added implicitly. Its type must expose storage:

Exposed type Eligible Named region
Qualified or unqualified *T, valid non-void T Yes One stored T representation.
Fixed array, struct, or enum, optionally qualified Yes The aggregate backing itself.
Scalar, tuple, slice, or function-reference type No Use a raw pointer to a slot holding that value.
void or qualified or unqualified *void No void has no loadable or storable representation.

A raw-pointer static’s pointee may be any valid non-void type. A top-level aggregate definition must describe its own backing with matching aggregate or variant syntax rather than name another aggregate reference.

A static initializer is a compile-time description of static data, relocations, and nested backing objects. After macro expansion, it permits only the following forms, recursively:

Form Restriction
Numeric, character, and Boolean literals Exact expected type and valid range.
String and C-string literals Expected type accepts immutable [u8] or *u8, respectively.
Unary negation of a numeric or character literal Normal literal and pre-negation range rules.
Conversion T(n) of a numeric literal n n has type T under Literal typing.
Tuple expression At least two recursively valid elements.
Array or nonempty struct syntax Exact expected aggregate type and complete recursively valid contents.
Qualified enum constructor Exact variant and recursively valid optional payload.
Named static, function, or associated function A visible, compatible symbol relocation, including a predefined one.

Grouping is transparent. Calls, anonymous functions, runtime loads, projections, indexing, slicing, assignments, control flow, sequences, local bindings, unsafe, other conversions, null, and computation other than literal negation are invalid. Macros expand before this whitelist, so a macro result is allowed when the resulting form is allowed.

Each allowed form is checked against its expected type with ordinary outer qualifier conversion; a numeric literal takes that type. Pre-negation range checking makes signed minima unavailable; character negation wraps at 32 bits and need not remain a Unicode scalar.

Top-level raw-pointer slots are initialized against their pointee type. Aggregate members use their exact declared type. Symbol relocations may refer forward, to the containing static, or form cycles through compatible reference-valued members. A mutable reference may lose outer mut or gain outer volatile; no deeper capability changes are implicit.

Nested aggregate syntax creates distinct static backing for each child and stores a relocation to it. Static initialization performs no runtime call or runtime allocation. An unqualified or only-volatile definition provides read-only storage; outer mut provides writable storage. Unsafe retyping cannot make read-only storage writable. Each separately backed child uses its own declared outer qualifier to determine writability. An implementation does not guarantee that externally supplied storage actually has the declared layout, duration, or writability.

Entry point

A package need not have an entry point. If the root module defines pub fn main, its exact resolved type must be fn () i32; a result alias resolving to i32 is accepted. Only that root definition is an entry point. Private, nested, declared-only, and associated functions named main are ordinary functions. Its result is the program’s exit status where the platform defines one. A package with an entry point is source-executable and usable as a library; exposing the platform entry symbol additionally requires the linkage rule under Linkage.

Types and Values

Types

The fifteen primitive types are:

Family Types
Fixed-width unsigned integers u8, u16, u32, u64
Pointer-width unsigned integer usize
Fixed-width signed integers i8, i16, i32, i64
Pointer-width signed integer isize
Floating point f32, f64
Unicode character char
Boolean bool
Unit void

The integer types are fixed- and pointer-width integers; integral types add char; numeric types add f32 and f64. bool, void, pointers, and function references are not numeric. usize and isize use the selected ABI’s pointer width. char stores a 32-bit unsigned value. f32 and f64 use IEEE 754 binary32 and binary64.

An unsigned N-bit integer ranges from 0 through 2N - 1. A signed N-bit integer ranges from -2N-1 through 2N-1 - 1.

void has exactly one value, null; all void values compare equal. It may be returned, sequenced, aliased, or bound by a let. A parameter, field, array element, slice element, or tuple element cannot resolve directly to void; static storage cannot contain it; and *void cannot expose a static slot.

Numeric literals are untyped; each takes the type given under Literal typing.

Value representation

A value is an immutable typed semantic entity, independent of its machine representation. A reference is a value designating storage or a callable function. Values use two language-level representation categories:

Category Types Representation carried by an expression
Direct Primitives, raw pointers, function references, tuples, slices The complete value.
Address-valued aggregate Fixed arrays, structs, enums One reference to backing storage.

The reference an address-valued aggregate carries is its backing reference. Aggregate equality compares backing addresses. A raw pointer to aggregate A designates a slot containing an A backing reference. Aggregate-valued fields and elements hold such a backing reference and have separate backing; direct members are stored inline.

Mutable reference types

A storage-accessing reference is a raw pointer, fixed array, struct, enum, or slice reference through which storage can be accessed. Every unqualified one is immutable at its outer level. mut R grants write capability only through that level. It is not binding mutability, ownership, exclusivity, a runtime flag, or an ABI change.

Resolved family Accepts mut Capability
Raw pointer Yes Replace the designated slot.
Fixed array Yes Reconstruct backing or replace element slots.
Struct Yes Reconstruct backing or replace field slots.
Enum Yes Reconstruct tag and payload.
Slice Yes Replace viewed element slots; not the descriptor.
Function reference, tuple, scalar, void No These forms provide no writable storage level.

Qualifiers apply at exactly the written level after alias resolution. Repetition is idempotent, including through an alias, and structural identity compares normalized qualifiers at every level.

Projection Result
Direct field or element of type T mut *T from a mutable base, otherwise *T; retain base volatile.
Aggregate field or element of type A Automatically load exact A; the load is volatile when the base is volatile.
Raw-pointer dereference Load the exact pointee type; volatile when the pointer is volatile.
Tuple projection, for binding, switch payload binding Exact declared element or payload type.

Volatile reference types

volatile R requires volatile accesses through that reference level; a store additionally requires mut. Volatility is valid exactly where mutability is valid and changes no representation or ABI property. Repetition is idempotent, and volatile mut R equals mut volatile R.

A control-flow join retains outer mut only when both inputs have it and outer volatile when either input has it. Neither qualifier moves inward. Volatile access is observable but neither atomic nor synchronization; unqualified access is non-volatile. A volatile access need not be one access of its slot’s width.

Raw pointer types

*T permits a load from a slot containing T; outer mut also permits a store and outer volatile makes those accesses volatile. A load produces exact T without changing nested qualifiers. Raw pointers support only == and !=, which compare addresses.

*void and mut *void are opaque pointers. They may be passed, compared, converted, and constructed from pointer-width integers, but not dereferenced or assigned through because void has no stored representation.

Function reference types

A function-reference type is callable and is not a raw pointer. Parameter labels do not participate in identity; only ordered parameter types and the result type do. A function name is a value of its declared function-reference type.

A function reference occupies one pointer-sized direct value. It supports equality and inequality by function identity, but no ordering, arithmetic, dereference, or conversion to or from raw pointers or integers. Calling a null or otherwise invalid function reference is undefined behavior. Calls require exact arity, positional compatible arguments, and yield the declared result type.

mut *fn (value: i32) i32 is a mutable slot containing a function reference; dereference the slot before calling its contents. Qualifiers within parameter and result types remain part of those types.

Tuple types

A tuple is an ordered structural direct value with at least two non-void elements. Identity is positional and structural after alias resolution. Projection uses an in-bounds zero-based integer literal that must decode without overflow to u64. Projection yields the selected value, never an assignable slot.

Tuple equality recursively compares direct elements; address-valued aggregate elements compare backing addresses and slices compare descriptors. Tuple ordering is undefined.

Fixed array types

[T; N] is address-valued backing for N contiguous slots of non-void T. N is a numeric literal that must decode to a positive integer representable by u64 and fit target usize when used by generated iteration. Names and expressions are not lengths.

An array initializer supplies exactly N comma-separated elements without a trailing comma. Elements are evaluated and stored left to right. Indexing accepts u8, u16, u32, u64, or usize and follows the projection rules. A valid index’s target-width value is less than N; invalid indexing is undefined behavior. Array type identity requires equal resolved element types and lengths.

Slice types

[T] is a direct descriptor of contiguous non-void element storage: an element storage reference followed by a usize element count. The descriptor is copied by value. .length returns its count as usize; equality compares both descriptor fields.

Slicing a fixed array or slice yields [T], preserves the base’s outer mut and volatile, and neither copies elements nor extends storage duration. Bounds are converted to pointer width. For source count N:

Form Count Validity
base[start..end] end - start start <= end <= N
base[..end] end end <= N
base[start..] N - start start <= N
base[..] N Always, including N = 0.

Bounds may use any accepted array index type. Element access uses the same index types and projection rules. Invalid bounds or indexes are undefined behavior; no check is required.

A slice element type must not reach a recursive alias cycle, including through an otherwise valid aggregate. This rule applies both to written slice types and slices produced by range syntax.

Struct types

A struct is an anonymous ordered structural address-valued type with at least one uniquely named non-void field. Identity includes field names, order, resolved types, and whether the struct is packed. A struct initializer names every field exactly once in any order; evaluation and storage follow initializer source order. Selection requires a struct reference and follows the projection rules.

Enum types

An enum is an anonymous ordered structural discriminated union with unique variants. Each variant has zero or one payload; an omitted or void payload is a unit variant. Identity includes variant names, order, resolved payload types, and effective IDs.

Variant IDs are either omitted for every variant or written for every variant. Omitted IDs are zero-based source positions; mixing omitted and written IDs is invalid before insertion. A written ID must be a unique integer literal fitting u64 and target pointer width.

An enum constructor is qualified enum-variant syntax through a visible alias path naming its enum: Event::Offline or Event::Reading(value). The prefix alone determines the enum type; expected context supplies only outer qualifiers, unless the prefix alias itself is qualified, which then fixes them. A structural enum without an alias cannot be constructed. A unit variant takes no argument and a payload variant exactly one. As an expression, an enum constructor is a runtime aggregate initializer; as the right side of an aggregate assignment, it is a constructor that reconstructs existing backing. Equality compares backing addresses.

Type aliases

let Name: Type = T; declares a transparent module-level alias. Type is accepted only in this annotation and is not a type expression. Aliases have no pub, are visible throughout their module, and may refer forward.

A recursive alias cycle is valid only when at least one alias in the cycle has a fixed-array, struct, enum, or function-reference type as its direct right-hand side, optionally with eligible outer mut or volatile qualification. Pure alias cycles, cycles crossing only raw pointers, and cycles through slice elements are invalid. No recursive cycle may be reachable from a slice element type.

Conversions

The only implicit conversions between distinct resolved types discard outer mutability and/or add outer volatility:

R -> volatile R
mut R -> R
mut R -> mut volatile R
mut R -> volatile R
mut volatile R -> volatile R

R must be an eligible storage-accessing reference. No implicit conversion adds mutability, removes volatility, changes nested qualifiers or function signatures, or performs numeric, Boolean, character, or deep reference conversion. These conversions apply to bindings, arguments, results, assignments, equality, and control-flow joins.

Explicit conversion is allowed only for pairs in this table after alias resolution:

Source Target Safe Meaning
Any type Same resolved type Yes No operation.
Integer Integer Yes Numeric integer conversion.
Integer f32 or f64 Yes Convert numeric value.
f32 or f64 Integer Yes Truncate toward zero.
f32 or f64 f32 or f64 Yes Convert at target precision.
char Integer Yes Convert its 32-bit unsigned numeric value.
Storage-accessing reference Same reference with outer mut removed and/or volatile added Yes Preserve representation while restricting access.
Raw pointer usize or isize Yes Convert the address at pointer width.
usize or isize Raw pointer No Construct a target pointer.
usize or isize Address-valued aggregate No Construct a backing address.
Raw pointer Another raw-pointer type No Preserve address while changing pointer type or access.

Unsafe conversions require lexical unsafe. They do not validate alignment, lifetime, provenance, pointee type, or writability. Retyping may add mut, remove volatile, or change nested pointer types, but it cannot make read-only storage writable. No conversion turns immutable aggregate or slice access into mutable access. Explicit numeric conversion uses values, never bit reinterpretation.

Memory, bindings, and storage

Storage is a region with an address, size, alignment, duration, and contents capable of holding value representations. Its data are the bytes it currently holds, not the source syntax that produced them. Allocation creates a storage region with a distinct live identity. A load reads a stored value representation through a reference; a store replaces data in existing storage through a writable reference.

A storage slot holds one value representation. Aggregate backing holds array elements, struct fields, or an enum tag and payload. Copying a reference copies neither its region nor its data. The lifetime of a binding and the duration of referenced storage are independent.

A binding is an immutable association between a name and a value within a scope. An initializer is an expression or aggregate form supplying the initial value or contents of a binding or storage region. let name: T = expression evaluates the expression once before the name enters scope, requires compatibility with T, then creates a binding of type T to that value. let name = expression does the same with the expression’s type. It performs no allocation by itself. Binding an aggregate reference creates an alias without copying backing or adding capability.

A runtime aggregate initializer is array or nonempty struct syntax with an expected aggregate type, or an enum constructor used as an expression. It allocates fresh backing at its execution site, constructs it, and yields the expected aggregate reference, including outer qualifiers supplied by context; construction writes are volatile when that reference is. Writable scalar slots come from aggregate storage or another storage reference; the language has no address-of or local scalar allocator.

An assignment is a store or constructor application through existing writable access. A constructor is destination-dependent aggregate syntax that writes an aggregate into existing backing without allocating the destination:

Destination Right side Effect and result
mut *T, non-void T Compatible value Store into the existing slot; yield stored T.
Mutable aggregate mut A Matching aggregate syntax Apply a constructor in existing backing; yield void.
Mutable aggregate mut A Ordinary aggregate value Invalid; aggregate copy assignment does not exist.
mut *A Matching aggregate initializer Allocate fresh A, store its reference in the slot, and yield A.

Aggregate syntax stays matching syntax when grouped, as an unsafe body, or as a block without entries. An assignment never changes a binding or its destination reference. Aggregate construction preserves outer backing identity. Nested aggregate syntax allocates separately backed children and stores their references; replacing a child reference does not change an earlier alias to the old child. Selecting an aggregate child automatically loads its declared reference, so applying a constructor to that selection updates the selected child’s backing.

Initialization and constructor assignment are not atomic. If evaluation exits early, completed writes remain.

Runtime aggregate backing lasts until the allocating function returns. Each evaluation creates fresh backing, including inside loops and nested constructor updates; leaving a block or iteration and replacing a child do not reclaim it. Repeated loop allocation may therefore exhaust stack storage. Statics and literal strings have module runtime duration.

The language performs no ownership, exclusivity, provenance, alias, or lifetime analysis. Escaping a reference does not extend storage duration.

Numeric behavior

Integer and char addition, subtraction, multiplication, and negation wrap at their representation width; unsigned negation is valid. Decimal floating literals and arithmetic round to nearest, ties to even; floating exceptions do not trap.

Integer conversion uses destination width: narrowing keeps low bits, widening sign-extends a signed source and zero-extends an unsigned source or char, and equal-width conversion yields the destination modular value. Integer-to-float rounds to nearest, ties to even. Float-to-integer truncates toward zero and has no defined result for NaN, infinity, or an out-of-range truncated value. f32 to f64 extends precision; f64 to f32 rounds and may produce infinity. Integer-to-char, and numeric conversion to or from bool, do not exist.

Signed division truncates toward zero and remainder has the dividend’s sign. Unsigned and char division use unsigned values. Right shift is arithmetic for signed integers and logical for unsigned integers and char; left shift discards high bits. A shift count must be nonnegative and less than the operand width. Integer or char division or remainder by zero, signed minimum divided by or remainder by -1, and an invalid shift have no defined result and require no runtime check.

Floating operations may produce infinities and NaNs. Floating remainder uses fmod semantics: a finite dividend modulo an infinite divisor is the dividend; zero divisor, infinite dividend, or NaN operand produces NaN; otherwise a nonzero result has the dividend’s sign. Equality is false and inequality true when either operand is NaN; ordered comparisons with NaN are false.

Raw pointer and memory behavior

Packed-field pointers and raw-pointer loads and stores permit unaligned addresses. Index and range operands are converted to pointer width by zero-extending narrower accepted unsigned types or discarding high bits of wider ones. Slicing only constructs a descriptor.

Pointers, slices, and aggregate addresses may alias and escape. Conversions preserve only an address. Dereferencing an invalid, null, dangling, or incorrectly typed pointer; writing read-only storage; using an invalid index or range; or accessing an expired slice is undefined behavior. No access is atomic or synchronizing. There is no pointer arithmetic.

Expressions and Control Flow

This section defines only MIR and LIR. Every optional or HIR-only form has already been reduced to the explicit core language described here.

Static typing

In the table below, N is one numeric type, I one integral type, T any valid type, and A one address-valued aggregate. Repeated letters require equal resolved types, except for permitted outer reference-qualifier conversion.

Expression Required types Result
-value N N
a + b, a - b, a * b, a / b, a % b (N, N) N
a & b, a | b, a ^ b, a << b, a >> b (I, I) I
~value I I
a < b, a > b, a <= b, a >= b (N, N) bool
a == b, a != b Compatible (T, T) bool
!value bool bool
target += value, -=, *=, /=, %= target: mut *N, value: N N
target &= value, |=, ^=, <<=, >>= target: mut *I, value: I I
target &&= value, ||= target: mut *bool, value: bool bool

The eager operators listed here and compound assignments are core operations, not syntactic sugar. The checker resolves each eager operator to the corresponding predefined operation for its exact operand type and records that resolution in LIR; it does not rewrite the expression. User-associated functions never participate in operator resolution.

Operator forms Resolved predefined operations
-a, !a, ~a T::neg(a), bool::not(a), T::bit_not(a)
a + b, a - b, a * b, a / b, a % b T::add(a, b), T::sub(a, b), T::mul(a, b), T::div(a, b), T::rem(a, b)
a & b, a | b, a ^ b T::bit_and(a, b), T::bit_or(a, b), T::bit_xor(a, b)
a << b, a >> b T::shl(a, b), T::shr(a, b)
a < b, a <= b, a > b, a >= b T::lt(a, b), T::le(a, b), T::gt(a, b), T::ge(a, b)

Equality and inequality are primitive. They support compatible values whose types define equality: primitive values, raw pointers, function references, tuples, slices, and address-valued aggregates. Assignment, dereference, conversion, indexing, slicing, and projections are also primitive.

Compound Boolean assignment first evaluates and loads its target, conditionally evaluates the right operand, then stores the selected Boolean result. As with && and ||, a Never right operand leaves its type bool.

Literal typing

Numeric literals are untyped; the checker gives each a type. An integer literal takes its expected type when that is an integer type, and a floating literal when that is f32 or f64; otherwise an integer literal has type i32 and a floating literal f32. An integer literal never takes a floating or char type. An integer literal must denote a value of its type; usize and isize literals are checked against their 64-bit carrier, and target width is checked by the emitter. A floating literal must round to a finite value of its type. Unary - is not part of a literal, so a signed minimum cannot be written as one literal.

An untyped expression is a numeric literal; - or ~ applied to an untyped expression; an eager arithmetic, bitwise, or shift operation on two untyped expressions; a tuple expression of untyped elements; or one of these grouped, as an unsafe body, or as a block without entries. An untyped expression therefore names nothing and cannot jump. Besides the contexts under Contextual initializers, expected types reach operands as follows:

Position Expected type
Operand of prefix - or ~ When untyped, the operation’s expected type if the operator accepts it.
First checked operand of an eager binary operator When untyped, the operation’s expected type if the operator accepts it.
Second checked operand of an eager binary operator or ordering The first operand’s type.
Second checked operand of == or != When untyped, the first operand’s type.
Right operand of a compound assignment The target’s stored type.
Index or slice bound usize when untyped.
Conversion argument When untyped, the target if numeric, or usize for a raw-pointer or aggregate target.
Conditional branch checked second, without expected type When untyped, the type of the branch checked first.
Untyped switch arm, without expected type The join of the arms checked before it.

A binary operator, ordering, or equality checks its right operand first when only the left is untyped, and otherwise its left first; an ordering gives its first operand no expected type. When the first checked operand or an assignment target is Never, the other side has no expected type. A conditional without an expected type checks its else branch first when only its if branch is untyped, and a switch without one checks untyped arms after all others.

let name = expression checks its initializer without an expected type, and the binding has the initializer’s type, including outer qualifiers; a Never initializer is invalid. A type is inferred only where the value is written: a later use of a binding never changes it, and break operands and loop alternatives take only the loop’s expected type.

Contextual initializers

Array and nonempty struct initializers require a known expected aggregate type. An enum constructor determines its own type; context supplies only outer qualifiers. Expected types flow through exactly these contexts:

Context Expected type
let name: T = expression Written T; let name = expression supplies none.
Function argument Declared parameter type.
Assignment through mut *T Stored T, not the pointer type.
Aggregate constructor assignment Destination aggregate type.
Aggregate member or enum payload Exact declared member type.
Tuple with known matching tuple type Corresponding element type.
Explicit return or function body Declared result type.
Final block expression Surrounding result expectation.
if branches or switch arms Surrounding result expectation.
while or for break operands and else, never the discarded body.
loop break operands, never the discarded body.
unsafe body or grouped expression Surrounding expectation.

The checker does not infer an aggregate shape from initializer contents. A projection gives no expected type to its base, and one branch provides context to another only under Literal typing. Context changes the type of numeric literals alone.

Blocks and bindings

Every MIR block has a final expression. In a; b, a is evaluated and discarded before b, whose result becomes the block result. Semicolons occur only in blocks. { null } is the canonical empty block and has type void; it is not an aggregate initializer.

A let evaluates its initializer, creates its binding, then evaluates the remaining block under that binding. The initializer must be compatible with a written type; without one the binding takes the type given under Literal typing. If it does not complete, no runtime binding is created.

Earlier expressions and source following a jump are still resolved and checked. Their inability to run does not exempt them from any static rule.

Calls, constructors, and conversions

A call evaluates its callee and then its arguments left to right. Arity is exact; arguments are positional and must be compatible with declared parameter types.

If the callee is Never, the call is Never. Every argument is still checked without parameter-provided expected type; an argument requiring such context is invalid. Arity and parameter compatibility do not apply because no signature exists.

A qualified enum variant path is resolved as an enum constructor before associated-function lookup. For any other application whose callee is a path, lexical value lookup comes first. If the path names a value, the application is a call. If it names no value but resolves as a type T, T(value) is an explicit conversion. Otherwise it remains an ordinary call and must have a callable callee. A conversion target must be a type path; a complex type must first be named by an alias.

A conversion takes exactly one evaluated value, whose numeric literals are typed under Literal typing. Aggregate syntax is not a value with a source type and cannot be its argument. The target is validated even when the argument cannot complete. Conversion permissions are defined under Conversions.

A runtime aggregate initializer allocates fresh backing. Array and struct forms evaluate members in source order; an enum constructor evaluates its payload, clears its destination, writes the tag, then stores the payload. A constructor uses the same order in existing backing. A jump during construction leaves completed writes in place.

Assignment

Simple assignment evaluates its destination once, then its right side, then performs the store or constructor. Its result is defined under Memory, bindings, and storage.

The assignment target expression itself denotes the writable reference. Prefix * performs a load and is not used to select the destination slot.

An eager compound assignment evaluates its target once and retains that writable reference, loads the stored value, evaluates the right operand, applies the selected eager operation, stores the result, and yields it. A Boolean compound assignment instead follows the conditional rule under Static typing. On any path where an evaluated operand does not complete, remaining work and the store are skipped.

Unsafe blocks

unsafe { body } evaluates and yields body; it adds no runtime operation. Its lexical context permits only conversions marked unsafe in the conversion table. It neither validates later access nor makes an unlisted conversion valid. The context includes nested expressions but does not cross a lifted anonymous-function boundary. Dereference, assignment through a writable pointer, and pointer-to-integer conversion do not themselves require unsafe.

Control flow

Every control body is a block. A completed if, while, loop, for, or switch is an ordinary operand. There is no special else if form; nesting places another if inside the else block.

Static non-yielding classification

Never is the internal bottom type of an expression with no normal completion; it cannot be written as a source type. return, break, and continue have type Never.

A strict expression is Never when an operand that must complete before it can produce a value is Never. Strict forms include prefix and eager binary operations, comparisons, calls and arguments, conversions, projections, assignments, aggregate and tuple construction, a sequence’s left operand, a binding initializer, and control conditions. All later source is still checked. A Never switch scrutinee or for iterable is invalid because it provides no type required by that construct.

Branch joins ignore Never arms. These rules are static propagation, not constant evaluation or general reachability analysis. Merely containing a runtime jump does not make an enclosing expression Never when it also has a normal path. A break is reachable unless, inside its target loop, it is checked after a Never operand that must complete first; an unreachable break is still checked against the loop’s expected type but does not contribute to its result.

Return

return value evaluates value and exits the current function. Every MIR return has an operand compatible with the declared result type. Returning newly allocated stack-backed aggregate storage is type-correct but does not extend its duration.

Conditional

if condition { left } else { right } requires a bool condition and evaluates only the selected branch. MIR always has an else. Value-producing branches must have compatible types; one Never branch leaves the other’s type, both Never branches make the conditional Never, and a Never condition makes the whole conditional Never. A join keeps outer mut only from both inputs and outer volatile from either.

Loops

loop block repeats unconditionally and discards each normally completed body value. Its result type is the common type of reachable break operands; with no normally completing break it is Never. continue starts the next iteration.

while condition block else alternative requires a bool condition tested before each iteration. Body values are discarded. A false condition evaluates alternative; break value exits without evaluating it. Every reachable break and the normal alternative must be compatible. A Never condition provides no normal termination path.

for name in iterable block else alternative evaluates an array or slice once, fixing its count, then visits elements in ascending index order. name is an immutable inferred binding of the exact element type and is scoped to the body; it receives the element value, not its slot, loaded when its iteration starts. Outer access qualifiers of the iterable do not move into the element type, though a volatile iterable makes each element load volatile. Normal exhaustion, including an empty slice, evaluates the alternative. continue advances to the next element; break bypasses the alternative. Result typing otherwise matches while. A Never iterable is invalid because it supplies no element type.

The body of loop, and the condition and body of while, are inside that loop’s jump scope. A for body is inside its scope; its iterable and every loop alternative are outside. Each break and continue targets the innermost enclosing loop in the same function and is invalid without one.

Switch expressions

switch requires a non-Never enum scrutinee, evaluates its address once, and selects an arm by effective variant ID. It loads the tag once and, on entering an arm that binds it, the payload once; both loads are volatile when the scrutinee is. A case names a variant exactly once. A payload binding is optional, scoped to its arm, and has the exact payload type; unit variants cannot bind a payload.

Without default, every variant must be covered. With default, variants may be omitted, but default is invalid after all variants are named. Unknown or duplicate cases and duplicate defaults are invalid. A written default also receives an invalid discriminator read from raw memory; an invalid discriminator without default is undefined behavior.

Only the selected arm runs. Value-producing arms use conditional compatibility and Never rules, and surrounding expected type flows into every arm. The next case, default, or closing brace terminates an arm; no separator is written.

Evaluation order

Form Order
Eager binary operator Left, then right.
Assignment Target once, right side, then store or constructor.
Eager compound assignment Target, load, right side, operation, store.
Function call Callee, then arguments left to right.
Tuple construction Elements left to right.
Tuple projection Tuple value, then extraction.
Block sequence Entries in order, then final expression.
let Initializer, bind, then remaining block.
Explicit conversion Argument, then conversion.
unsafe Body.
Dereference Pointer, then load.
Field selection Base, then projection or load.
Slice length Descriptor, then count extraction.
Aggregate initializer Allocate, construct contents, yield reference.
Array or struct construction Members in source order.
Enum construction Payload, destination initialization, tag, payload store.
Indexing Base, then index.
Slicing Base, supplied start, supplied end.
if Condition, then selected branch.
switch Scrutinee, then selected arm.
while Condition before each body; alternative only when condition is false.
loop Body repeatedly until a reachable break.
for Iterable once, elements in index order, alternative on exhaustion.
&&=, ||= Target and load, conditionally right, then store.
return Operand, then function exit.
break Operand, then target-loop exit.
continue Advance to the target loop’s next iteration.

All source is checked regardless of runtime selection. Static initializers have no runtime evaluation order and may refer forward.

Syntactic Sugars and Macros

HIR reductions

Syntactic sugar is an HIR construct whose meaning is defined by replacement with core-language constructs. The reducer applies each syntactic replacement before producing MIR. Type and Never consequences shown in the table are enforced later by the checker; the reducer does not determine them.

HIR form MIR form and type
Omitted function result Explicit void.
Bare return or break The same jump with operand null; result remains Never.
Empty block or block ending in ; Block ending in explicit null; type void unless an earlier strict entry is Never.
Empty block entry ; An explicit discarded null;.
a && b if a { b } else { false }; both operands must be bool.
a || b if a { true } else { b }; both operands must be bool.
if without else else { null }; the written branch must be void or Never.
while or for without else else { null }; normal completion type void.
Enum IDs omitted for every variant Explicit zero-based source-position IDs; a mixed enum is rejected.
Self The enclosing expanded impl target.
Self::f <target>::f, requiring a path spelling for the target.
Anonymous function A private lifted function plus an ordinary function reference of the written function type.
Macro invocation The expression or type HIR returned by the predefined macro, recursively reduced.

Generated names are deterministic and cannot collide with source names. Relative and absolute paths retain their spelling and resolution context. Lifted anonymous functions join the enclosing module after Self replacement.

Self is valid only within a function contained by an impl block and is invalid in the block’s target. Its replacement is resolved in the containing module. Self::f is invalid when that target has no predefined-name or alias-path spelling, although Self remains valid as a type there.

An anonymous function does not capture. Its body may use only its parameters, its own local bindings, and visible module items. An enclosing unsafe context does not enter the lifted body.

Operators other than && and ||, compound assignments, calls or conversions, contextual aggregate forms, loops, switches, and unsafe are not sugar. They remain core nodes from HIR through LIR, with semantic facts attached by the checker.

For either short-circuit reduction, the left operand is evaluated first and the right only on the selected path. A Never left operand makes the result Never; a Never right operand leaves the other branch as a completion path.

Compile-time macros

@name(arguments) is accepted only in expression and type contexts, never as an item. The language provides no syntax for defining macros. The predefined macros are @CType and @sizeOf; every other name is invalid.

Expansion recursively replaces each invocation with HIR matching its context. Macro-specific argument validation and macro-local name, alias, type, and ABI queries belong to the reducer; they do not constitute general MIR checking. General name resolution and all core-language type checking remain exclusive to the checker. No invocation reaches MIR.

Expansion permits 32 invocations total along one path and rejects the 33rd. Counting starts independently for every function body, parameter type, result type, alias definition, static annotation, static initializer, and impl target. Nested arguments and generated invocations count; ordinary HIR nesting does not. An invocation used as a macro argument is replaced by its result, type or expression, before the enclosing macro applies.

Macro arguments are not generally name-checked or type-checked before substitution. A complete qualified or absolute path is stored as a type unless parenthesized. Self, mut, volatile, function, array, slice, struct, enum, and packed struct forms force a type argument; a pointer is type-forcing when its pointee is, and a tuple when at least one element is. Other arguments, including a bare relative name, are expressions. A type-oriented macro may reinterpret a path expression, its dereference, or a tuple wholly composed of reinterpretable expressions as the corresponding type form. A qualified or absolute path nested inside a tuple does not by itself make that tuple type-forcing.

@CType

@CType("C declaration") takes one string in a type context. It trims the decoded string, collapses each internal whitespace run to one space, and matches case-sensitively. It maps supported C scalar and pointer declarations to an ordinary l type according to the selected C ABI; this specification contains no machine-specific mapping table.

char and unsigned char map to u8, signed char to i8, _Bool to bool, size_t to usize, ptrdiff_t to isize, float to f32, and void to void. The primitive each remaining spelling maps to is implementation-defined per target; it must have the width, signedness, and floating representation the selected C ABI gives that C type. Where plain C char is signed, u8 agrees with it across calls only for values 0 through 127.

The accepted scalar spellings and portable pointer-declarator grammar are fixed. A pointer declarator is tokenized at spaces and each *:

c-scalar = "char" | "unsigned char" | "signed char" | "_Bool"
         | "short" | "short int" | "signed short" | "signed short int"
         | "unsigned short" | "unsigned short int"
         | "int" | "signed" | "signed int" | "unsigned" | "unsigned int"
         | "long" | "long int" | "signed long" | "signed long int"
         | "unsigned long" | "unsigned long int"
         | "long long" | "long long int" | "signed long long" | "signed long long int"
         | "unsigned long long" | "unsigned long long int"
         | "size_t" | "ptrdiff_t" | "float" | "double" | "void"
c-qualifier = "const" | "volatile"
scalar-word = /* one word in an accepted scalar spelling */
c-pointer-declarator = (c-qualifier | scalar-word)+ ("*" c-qualifier*)+

Removing qualifiers from the prefix and joining its scalar words with spaces must leave exactly one accepted scalar spelling. Prefix qualifiers apply through the first pointer; qualifiers following a * apply when that pointer is another pointer’s pointee. At each pointee level, absence of const adds mut, const leaves access immutable, and volatile adds volatile. Qualifiers after the final * have no type-level effect. Repeated qualifiers are idempotent. The result carries no C-specific marker.

@sizeOf

@sizeOf(T) is expression-only and takes one type. The reducer expands nested macros, substitutes applicable Self, resolves aliases in the invocation’s module, and queries the selected ABI. It produces usize(n), the conversion of an integer literal n equal to the language value size of T.

void has size zero; qualifiers do not affect size; an aggregate measures its backing; an aggregate-valued member measures one reference slot; and a raw pointer measures one pointer. Macro-local resolution rejects an unknown alias, an invalid recursive alias cycle, an array length that is not a positive integer, and mixed omitted and written enum IDs; the argument is otherwise unchecked because it never reaches MIR, while the resulting MIR is checked normally.

Prelude

Predefined names

The fifteen primitive names are predefined identifiers, not keywords:

u8 u16 u32 u64 usize i8 i16 i32 i64 isize f32 f64 char bool void

Each is bound in every module and resolves as a one-segment type path. The binding does not participate in longer or absolute paths. No item, associated function, parameter, function-type label, local binding, field, or variant may use a predefined name, so these bindings cannot be shadowed.

Predefined functions

The following associated functions exist in every module. Each occurrence of T in one signature is the same resolved primitive type.

Targets Functions
u8 u16 u32 u64 usize i8 i16 i32 i64 isize char f32 f64 neg(T) T; add(T, T) T; sub(T, T) T; mul(T, T) T; div(T, T) T; rem(T, T) T
u8 u16 u32 u64 usize i8 i16 i32 i64 isize char f32 f64 lt(T, T) bool; le(T, T) bool; gt(T, T) bool; ge(T, T) bool
u8 u16 u32 u64 usize i8 i16 i32 i64 isize char bit_not(T) T; bit_and(T, T) T; bit_or(T, T) T; bit_xor(T, T) T; shl(T, T) T; shr(T, T) T
bool not(bool) bool

They are ordinary function values when named explicitly. A source impl may not declare one on a target that provides it; the same name on another target is ordinary. The eager operators listed under Static typing select these predefined meanings by exact operand type but remain core operator nodes; defining an associated function cannot overload an operator or add a coercion.

Predefined macros

The prelude provides @CType and @sizeOf with the reductions specified under Compile-time macros. They are HIR-only and are absent from MIR and LIR.

Syntax

Grammar notation

Form Meaning
name A production defined by this specification
"text" or 'text' Literal UTF-8 text between matching quotes
a b a followed by b
a | b Either a or b
(a) Grouping
a? Zero or one occurrence
a* Zero or more occurrences
a+ One or more occurrences
a{m,n} From m through n occurrences
/* ... */ A prose definition or restriction, not language input

Alternation has the lowest precedence, concatenation the next higher precedence, and postfix repetition the highest. Parentheses override that order. Grammar defines syntax only; all applicable lexical, resolution, typing, and semantic rules also apply.

Source text and tokens

Source text is a sequence of Unicode scalar values encoded as UTF-8. Implementations must not normalize it; canonically equivalent sequences remain distinct. The complete lexical definition is one grammar:

ASCII-letter       = /* U+0041 through U+005A or U+0061 through U+007A */
ASCII-digit        = /* U+0030 through U+0039 */
ASCII-alphanumeric = ASCII-letter | ASCII-digit
decimal-digit      = ASCII-digit
binary-digit       = "0" | "1"
hexadecimal-digit  = decimal-digit | /* U+0041 through U+0046 or U+0061 through U+0066 */
character          = /* any Unicode scalar value */
newline            = /* U+000A */
Unicode-whitespace = /* U+0009 through U+000D, U+0020, U+0085, U+00A0, U+1680,
                        U+2000 through U+200A, U+2028, U+2029, U+202F, U+205F, or U+3000 */

token = whitespace
      | documentation-comment
      | line-comment
      | keyword
      | boolean-literal
      | null-literal
      | identifier
      | numeric-literal
      | character-literal
      | string-literal
      | c-string-literal
      | operator-or-punctuation

whitespace            = Unicode-whitespace+
non-newline-character = /* any character except newline */
line-comment          = "//" non-newline-character*
documentation-comment = "///" non-newline-character*

identifier = (ASCII-letter | "_") (ASCII-letter | ASCII-digit | "_")*

keyword = "pub" | "fn" | "if" | "else" | "while" | "loop" | "for" | "in" | "switch"
        | "case" | "default" | "break" | "continue" | "let" | "static" | "mut"
        | "volatile" | "return" | "type" | "Type" | "namespace" | "impl"
        | "struct" | "enum" | "packed" | "unsafe" | "extern" | "use"
        | "Self" /* HIR-only use: replaced before MIR */
boolean-literal = "true" | "false"
null-literal    = "null"

decimal-digits     = decimal-digit (decimal-digit | "_")* decimal-digit | decimal-digit
hexadecimal-digits = hexadecimal-digit (hexadecimal-digit | "_")* hexadecimal-digit | hexadecimal-digit
binary-digits      = binary-digit (binary-digit | "_")* binary-digit | binary-digit

integer-literal = decimal-digits | "0x" hexadecimal-digits | "0b" binary-digits
float-literal   = decimal-digits ("." decimal-digits exponent? | exponent)
exponent        = ("e" | "E") ("+" | "-")? decimal-digits
numeric-literal = integer-literal | float-literal

character-content = /* any character except U+0027, U+005C, or newline */
character-literal = "'" (character-content | character-escape) "'"
character-escape  = "\n" | "\r" | "\t" | "\0" | "\\" | "\'" | unicode-escape
unicode-escape    = "\u{" hexadecimal-digit{1,6} "}"

string-character           = /* any character except U+0022, U+005C, or newline */ | string-escape
multiline-string-character = /* one character other than U+005C that does not begin the closing delimiter */
                           | string-escape
string-escape      = "\n" | "\r" | "\t" | "\0" | "\\" | "\'" | '\"' | unicode-escape
single-line-string = '"' string-character* '"'
multiline-string   = '"""' multiline-string-character* '"""'
string-literal     = single-line-string | multiline-string
c-string-literal   = "c" (single-line-string | multiline-string)

operator-or-punctuation = "@" /* HIR-only macro introducer; absent from MIR */
                        | "::" | "(" | ")" | "{" | "}" | "[" | "]" | "," | ":" | ";" | "." | ".." | "="
                        | "+" | "-" | "*" | "/" | "%" | "+=" | "-=" | "*=" | "/=" | "%="
                        | "==" | "!=" | "<" | ">" | "<=" | ">="
                        | "&&" | "||" /* HIR-only short-circuit operators; reduced before MIR */
                        | "&&=" | "||=" | "!"
                        | "&" | "|" | "^" | "~" | "<<" | ">>" | "&=" | "|=" | "^=" | "<<=" | ">>="

Except for numeric candidates, the lexer takes the longest valid token at each position. A spelling equal to a keyword or literal word is reclassified as that word. c immediately followed by a string delimiter begins a C string. Operators and punctuation use longest match; ::: is :: followed by :, so trivia must separate an annotation colon from a following absolute path.

The token stream is the ordered, lossless decomposition of one or more source files into tokens, preserving file boundaries and origins. Every character of valid source text belongs to exactly one token. Each token retains its exact UTF-8 lexeme and a half-open byte range. Tokens are contiguous, no end token is appended, and concatenating their lexemes reconstructs the source exactly. The parser discards whitespace and comments before applying HIR grammar; they may separate any two syntactic tokens. An item’s range runs from its first through last syntactic token.

Each maximal nonempty whitespace run is one token. A line comment ends before newline or at end of input; newline belongs to following whitespace. /// begins a documentation comment and every other // a line comment. Both are discarded by the parser; documentation comments are reserved for item documentation. Block comments do not exist: /* is / followed by *.

Identifiers are case-sensitive and ASCII-only. Non-ASCII scalar values occur only in whitespace, comments, character literals, and strings. Keywords and literal words cannot be names. Each has its own token kind. Primitive names are identifiers. Type is only the annotation of a type alias; lowercase type is reserved and begins no production. Boolean literals have type bool, and null has type void.

After an ASCII digit, the numeric candidate consumes the longest numeric-literal prefix. A decimal . enters the candidate when followed by a decimal digit or _ and the preceding syntactic token is not .. A following e or E enters with an optional sign, and immediately following identifier characters, optionally after one _, are consumed as a suffix. The whole candidate is validated as one token or one lexical error; malformed text is not split, and a candidate with a suffix is invalid. _ may repeat between digits, but a digit must occur at each end of a digit sequence. - is always a separate operator.

A character escape must decode to one Unicode scalar value; surrogates and values above U+10FFFF are invalid. Escapes in strings are decoded before semantic use while the token retains its original spelling. Three opening quotes select a multiline string.

An ordinary string literal has immutable type [u8]. Its elements are the UTF-8 bytes of decoded text in module-duration read-only storage, with no trailing zero; indexing observes bytes and the descriptor count is byte length. A C-string literal has immutable type *u8 in module-duration read-only storage, with exactly one appended zero byte; embedded zeros remain. Each literal occurrence has its own storage, reused by every evaluation of it, so equal literals at different sites have distinct addresses.

Module grammar

Module and expression grammar is exclusively HIR grammar. Comments identify forms that the reducer removes before MIR.

module          = use-declaration* item*
use-declaration = "use" identifier ";"

item = function-item
     | static-variable
     | type-alias
     | namespace-declaration
     | impl-block

function-item = "pub"? "extern"? signature ";"
              | "pub"? signature block
namespace-declaration = "namespace" identifier "{" use-declaration* item* "}"
impl-block             = "impl" type-expression "{" impl-function* "}"
impl-function          = "pub"? signature (";" | block)
static-variable        = "pub"? "static" identifier ":" type-expression ("=" static-initializer)? ";"
                       | "pub"? "extern" "static" identifier ":" type-expression ";"
static-initializer     = assignment-rhs
type-alias             = "let" identifier ":" "Type" "=" type-expression ";"

/* HIR only when the result is omitted: the reducer inserts void. */
signature  = "fn" identifier "(" parameters? ")" type-expression?
parameters = parameter ("," parameter)*
parameter  = identifier ":" type-expression

type-expression = "Self" /* HIR only: replaced with the enclosing impl target */
                | macro-invocation /* HIR only: expanded */
                | item-path
                | raw-pointer-type
                | mutable-type
                | volatile-type
                | function-type
                | tuple-type
                | fixed-array-type
                | slice-type
                | struct-type
                | enum-type

item-path        = "::"? identifier ("::" identifier)*
raw-pointer-type = "*" type-expression
mutable-type     = "mut" type-expression
volatile-type    = "volatile" type-expression
function-type    = "fn" "(" parameters? ")" type-expression
tuple-type       = "(" type-expression "," type-expression ("," type-expression)* ")"
struct-type      = "struct" "{" fields "}"
                 | "packed" "struct" "{" fields "}"
fields           = field ("," field)* ","?
field            = identifier ":" type-expression
enum-type        = "enum" "{" enum-variants ","? "}"
enum-variants    = enum-variant ("," enum-variant)*
/* HIR only when the ID is omitted: the reducer inserts the source-position ID. */
enum-variant     = identifier (":" type-expression)? ("=" numeric-literal)?
fixed-array-type = "[" type-expression ";" numeric-literal "]"
slice-type       = "[" type-expression "]"

/* HIR only: every invocation is expanded before MIR. */
macro-invocation = "@" identifier "(" macro-arguments? ")"
macro-arguments  = macro-argument ("," macro-argument)*
macro-argument   = expression | type-expression

A primitive type is a one-segment item-path. A function signature’s result follows ); omission reduces to void. A function type always writes its result, including void.

Expression grammar

expression = jump | assignment
binding    = "let" identifier (":" type-expression)? "=" assignment

/* HIR only when an operand is omitted: the reducer inserts null. */
jump = "return" assignment?
     | "break" assignment?
     | "continue"

block      = "{" block-body "}"
/* HIR only when the final expression is omitted: the reducer inserts null. */
block-body = block-entry* expression?
block-entry = expression ";" | binding ";" | ";" /* HIR only: reduced to discarded null */

/* HIR only when else is omitted for if, while, or for: the reducer inserts else { null }. */
control = control-head block ("else" block)?
        | "loop" block
        | switch-expression
control-head    = control-keyword expression | "for" identifier "in" expression
control-keyword = "if" | "while"

switch-expression = "switch" expression "{" switch-arm* "}"
switch-arm = "case" identifier ("(" identifier ")")? ":" expression
           | "default" ":" expression

assignment = logical-or
           | logical-or "=" assignment-rhs
           | logical-or compound-assignment assignment
assignment-rhs = assignment

aggregate-initializer = "[" initializer-elements? "]"
                      | "{" initializer-fields ","? "}"
initializer-elements = assignment-rhs ("," assignment-rhs)*
initializer-fields   = initializer-field ("," initializer-field)*
initializer-field    = identifier ":" assignment-rhs

compound-assignment = "+=" | "-=" | "*=" | "/=" | "%="
                    | "|=" | "^=" | "&=" | "<<=" | ">>="
                    | "&&=" | "||="

/* HIR-only operator tails: the reducer replaces || and && with conditionals. */
logical-or     = logical-and ("||" logical-and)*
logical-and    = bitwise-or ("&&" bitwise-or)*
bitwise-or     = bitwise-xor ("|" bitwise-xor)*
bitwise-xor    = bitwise-and ("^" bitwise-and)*
bitwise-and    = equality ("&" equality)*
equality       = relation (("==" | "!=") relation)*
relation       = shift (("<" | ">" | "<=" | ">=") shift)*
shift          = addition (("<<" | ">>") addition)*
addition       = multiplication (("+" | "-") multiplication)*
multiplication = unary (("*" | "/" | "%") unary)*

unary   = ("-" | "!" | "~" | "*") unary | postfix
postfix = primary postfix-tail
postfix-tail = ("." (identifier | tuple-index) | "[" subscript "]" | "(" arguments? ")")*
subscript = expression | expression? ".." expression?

primary = numeric-literal
        | character-literal
        | string-literal
        | c-string-literal
        | "true" | "false" | "null"
        | item-path
        | associated-path
        | anonymous-function /* HIR only: lifted by the reducer */
        | tuple-expression
        | aggregate-initializer
        | "(" expression ")"
        | block
        | control
        | macro-invocation /* HIR only: expanded */
        | "unsafe" block

/* HIR only: replaced by a lifted named function and function reference. */
anonymous-function = "fn" "(" parameters? ")" type-expression block
/* HIR only because Self is replaced before MIR. */
associated-path = "Self" "::" identifier
tuple-expression = "(" expression "," expression ("," expression)* ")"
tuple-index = numeric-literal
arguments   = expression ("," expression)*

Repetitions from logical-or through multiplication and in postfix-tail associate left. Assignment and prefix unary operators associate right. A jump is a complete expression, not a primary; parentheses can place one where a primary is required.

Every control head consumes an expression directly. Parentheses are ordinary grouping. Every body is a block, so a completed control expression composes without parentheses and an else belongs to the control whose block immediately precedes it. {} is an empty block, never an empty aggregate initializer. associated-path exists only for Self; every other associated path is an item-path.

ABI

Machine and ABI model

The selected target ABI supplies pointer width, endianness, pointer representation, scalar size and alignment, aggregate and stack layout, symbol decoration, and the platform C calling convention. This specification expresses all target-dependent behavior through that ABI and contains no machine or target list.

Fixed-width primitives retain their specified widths. usize, isize, raw pointers, function references, enum tags, and aggregate backing references use target pointer width. The emitter rejects a checked pointer-width literal, enum ID, fixed-array iteration count, string length, layout, or symbol that the selected ABI cannot represent. Generated code may call the target’s compiler-runtime or C math routines for operations without a native instruction, such as floating remainder or wide division.

@CType is the sole language facility for mapping supported C type declarations to l types. Its mapping follows the target C ABI as specified under Compile-time macros; after expansion, its result is an ordinary l type with no C-specific identity or behavior.

Calling convention and interoperability

Functions use the selected target’s C calling convention. An extern function declaration may therefore name a compatible externally supplied function without a language-specific adapter. The source declaration must match the linked definition; no C wrapper is generated and compatibility is not verified.

The selected C ABI governs extension, register, return, and indirect-call rules for every primitive type, whether written directly or produced by @CType; char crosses a call as C uint32_t and bool as _Bool. mut and volatile are access properties and never alter calling convention or stored representation.

An ordinary string is an immutable [u8] descriptor and is not a C string. A C-string literal is an immutable *u8 with one trailing zero and is intended for compatible C pointer parameters.

An address-valued l aggregate parameter or result carries one backing address and therefore corresponds to a C pointer, not a C aggregate passed by value. Tuples and slices use direct l composite representations; no compatibility with C structs is promised. Variadic functions, by-value C aggregates, and functions using a calling convention other than the target’s C calling convention have no general direct mapping and require a compatible external wrapper.

Language value layout

mut and volatile change no size, alignment, padding, representation, or calling convention. @sizeOf(T) reports the selected ABI’s language value size under these rules:

  • Fixed-array backing is a contiguous sequence of element representation slots. The array layer adds no inter-element padding beyond each element representation’s own size and alignment requirements.
  • A slice is an element storage reference followed by a pointer-width unsigned count, stored and passed as a direct value. Its size is twice pointer size.
  • An ordinary struct follows target struct layout, including field and tail padding. A packed struct preserves field order, removes struct padding, and has alignment one; accesses may be unaligned.
  • An aggregate-valued field or element stores one backing reference slot, not inline child backing. Raw pointers, function references, tuples, and slices use their direct representations.
  • An enum is a target-layout structure containing a pointer-width tag followed by payload storage whose size and alignment are the maximum required by any variant representation. A unit payload has size zero and alignment one. Padding aligns the payload and complete enum.
  • A tuple is a non-packed target-layout structure of its element representations. An aggregate element occupies one backing reference slot.
  • A function reference has pointer size. void has value size zero.
  • A stored bool occupies one byte for C _Bool compatibility; a load yields the byte’s lowest bit.

Static backing and runtime backing use the same language layout. Relocations and padding do not change a type’s ABI.

Linkage

A function or static definition without pub has internal linkage. A public definition is exported. Every bodyless function and static declaration without an initializer has external linkage, while pub independently controls source visibility. An externally supplied entity must have a compatible symbol, type, layout, calling convention, and access behavior.

An extern declaration uses its item name as its logical symbol, independent of its module path and package. Every other ordinary function and static item, definition or declaration, uses the symbol of its place: that of its package, its module path within that package, and its item name. An item of an interface belongs to the library’s package; every other item belongs to the package being compiled.

The symbol joins package name segments, module path segments within the package, and the item name with __; - in a package segment becomes _, and an unnamed package contributes no segment, so its root item’s symbol is its item name. The package name surrounds the root module for linkage only. Relative and absolute source paths still resolve from that package’s own root. Distinct places may share a symbol, as a_::b and a::_b or packages a--b and a/b do, and qualified symbols in an unnamed package are not a stable foreign interface. An extern symbol cannot contain adjacent underscores, so it never reaches a qualified or named-package definition.

Declarations sharing a symbol denote one external entity: all must be functions, or all statics, of the same resolved type. No definition may share its symbol with a declaration or another definition.

A named package prefixes main, so it does not expose the unqualified platform entry symbol. An executable entry point therefore requires the logical root symbol main without a package prefix.

Associated function symbols

An associated function symbol is independent of package and module names. It is the decimal character count of its canonical resolved-type encoding, followed by that encoding, __, and the function name. No ordinary symbol starts with a digit. Alias names never occur in the encoding; equal resolved types encode equally and distinct types differently. A predefined function used as a value has the same form of symbol; each object using it emits an identical mergeable definition.

Primitive encodings are:

Type Code Type Code Type Code
i8 a u8 h bool b
i16 s u16 t char c
i32 l u32 m f32 f
i64 x u64 y f64 d
isize i usize j void v

Complex encodings use unsigned decimal integers without leading zeros:

Angle brackets below enclose metavariables, postfix * and + have the notation meanings from Grammar notation, spaces are separators and are not emitted, and tags, _, and decimal integers are emitted literally.

Type Encoding
Tuple T <type>+ E
Fixed array A <length> _ <type>
Slice S <type>
Raw pointer P <type>
Function F <result type> <parameter type>* E
Struct O (<name> <type>)* E
Packed struct Q (<name> <type>)* E
Enum N (<name> <payload type> <id> _)* E; unit payload is v
mut T M <type>
volatile T V <type>
Back-reference R <n> _, where n counts enclosing A, F, O, Q, or N types from zero, innermost first

Each field or variant name is prefixed by its character count. Combined qualifiers encode as M V <type>. A compound type structurally equal to an enclosing type uses a back-reference instead of unfolding, making recursive structural types canonical. The logical symbol alphabet is ASCII letters, digits, and _; target object formats may apply their ordinary external decoration.