Learn Landin in Y minutes
Ada, but small. Zig, but sweeter. One systems language from 32 KB to 32 TB*. Move fast, keep the pointers, and let the compiler tell you when you are being an idiot.
Named after Peter Landin, who coined the term "syntactic sugar" and wrote "The Next 700 Programming Languages". This one is the 701st.
Version 0.2.5 — diagnostics and repairs. 0.1.0 was the first version of the specification proper, arrived at over seventeen pre-release revisions, four prototype programs and two outside reviews; 0.2.0 sealed the first implementation roadmap that followed, over which a working compiler was built and the specification was held to what that compiler could be made to do. 0.2.1 arranged the specification by subject under a second roadmap and added the rules an outside review of that work found unwritten or wrong — D243 to D245 in spec.md, and where Darwin puts a small composite on the stack [1975]. 0.2.2 gave [1630]'s assembly typed operands bound to registers, D248, and lowered it on Cortex-M0, x86-64 and arm64; a refusal by name says what the form is, D246; and a routine or struct holds at most a stated number of what it declares, D247. 0.2.3 was the compiler as a tool: a warning is the compiler's judgement with the exact fix that settles it, D251; a source has one layout, which refine fmt gives it, D252; an editor is answered by refine lsp, past a broken routine body, D253; and a doc comment is about the declaration directly below it, [2000] and D254. A UTF-8 index decodes the codepoint it selects, D249, and a range selection is a view and not a place, D250. 0.2.4 added Linux arm64, the standard AAPCS64 with an unsigned plain char, D256; a build assumes a CPU feature level that changes no layout, D255, and targets the compiler's own host unless another is named, D257. A traversal that can fail is written try for, [0960] and [1320]; a capability parameter selects a provider without excluding new roots, D258; and a UTF-8 ordinal index spans the whole text view, D259. 0.2.5 keeps a parse's mistake inside its construct and suppresses the reports caused by it, D260; reports a refused statement as the smallest change that mends it and offers that repair to tools, D261; refuses an import without a root at the import, D262; and lets a checker refusal poison what it refused, D263. That history is kept in a separate archive; the part of it with lasting value is at the end of this file, under WHAT WAS TRIED AND DROPPED.
Numbering: [NNNN] is stable, which is the point of it — an insert never renumbers anything, so the order things are read in and the order of the numbers need not agree. They no longer do. Both documents are arranged by subject rather than by the order things were found, so [1000] is read before [0900] here and [1950] sits beside [1890] in spec.md, and that is the numbering working rather than failing. Gaps of ten leave room. Sections carry titles and no numbers, because nothing cites a section. Refer to decisions by number.
This file explains the language and does not decide it. spec.md is the normative document: it holds the grammar of the kernel the compiler enables today, the rules this file left unsaid, and the register of decisions taken while implementing them. Where the two could be read differently, spec.md decides. Everything still open, every implementation dependency and every disposition is owned by ROADMAP.md, not by a second list here.
*32 TB is an unverified goal. What it measures at the hosted end, and what evidence would prove it, are still open, and nothing hosted today establishes it. Terabytes may vary.
COMMENTS
These three are shown by being written, so each is kept exactly as it appears in a program: the marker is the comment.
DECLARATIONS
Full form: name, type, value
Full form: name, type, value. Immutable by default.
greeting: utf8 = "hello"
Two questions about a reference
Two questions about a reference, and they are independent: may the name be pointed somewhere else, and may the thing it points at be written? mut on the binding answers the first. The type answers the second, with mut inside it, as the POINTERS section shows — and nothing else answers either.
mut cursor: ptr u32 = addr first -- re-pointable, reads knob: ptr mut u32 = addr setting -- fixed, writes gpioa: ptr mut u32 = ptr(0x4002_0000) -- fixed, writes
Exported from the module
Exported from the module. Without it, module-internal.
public version: u32 = 3
Several names may share one declaration
Several names may share one declaration, which is the same form field lists already use.
public red, green, blue: u8 north, south, east, west: atom
It means the declarations written one per name, in order: each name gets its own storage and everything written around the list — mut, public, a parameter convention, at or from. A shared initializer runs once, for the first name, and the others start as copies of it. A shared declaration needs a written type, because low, high := bounds() would read as a destructuring [1810]. Type declarations, functions, type and fixed formals, variant parts and condition bindings keep one name each (D233).
mut low, high: u32 = next_seed() -- one call; high starts equal to low clamp: (inout low, high: i32, value: i32) -> (result: i32) = ... end
Left of ':' is always the name being introduced
Left of ':' is always the name being introduced. Right of ':' is what fills it: a type, or (with '=') a value.
In a local declaration, both the written type and the initializer use the names visible before that declaration. If an enclosing t names a type, mut t: t = value uses that type and then introduces the local t; later statements see the local. Condition bindings follow the same rule [1070]. Module declarations and routine signatures retain their collected scopes [0130] [1290].
Control words are reserved in every name position [1760]. Names such as begin, match, loop, break and unchecked cannot be variables, parameters, fields or labels. begin = 10 is invalid; use a name such as begin_value instead. Parentheses do not turn a keyword into a name.
Types are declared like any other value, with 'type'
Types are declared like any other value, with 'type'.
point: type = struct x: f32 y: f32 end point
NUMBERS AND LITERALS
Integers: u8 u16 u32 u64, i8 i16 i32 i64
Integers: u8 u16 u32 u64, i8 i16 i32 i64. u128 and i128 are not in this version. No derived program needed them, and no target here carries them natively: Cortex-M0 would hold one in four words with no runtime helper for its multiplication or division, and its C toolchain refuses the type outright. The Language evolution successor roadmap owns them, and a program that needs 128-bit arithmetic is what brings them back (D237). Any other width exists as well — u4, u12, u23 — for the packed fields of [0730], where the datasheet decides how many bits a thing gets. D228 admits u1 through u64 only in packed field positions; extracting one produces the next enabled machine width. A flag is spelt bool. A numeric one-bit field may be spelt u1 and produces u8 values 0 or 1; it does not acquire implicit boolean conversions.
Floating point: f32, f64
Floating point: f32, f64. No f80. f16 is not in this version either. Neither baseline x86-64 nor Cortex-M0 has a binary16 instruction, and adding the width would turn converting an integer to a float from a conversion that cannot overflow into one that traps [0700]. Language evolution owns it with the same kind of trigger (D237).
Integer literals are untyped
Integer literals are untyped. They take the type of their context, and are checked at that point.
tiny: u8 = 5 large: u64 = 5 -- bad: u8 = 300 is a compile error
With no context, an integer literal defaults to i32
With no context, an integer literal defaults to i32.
plain := 5
Float literals are always recognisable as such
Float literals are always recognisable as such. There is no silent slide between the two classes.
ratio: f32 = 5.0 -- wrong: f32 = 5 is a compile error half := 1.0 / 2.0 -- 0.5 zero := 1 / 2 -- 0, integer division
Bases, separators, exponents
Bases, separators, exponents. An integer starts and ends its digit run with a digit of that base. Underscores may repeat between digits, as in 1__000; a base prefix alone and a trailing underscore are refused. Each float component follows that digit-run rule too: both sides of the dot and a written exponent need digits, with underscores only between digits. A number has no suffix: a letter directly after one is part of it, so 1u64 is refused and the type goes on the binding, as in mask: u64 = 1.
hex_value := 0xDEAD_BEEF bin_value := 0b1010_0101 oct_value := 0o755 big_value := 1_000_000 sci_value := 1.5e10 neg_exp := 1.0e-6
Hex float literals express every representable value exactly
Hex float literals express every representable value exactly, including subnormals.
pi_exact: f64 = 0x1.921fb54442d18p+1 smallest_subnormal: f32 = 0x1.0p-149 smallest_normal: f32 = 0x1.0p-126
IEEE special values
IEEE special values. The sign of a literal is preserved by constant folding. The sign of a NaN produced by arithmetic is not specified.
neg_zero: f64 = -0.0 pos_inf: f64 = f64.infinity neg_inf: f64 = -f64.infinity quiet_nan: f64 = f64.nan neg_nan: f64 = -f64.nan
Character literal is a codepoint, typed u32
Character literal is a codepoint, typed u32.
letter_a := 'a'
Text literals are untyped too
Text literals are untyped too. They take utf8, []u8, utf16 or cstring from context; with no context, utf8. Every literal carries a trailing NUL that the length does not count, so passing one to C costs nothing.
name: utf8 = "hello" cname: cstring = "hello"
A literal lives in read-only storage, so a reference to one cannot be written through by [0070] however the binding is declared. Writable text is a copy into storage you asked for.
-- greeting: []u8 = "abc" -- greeting[0] = 0x78 -- error, the literal is static
The escape set is closed and small
The escape set is closed and small: \n \r \t \e newline, return, tab, escape \\ \" \' the characters themselves \xNN one byte, exactly two hex digits \u{...} one codepoint, any number of digits There is no octal form and no \0; write \x00. An unknown escape is a compile error, never silently the character. \xNN is only valid where bytes are meant, \u{...} only where text is meant, so a literal can never be invalid UTF-8.
line := "col\tsep\n" smile := "\u{1F600}" byte: []u8 = "\xFF" -- byte escapes require []u8 context
Raw literals: N quotes on each side, N at least three
Raw literals: N quotes on each side, N at least three. If the content contains three, use four. Nothing is escaped. The entire opening quote run chooses N. Six adjacent quotes are one opener of length six, not an empty literal with two delimiters. A later matching closer is required. Line endings inside remain content; write "" for empty text. The indentation of the closing delimiter is stripped from every line, so a block can sit indented in the code and still start at the left margin.
help := """ usage: tool [options] -v verbose """
OPERATORS
Arithmetic
Arithmetic: + - * / % Integer division truncates toward zero; the remainder takes the sign of the dividend.
q := -7 / 2 -- -3 r := -7 % 2 -- -1
No implicit conversion
No implicit conversion. Conversion is a type applied to a value. If the value is known at compile time, an impossible conversion is a compile error; otherwise it traps.
-- bad: u8 = u8(300) compile error runtime_narrow := u8(measured) -- traps if out of range
Shifts fill with zeros beyond the width, for any amount
Shifts fill with zeros beyond the width, for any amount. Signed >> keeps the sign. One form only.
z: u32 = 1 << 40 -- 0 in a u32 context
Bitwise
Bitwise: & | ^ ~ << >> These bind TIGHTER than comparisons, unlike C.
flag := status & 1 == 0 -- means (status & 1) == 0
Logical: and, or, not
Logical: and, or, not. Words, so '!' stays free for the error channel.
ok := a < b and not done
Ranges
Integer range traversal is written in a for header: for i in 0..9 do includes 9; for i in 0..<10 do excludes 10. The same bounds spellings select a slice, as in items[0..<10]. They do not construct a value: 0..<10 cannot be stored, passed, or returned on its own. Left-exclusive forms are gone; a library can supply custom stepping through an iterable value [1330].
sizeof, alignof and lenof
sizeof, alignof and lenof. On an array the length is a compile-time value; on a slice it is a run-time value; on a literal it is compile-time. The expressions in a literal measured by lenof are checked for one scalar element type but are not evaluated or read.
w1 := sizeof u32 a1 := alignof u64 n1 := lenof grid n2 := lenof ([next(), next(), next()]) -- 3; next is not called
addr takes an address
addr takes an address, .val names the pointee — reading on the right of an assignment, written on the left.
Assignment is a statement
Assignment is a statement, never an expression: = += -= *= /= %= &= |= ^= <<= >>= and the wrapping forms +%= -%= *%=
No ++ and no --
No ++ and no --. There are inc and dec statements, though x += 1 says the same thing.
inc total dec total
Evaluation order is fixed
Evaluation order is fixed, because a language whose point is that costs are visible cannot leave where they happen to the compiler. Operands and call arguments evaluate left to right. Aggregate fields initialise in the order they are written, whatever order the layout puts them in. 'and' and 'or' short-circuit, left to right. An assignment evaluates its destination place first, then the value. A deferred call is the one thing that does not evaluate where it is written, and [1100] says when it does instead.
The dot is not only field access
The dot is not only field access. All of these are member selection: p.val (pointer target), t.less (concept entry), http.get (module member), f64.nan (named value of a type). Separate tokens: .. (range), ... (inferred error set), and the decimal point.
POINTERS
Pointer type, address, dereference
Pointer type, address, dereference. All words, no sigils. mut inside the type says the pointee may be written; without it the pointee is read-only. .val names the pointee, so it reads on the right of an assignment and, with mut, is an ordinary target on the left.
addr p.val addresses the pointee and keeps p's origin. Likewise, addr items[i] for a slice addresses its backing element. Taking addr p instead addresses the pointer variable itself, whose storage may be local.
mut value: u32 = 42 p: ptr mut u32 = addr value v := p.val p.val = 43 ro: ptr u32 = addr value -- same address, read-only through ro -- ro.val = 1 -- error
addr of a writable place yields a mutable pointer; addr of an immutable place yields a read-only pointer. A mutable binding holding that read-only pointer may replace the pointer, but cannot write through it [0450].
A mut reference satisfies a plain one, never the reverse
A mut reference satisfies a plain one, never the reverse. That is not a conversion and [0310] is not bent: no bit changes and nothing is lost, a permission is forgotten. It is the one relaxation the language has, it goes one way, and it is what makes a read-only parameter usable at all.
add_up: (xs: []i32) -> (total: i32) = ... end add_up(writable_slice) -- fine, []mut i32 relaxes to []i32
Permission is shallow
Permission is shallow: it comes from the reference type and from nowhere else. An immutable binding or an 'in' parameter protects the value it holds, not what that value points at — so a list handed over as 'in' still yields writable elements if its storage is writable. Deeper protection would mean knowing which references a value owns and which it merely borrows, and this language does not track ownership [0910]. Where a read-only view is wanted, hand one out: that is what an accessor is for, and the caller relaxes what it gets by [0440].
Address literal
Address literal. The pointee type comes from context, and a register is written through, so it is a mut pointee sitting in an immutable binding — which is exactly the pair [0070] separates. What makes an access to it a device transaction is the operation that performs it [0850], not the pointer's type.
gpio: ptr mut u32 = ptr(0x4001_0000)
Both directions between a pointer and an integer are the
Both directions between a pointer and an integer are the ordinary conversion of [0700], a type applied to a value. No special word: this is the one conversion the language marks by what it costs rather than by how it is spelled.
dma_source := u32(addr port.val.dr)
What it costs: an integer that used to be a pointer has no origin. Build a pointer back out of one and you get something that borrows nothing, whose lifetime nobody knows, and about which the compiler will say nothing ever again. It is the one place inside the language where the lifetime system is deliberately left behind, on the same footing as the C boundary. Taking the address of a register is unremarkable by comparison: volatility is a property of the access operation, not of the number [0850].
There is no null
There is no null. "maybe a pointer" is an ordinary union of an atom and a pointer type. With one atom, the compiler represents it as a plain pointer with 0 for the empty case. The spelling does not decide how a union of several atoms and a pointer is laid out: with several atoms, the atom's own code is stored beside the pointer and code zero, which no atom has, marks the present case, so a smaller union or an atom set widens into it without changing a bit (D235).
none_found: atom maybe_ptr: type = none_found | ptr mut u32 lookup: (key: u32, cell: ptr mut u32) -> (found: maybe_ptr from cell) = if key == 0 then found = none_found else found = cell end if end lookup read: (m: maybe_ptr, fallback: u32) -> (value: u32) = match m none_found: value = fallback ptr (p): value = p.val end match end read
A pointer goes into the union without being written down as anything; coming back out is a match, and both cases have to be named. ptr is the arm for the present case, and the name in brackets after it is the pointer itself. On a return edge, an exact from clause applies only when this optional value actually carries the pointer. An edge provably returning none_found has no reference origin to compare: absence is not the untracked pointer produced by [0470].
Pointers are a system tool
Pointers are a system tool: hardware, the C boundary, and library internals. Everyday code uses slices, handles and indices.
Three operations sit between pointers and slices
Three operations were proposed between pointers and slices, in core where system-tool policy belongs:
mem.offset: (p: ptr mut u8, n: usize) -> (q: ptr mut u8) mem.base_of: (t: type, s: []t) -> (p: ptr u8) mem.base_of: (t: type, s: []mut t) -> (p: ptr mut u8) mem.slice_from: (t: type, p: ptr mut u8, n: usize) -> (s: []mut t)
Implementation pressure rejected the third one [0510]: no operation may turn an arbitrary allocation into a slice that claims every slot already contains t. The repository core/mem instead keeps pointer arithmetic inside its private raw-storage operations. It can copy one initialized slot directly into the next slot of a private replacement, but it exposes neither spare capacity nor a general pointer-to-slice conversion. The library records offset and base_of as unneeded conveniences: allocator internals use [0470]'s explicit address conversion, and consumers obtain an element address only after checking that a slice is nonempty. Neither name is a compiler primitive or an enabled library API. D196 records the disposition without reopening slice_from.
slice_from is where uninitialised storage is smuggled in
slice_from is where uninitialised storage is smuggled in, and calling that an answer was too kind to it. It hands back a []mut t over memory holding no t, so the type says more than is true from the allocation until the write, and nothing checks the gap. Containers hold the invariant themselves — a growing array by its length, a hash table by its state array — and that works, for a container whose author is careful. Where it does not work is inline storage. small(t, capacity) holds an [capacity]t that is not full yet, and there is no honest value to put in the empty slots: [0540] forbids zeroed for a t with no zero image, and ptr is such a t. So until there is a way to say uninitialised in a type, that shape is restricted to a t that has a zero image, and general uninitialised generic storage is not supported. The answer is core/mem's raw(t). It is an ordinary parameterised struct whose identity and fields stay private to that module. A caller may hold the inferred result of reserve; another core module names the same private identity through the public alias storage(t). Neither route exposes the representation. capacity and initialized report the two counts, admit initializes exactly the next slot, get reads only the initialized prefix, replace writes an existing initialized slot, and used exposes that prefix as a mutable slice from storage. release removes only its tail, and dispose returns the backing byte pointer only when the prefix is empty and backing is present. It clears that backing to the private union's absent atom; a repeated disposal reports raw_empty, never a zero or fabricated pointer. transfer copies one initialized source slot directly into the next slot of a private replacement, without exposing a reference-valued item between the two states. The four invalid requests are foreseeable and therefore declared outcomes: raw_full, uninitialized, raw_empty, and raw_not_empty. For byte storage, clear shortens the typed initialized prefix to zero in one step; it leaves the backing allocation for dispose to return.
Growth uses two raw values. Allocate and reserve an empty replacement, copy the old initialized prefix into it, and roll that private replacement back if any admission fails. After every copy succeeds, release the old tail to zero, dispose its allocation, and assign the replacement to the published binding. That last assignment is the publication point. The private initialized slice is the length witness. The first complete typed store precedes taking its singleton slice; a later append writes the next item before a narrow unchecked range extension publishes it. No slice ever describes spare capacity and no spare byte is read as t. A used view must end before a capacity or prefix transition; writes through aliases retain [0860]'s stated local-analysis limitation.
This is not a memory-safety claim. Before dispose, save capacity * sizeof t as the allocator's release extent; the caller still supplies a live, aligned allocation large enough for that many slots. Pointer validity, alignment and a lying extent remain outside the language guarantees [1720]. The private module boundary enforces the state transitions without a raw keyword, a new built-in type kind, or the dishonest slice_from operation.
ARRAYS, SLICES AND TEXT
The initialized allocation helper mem.new(state, value) stores the complete initial value before returning ptr mut t; reference-containing values must satisfy its escaping parameter. mem.delete consumes one pointer binding and releases the original object extent through the supplied allocator. mem.new_bytes(state, count) instead returns a private byte_buffer owner: every byte is initialized to zero, mem.bytes(owner) borrows its mutable slice, and mem.drop_bytes(state, owner) releases the original allocation and clears the owner after the byte prefix is emptied. A zero count makes no allocation. A copied view or owner is still subject to manual lifetime discipline; consumption is not ownership.
Array: a value
Array: a value. Assignment copies. Size is part of the type. Its bound is a closed fixed expression: integer literals and fixed parameters combined with parentheses, unary -, and non-wrapping +, -, *, / and %. It is substitution and arithmetic, never a call or compile-time user execution; width-dependent wrapping, bitwise and shift operations are not bound forms. A negative result is refused. A zero result is accepted: [0]t and an admitted fixed expression that folds to zero denote a zero-length array. This does not add an empty array literal or make zero-length repetition valid. An array retains its complete element type through a pointer, slice, generic call or stored field. Pointer and slice element permissions remain part of that identity, and an any C element retains its own data and evidence for C. A whole element of aggregate type is the same value and place whether its index is written as a known position or computed at run time. A computed index is evaluated and bounds-checked once before the element is used; this does not expose a pointer or change the array's by-value copies. An array literal assigned to existing storage is formed there in written order: each element is evaluated and written before the next one begins. A later element can therefore observe an earlier write, and a failure can leave the written prefix changed; no hidden array-sized temporary is implied. An explicitly typed local may finish a nonempty written prefix with [0560]'s repeated suffix; the same direct formation and ordering apply.
grid: [4]f32 = [1.0, 2.0, 3.0, 4.0] mut next: [4]f32 next = [grid[0], grid[1], grid[2], grid[3]]
The length may be inferred from the literal
The length may be inferred from the literal.
triple := [1.0, 2.0, 3.0] -- [3]f32
The first element supplies the element type, whether it is a number, a slice, a pointer or a struct, and every later element must be of that type (D241).
zeroed is the all-bits-zero image of a type
D152 also admits uninit as an explicit array-field initializer inside a private compact nominal construction. It leaves the field's bytes unspecified; the defining module must expose only items it has written. It is not an alternative spelling of zeroed and cannot initialize a stand-alone array.
zeroed is the all-bits-zero image of a type. Two separate properties decide where it may appear. A type HAS a zero image when all-zero is a valid value for it; that is what lets a surrounding struct or array be zeroed. A function address has no zero image, so a struct or active variant payload containing one must supply it explicitly rather than using whole or trailing zeroed. A type ACCEPTS the word zeroed only when that image has one obvious reading. Numbers, bool, ranges containing zero and aggregates of those accept it. Named value sets do not: write the name. Pointers and 'any' have no zero image at all, because there is no null. Where the destination supplies that context, assignment writes the complete zero image as one value rather than spelling its parts. D228's packed structs are raw images: zeroed is permitted even when an encoded field's zero pattern has no name. Reading that field then traps; copying the image preserves its bits.
mut buffer: [256]u8 = zeroed buffer = zeroed -- clear the existing array as a whole irqs: irq_set = zeroed -- fine: a set is bools, see [0730] -- mode: clock_mode = zeroed -- error, write 'internal' -- p: ptr u32 = zeroed -- error, no zero image
The first of those two properties is also a concept
The first of those two properties is also a concept, so generic code can ask for it. zeroable is the one closed compiler concept: it needs no source declaration, and its conformances are supplied by the compiler rather than declared, which is the only kind of conformance that is. The compiler is the only thing that knows a type's bit patterns. Its conceptual signature is:
-- compiler-owned; a program does not repeat this declaration zeroable: type = concept (t: type) end zeroable
A program cannot declare a zeroable conformance. The supplied family contains the enabled scalars, a fixed array exactly when its element is zeroable (also at length zero), and an aggregate exactly when its recursively active all-zero shape has a zero image. Atoms, functions, pointers and slices are not in it. This is a closed named family, not reflection over arbitrary representations.
Which is what lets a shape that needs a value for storage it has not filled yet say so, instead of pretending — see small at [1350] and the reason at [0510].
Repetition, for static patterns that live in flash
Repetition, for static patterns that live in flash.
pattern: [256]u8 = [256 of 0xFF] filled: [256]u8 = [of 0xFF] -- length from the type read_header: () -> none = header: [8]u8 = [0x7F, 0x45, of 0] end read_header
The repeated expression runs once, not once for every element. In the mixed form [e1, ..., ek, of repeated], an explicitly typed local, explicitly typed module binding or assignment to a mutable fixed array requires 1 <= k < N, where N is the destination length. A local or assignment evaluates and stores the prefix left to right, then evaluates repeated once and compactly fills the suffix. An assignment reaches its destination first; all right-hand reads use the incoming definite-assignment state, and successful completion assigns the whole array. A module initializer requires every prefix and suffix expression to be compile-time known [1940] and keeps a compact static image: the finite prefix plus one repeated suffix pattern. Inference, nesting and general-value mixed forms remain refused. of stays contextual: [of, other, of] and [of + 1] remain ordinary literals when of names a binding. A nonzero written fixed-array type accepts either full-array form for module state or a local; at module scope the expression must be a compile-time known scalar [1940]. A counted repetition may also supply an inferred local or module binding's length and scalar element type; an untyped integer element defaults to i32. Assignment to an existing fixed array accepts an explicit count or takes it from the destination:
flash: [4]u32 = [of 0xFFFF_FFFF] header_image: [8]u8 = [0x7F, 0x45, of 0] mut row: [4]u32 = [of next()] inferred := [4 of next()] row = [of next()] -- each call to next happens once row = [header(), of padding()] -- prefix first, then one padding call
A zero contextual length and a zero count in the inferred form remain refused: [0]t is a valid fixed-array type, but repetition still needs a nonzero contextual destination or a count that supplies an inferred element shape. A count-less inferred initializer, such as row := [of 0], has no length to give and is refused; the other array value positions follow [0520] (D241). Every inferred extent must fit the target's usize. A module repetition uses [1940]'s target-aware fold and range rules; its compact repeated image survives direct-name chains, while a folded zero pattern has the same loader-zeroed image as omitted or zeroed state.
Slice: a view
Slice: a view. Pointer plus length. Copies nothing. mut inside the type says the elements may be written.
view: []f32 = grid[0..<2] -- read-only elements edit: []mut f32 = grid[0..<2] -- writable, since grid is mut
An empty slice still has a base
An empty slice still has a base. It is the lowest positive address aligned for the element type — the numerical element alignment — and may not be dereferenced (D141). The proposed base_of convenience is omitted [0500]; the representation rule does not depend on that API. Indexing checks the length before it computes an address, so an empty slice cannot produce one.
Fixed arrays are the vector type
Fixed arrays are the vector type. Arithmetic on numeric elements is element-wise, so there is no second type with the same shape and different rules. The first backend lowers it to compact scalar loops; real vector instructions are a later optimisation, never required for source correctness.
sum_four: (values: [4]f32) -> (sum: f32) = sum = 0.0 for value in values do sum += value end for end sum_four array_example: () -> (sc: [4]f32, s: f32) = va: [4]f32 = [1.0, 1.0, 1.0, 1.0] vb := va * va sc = va * 2.0 -- a scalar on either side is fine s = sum_four(vb) -- ordinary function, not a reduction builtin end array_example
D209 lifts +, -, *, / and unary - over numeric arrays, and %, +%, -%, *% over integer arrays. Arrays must have the same element type and length; a scalar of that element type broadcasts, not an array of another length. Each operand is evaluated once, left to right, and complete array values are retained before the element loop. Arithmetic assignment therefore survives overlap rather than reading back its own earlier destination writes. Elements are processed in ascending index order with the scalar trap, wrapping and floating-point rules. No reassociation changes sum_four's ordered fold from positive zero.
D200 refuses every array comparison, including == and <>; it does not silently choose whole-array equality or a mask. Named ordinary functions can state those algorithms explicitly. [4096]f32 + [4096]f32 needs a 16 KB result and may retain operand snapshots as well. The type makes that cost visible; compact loop code does not make the storage free.
Text types are distinct views
Text types are distinct views, not one string type: utf8 distinct []u8 text, validated shortest-form UTF-8 utf16 distinct []u16 cstring distinct ptr u8 no length, NUL terminated
The parser-support core/text slice arrives before those full distinct text types. It accepts []u8, gives byte offsets the opaque nominal name text.position, and supplies first/end, byte, advance, ordinal and subslice operations. A byte read at the end reports past_end; a returned subslice keeps its source origin.
The hosted views themselves accept quoted and raw literals. utf8 stores shortest-form UTF-8 bytes, utf16 stores UTF-16 code units, and cstring stores encoded bytes behind a read-only pointer. A literal cstring contains valid UTF-8, but a value published by a foreign boundary promises only accessible backing through its first NUL byte; its encoding is not prevalidated. lenof on utf8 and utf16 counts their code units. Every literal datum has one trailing zero code unit; slice lengths exclude it, and cstring carries no length. Equal decoded content at the same element width may share its read-only static datum. The three view identities remain distinct from one another and from their backing pointer or slice types through calls, aggregates and generics.
Ordinary explicit conversion admits the exact source-derived representation views needed at runtime: immutable []u8 to utf8 validates and may trap; utf8 to immutable []u8 preserves its base and length; and cstring to either form scans only to the first NUL, validating when the destination is utf8. Empty results keep the actual source carrier and origin. There is no pointer-to-cstring conversion, mutable result or utf16 representation conversion. core/text.from_bytes and from_c perform the same validation in ordinary source and report invalid_text; its exact byte equality, byte-substring, decimal parsing and bounded caller-buffer writing helpers add no normalization, allocation or hidden truncation.
Range slicing is an operation on utf8 and utf16, not an exposure of their backing slices. Its exact usize bounds count the view's bytes or UTF-16 code units, and both endpoints must lie on Unicode-scalar boundaries. The half-open form may end at the view's length. The inclusive form includes the complete scalar beginning at its upper bound. Either form returns the same immutable, source-derived text identity; cstring has no length and cannot be sliced. An invalid bound or a bound that splits an encoding traps. Traversal remains a separate [1320] operation rather than inheriting either backing carrier. Each exact hosted view has its own intrinsic iterable conformance, yielding Unicode scalar values as copied u32 items with a private usize code-unit cursor; cstring stops before its first NUL. Foreign C text is validated before each scalar is decoded; malformed encoding traps, including inside unchecked. Traversal declares no atom error. Use core/text.from_c when invalid encoding needs a recoverable invalid_text result. [0610] supplies indexing separately.
Indexing utf8 by an integer yields the codepoint there
Indexing utf8 by a usize yields that codepoint as a u32, the type a character literal [0250] and a traversal of the text [1320] already give it, and is a linear scan by codepoint ordinal. Indexing by the opaque core/text.position byte offset decodes the codepoint beginning there in O(1). The position must be in bounds and at a codepoint boundary. An ordinal outside the text, or a position at the end or on a continuation byte, traps. Both conformances exist; the argument type decides without an implicit integer conversion.
word: utf8 = "\u{e9}t\u{e9}" last := word[2] -- 233, the u32 of é, found by scanning same := last == '\u{e9}' -- true bytes: utf8 = word[3..3] -- é again, at byte 3: two bytes, no copy
The codepoint is decoded, so it is a value and not a place: addr word[2] and word[2] = 'e' are errors. Its bytes are a one-codepoint range of [0600], whose bounds count bytes rather than codepoints and whose result is still utf8. Converting the complete utf8 to its ordinary byte view does not inherit this codepoint-index operation; it has the ordinary slice operations.
DEFERRED to a later version, kept here as a design record
DEFERRED to a later version, kept here as a design record. It touches aliasing, generics, slicing, addr, layout, debug information and the optimiser all at once, and no program has yet proved it necessary. The Language evolution successor roadmap holds it, and a simulation that needs one field of a collection contiguous is what would bring it in. A collection may be stored transposed: one array per field instead of one array of structs. That is a property of the collection, not of the struct, so it is not a layout attribute. Field access through an index goes straight into that field's array, reading a whole element gathers a copy, and the address of a whole element does not exist. The point of it is the last line: one field, contiguous.
world: type = struct items: soa [4096]entity count: usize end world advance: (inout w: world, dt: f32) -> none = for i in 0..<w.count do w.items[i].x += w.items[i].vx * dt end for xs: []f32 = w.items.x shift_all(xs, dt) first := w.items[0] -- a copy -- p := addr w.items[0] -- error: the fields are apart end advance
Only for structs without a variant part. Layout attributes do not apply, because there is no single element layout.
TYPES YOU DECLARE
Atoms: one declaration, one value, its own type
Atoms: one declaration, one value, its own type. This is the one place where a declaration introduces a type and a value at once, because they coincide. Equality and inequality compare those declaration identities, including values drawn from disjoint or overlapping atom sets; no ordering or numeric conversion is implied.
not_found: atom no_access: atom
An enumeration is a union of atoms
An enumeration is a union of atoms. north, south, east and west were declared at [0100]. An atom set can be an array element, a struct field or a variant payload. These retain the declared set: a write needs one of its members, and reading it back grants no integer operations or conversions.
compass: type = north | south | east | west
Distinct type
Distinct type: same representation, different type, no operations inherited.
meter: type = distinct f32 second: type = distinct f32
Neither meter + second nor adding two meter values inherits f32's arithmetic. Construct and extract explicitly:
length: meter = meter(2.5) raw: f32 = f32(length)
Aliases keep the identity; generic deduction and conformances distinguish it from its base and from every other distinct declaration. A distinct view keeps its backing origin when wrapped and extracted. D213 specifies these conversions and the unchanged target representation.
Range subtype: checked at assignment and conversion
Range subtype: checked at assignment and conversion.
percent: type = u8 range 0..100
A range subtype constrains the places a value is checked on its way in: a binding, a parameter, a named return and a conversion. It is not a struct field, an array element, a pointer or slice target, a generic argument or something addr can point at. In each of those a write through the base type would reach constrained storage unchecked, and giving the constraint an identity of its own there would make a second nominal type with a second relaxation [0440]. To keep a checked value in storage, wrap it: a distinct type that only a checking function constructs is the proof carried [1730] (D236).
A guard on an ordinary integer does not give it a range subtype. If invalid input is handled with a fail guard and the successful path then stores the integer as percent, that store still has a trapping check. Once the value is in a percent place, passing it to another percent place carries the proof without a second check (D188).
Struct, block form and inline form
Struct, block form and inline form. Same thing. The inline form is also the parameter, return and payload list.
size2: type = (w: u32, h: u32)
Both declared forms may take type and fixed parameters. An inline body is nominal just like the block: two separately declared, equal field lists are different types, while an alias keeps the original identity. layout applies to either form. A fully applied instance substitutes them through scalar, fixed-array, nested ordinary-struct, function and existing variant payload fields. The formals remain compile-time names; they add no field or hidden runtime argument.
Struct with a variant part
Struct with a variant part. Common fields need no ceremony.
figure: type = struct label: utf8 area: f32 kind: variant circle: (radius: f32) | rectangle: (width: f32, height: f32) end kind end figure
A case with no payload is written bare
A case with no payload is written bare. It is an atom, and [1700] holds wherever atoms appear, so a variant reads as the union it is.
node_kind: type = struct kind: variant leaf | branch: (first: node_id, count: u32) end kind end node_kind
A case is written where its variant part is the context: constructing into that part, and naming an arm of a match on it. It is not a value of its own apart from the part, and neither is the part apart from its struct: copy the struct, or match it (D241). Selecting a bare case writes only its tag. Selecting a case with a payload zeros that case's payload storage before writing its fields. The inactive case storage and layout padding have unspecified bytes after selection; zeroed still writes the complete all-zero image of the containing struct. The work of selection therefore follows the selected payload's size, while whole-struct zeroing and copying follow the struct's reserved size.
Construction and conversion use the same form
Construction and conversion use the same form: a type applied to arguments.
origin := point(x: 0.0, y: 0.0) metres := meter(1.5)
A variant case is built the same way: the case name applied to its payload. The tour showed variants being matched long before it showed one being built.
mut f: figure = (label: "disc", area: 0.0, kind: circle(radius: 2.0)) f.kind = rectangle(width: 3.0, height: 4.0)
A struct literal is untyped
A struct literal is untyped, the way a number literal is, and takes its type from the context — named or anonymous. What does not convert is a value already typed as an anonymous struct, a return list for instance: two anonymous types match when field names, types and order all match, and neither ever becomes a same-shaped named type.
here: point = (x: 1.0, y: 2.0) pair: (quot: i32, rem: i32) = divide(10, 3) -- named: point_pair = divide(10, 3) -- no, that is named
A named parameterized struct is nominal per fully applied instance. The identity is the source declaration plus its complete normalized positional actual tuple, including an actual the current field list does not use. Aliases of actuals do not create a second identity; changing an actual or the template does, even when the resulting fields and layout are equal. A mention in a function signature or as another template's type actual needs that identity but does not place its value inline. The same is true for an ordinary struct that names itself, an alias of itself, or another ordinary struct in a function parameter or result: each function field remains one pointer carrier, so even a mutual signature-only cycle has finite layout. A declared or anonymous routine materializes its direct multiple-result parts when it forms their caller-owned result aggregate, not by traversing a nested callback signature. If the receiving template later uses that actual as a field, payload or nominal array element, its layout is materialized there, recursively through nested nominal and nominal-array actuals. A zero-length array still validates that element edge. Target widths, offsets and padding are layout facts, not identity.
At module scope that context forms a static image rather than running a constructor before the entry point [1460]. The image follows ordinary child fields and ordinary-struct variant payloads recursively. It still keeps every nominal boundary: a matching construction, zeroed, or a copied module image supplies the child, and a same-shaped different struct does not. Forward image references and aliases are allowed; a chain that returns to itself has no value. Widths, offsets and padding are not part of that folded source image — they follow the selected target's field layout [0750].
'of' fills every field the literal did not name
'of' fills every field the literal did not name, which is the same word and the same place array literals use it in at [0560]. It has to typecheck for each of them, so 'of false' works where the rest are bool and 'of zeroed' works wherever the rest have a zero image. A nonzero fill needs at least one omitted field. All omitted fields have the same complete type, including array length and reference permission. Its value is evaluated once after the named values and copied into each remaining field in declaration order. If an unpacked array repetition fills just one remaining field, its element is evaluated before the array is written and it can build directly in that field. A zeroed fill retains each field's own zero-image rule. This is not a default value: [0980] refused those on declarations, because a new parameter would then fit every existing call silently. Here the choice is made at each literal by whoever writes it. The trade is still real and belongs to that writer — a literal with 'of' picks up a field added later without a word, one without it breaks loudly.
cfg: control = (enable: true, mode: external, of zeroed)
A register has three kinds of field
A register has three kinds of field, and confusing them is the classic driver bug. A flag is one bit. A selection is a set of mutually exclusive named values, encoded as the datasheet says. A set is independent flags at once. And a plain number is a plain number. A set earns its place when its members mean something. A numbered bank of lines does not: sixteen output pins are [16]bool at 0..15, not sixteen invented atoms.
irq_rx: atom irq_tx: atom irq_err: atom internal: atom external: atom pll: atom
The encoding belongs to the union, not to the atom, so the same atom may be encoded differently in another register. The width is the smallest that holds the largest encoding, so it is usually left out; a base type is written only when the datasheet gives the field more room than it needs.
polarity: type = (active_low = 0 | active_high = 1) -- one bit clock_mode: type = (internal = 0 | external = 1 | pll = 4) divider_sel: type = u4 (by_1 = 0 | by_2 = 1) -- four bits, as given
A set is not a kind of its own. It is a packed struct of bool, one field per member, each sitting at the bit its member's encoding names — so membership is a field read, adding and removing a member is a field write, and building one is the ordinary struct literal with 'of' from [0720]. There is no set literal, no set operator and no membership operator, because none of them is needed once it is a struct. The union's encodings are what place the bits, so the bit assignment never falls to declaration order, which is the hole this section closed. Inside a larger image the same fields sit at the image's bits, offset by where the set begins. A generator reading a vendor's SVD writes those fields out as it writes every other register declaration [1540]; the checked-in vendor device fixtures are that output. General SVD generation retains the companion-tool owner. The language adds no set(X) type former that would write them for it (D238), and the expansion does not change the raw-image or validation contract.
irq: type = (irq_rx = 0 | irq_tx = 1 | irq_err = 2) irq_set: type = layout(packed, u32) struct irq_rx: bool at 0 irq_tx: bool at 1 irq_err: bool at 2 end irq_set control: type = layout(packed) struct enable: bool at 0 mode: clock_mode at 4..6 irq_rx: bool at 8 -- irq's encodings, from bit 8 irq_tx: bool at 9 irq_err: bool at 10 divider: u12 at 16..27 end control
Bit positions are written down rather than implied by the order of the fields. Every bit nobody claims is reserved by that fact alone: it cannot be named, it survives a read-modify-write untouched, and it appears in no completion list. That removes a field kind, removes the silent rule that the first field takes the low bits, and removes the dependence of the encoding on declaration order — which matters, because reordering fields of a published register would otherwise be a breaking change nobody sees. Within one packed struct it is all or nothing: if one field carries a position, all of them do. A field may be an array, which is how every real peripheral describes sixteen two-bit pin settings in one word. The element width times the count has to equal the range, and element zero takes the low bits.
moder: type = layout(packed) struct pins: [16]pin_mode at 0..31 end moder
Indexing such a field by a value known only at run time is ordinary code: it is a shift by a computed amount inside a register image, with a bounds check. D228 retains this bit-selection check in unchecked, because an invalid bit index has no computed byte address. On an image, that is — never straight through the device, for the reason [0740] gives.
set_pin: (inout m: moder, n: usize, mode: pin_mode) -> none = m.pins[n] = mode end set_pin
D228 makes the distinction precise: the packed struct is a raw image and accepts every carrier pattern. Copying it, passing it and returning it preserve all bits. Extracting an encoded field checks membership and traps on an unnamed pattern before producing a named value. Exhaustive matching applies to that validated value. An insertion preserves all other bits, and a fresh constructor starts with zero omitted bits. Packed fields have no independent byte address; pass the image for updates. layout(packed, u32) explicitly retains a 32-bit carrier when the highest named position would otherwise select a smaller one. The enabled representation widths are u1 through u64 within packed fields; they do not add arbitrary-width arithmetic or calling conventions.
Access behaviour is data, not keywords
Access behaviour is data, not keywords. How a register reads, how it writes and what reserved bits it wants are atoms handed to the one operation that touches it, so an SVD generator can express write-one-to-clear, clear-on-read, write-once and whatever the next vendor invents without the language growing a word for each. There are two operations: compiler.register_read(pointer, read_mode) and compiler.register_write(pointer, image, write_mode, reserved_policy, named_mask). The pointer is an ordinary one to u8, u16, u32 or u64, and its width is the one transaction the device sees. Writing a single field of a device register stays forbidden: build the whole value, write it once.
status_raw: (base: usize) -> (raw: u32) = port: ptr u32 = ptr(base + 0x04) raw = compiler.register_read(port, compiler.normal_read) end status_raw clear_events: (base: usize, events: u32) -> none = port: ptr mut u32 = ptr(base + 0x08) compiler.register_write(port, events, compiler.one_clears, compiler.write_zero, 0x0000_0007) end clear_events
The access behaviour is checked exactly there: a register whose read is 'none' has no read to perform, so compiler.no_read is an error rather than a mode, and so is compiler.no_write. Together with the rule above that gives the shape of every driver — read the whole image, change it locally, write it back whole. A fresh local constructor has no access to the previous hardware image. Preserving reserved hardware bits therefore requires an explicit image read before the local update; a whole-image write cannot hide that read. This read/modify/write sequence is not atomic, and its normal-read/normal-write premise does not extend to clear-on-read or one-clears behavior. Packed image source forms and every mode and policy are governed by D228. A generator writes one small typed function per register over these two operations, with the image's decode and encode beside it. D238 withdrew the register(t, read:, write:, reset:) wrapper type and the volatile ptr type the tour once used here: the operation, not the type, says that an access is a device transaction, and the complete prototype-1 driver needed nothing more. 'reset' initialises nothing. It is what the datasheet says the register holds after a reset, generated as a constant so that tools and readers know what they are starting from. The hardware puts it there, not the program.
reset_flags: (base: usize) -> none = port: ptr mut u32 = ptr(base) image: control = (enable: true, mode: external, irq_rx: true, of zeroed) compiler.register_write(port, control_encode(image), compiler.normal_write, compiler.write_zero, control_named_mask) end reset_flags read_modify_write: (base: usize, n: u32) -> none = port: ptr mut u32 = ptr(base) mut image := control_decode(compiler.register_read(port, compiler.normal_read)) image.divider = n compiler.register_write(port, control_encode(image), compiler.normal_write, compiler.preserve, control_named_mask) end read_modify_write
Fields keep the order you wrote them
Fields keep the order you wrote them, with natural alignment and padding in between, so a hexdump matches the source and the layout does not shift under a new compiler. layout(optimal) explicitly permits D210's stable descending-alignment candidate, selected only when its final padded size is strictly smaller. Equal-sized candidates keep source order. Complete nested fields, arrays and variant parts stay indivisible; their internal rules do not change. The selected target supplies every byte count, including for 32-bit descriptions. Initializer evaluation still follows written order [0410]. Optimization flags never reorder an unannotated struct. layout(c) applies the selected C rules [1975]; it does not choose a function's calling convention. The enabled C subset uses native-endian scalar fields, fixed arrays, nested nonempty C structs and explicitly C-convention callback fields. Optimal layout does not qualify a record for C transport. Byte order is not a property of a field. A field holds the target's own order, which compiler.byte_order names [1560]; a program reading or writing another order converts where the bytes cross, with shifts [0320] or an ordinary function, so the conversion is visible where it costs. D239 withdrew the per-field big and little prefixes the tour once showed: they would make every load and store of that field a conversion, leave addr of it pointing at bytes an ordinary pointer reads wrongly, and need a debugger presentation the two native debuggers do not share.
packet: type = layout(c) struct kind: u8 length: u16 -- big-endian as the wire sent it id: u32 end packet from_big_u16: (wire: u16) -> (value: u16) = value = (wire << 8) | (wire >> 8) -- on a little-endian target end from_big_u16
Attributes are prefix words, from a closed set
Attributes are prefix words, from a closed set. Arguments are always parenthesised. Closed value sets are atoms; arbitrary linker names stay strings.
mut public layout(c|optimal|packed)
and 'at' for a bit position. 'at', 'of' and 'align' are contextual: a field or an entry may still be called one of them, because nothing reads the word as an attribute there. Placement alignment is link's 'align' label [1640], not a word of its own. 'from' and 'with' are reserved and cannot name anything.
escaping caller fixed option link(section: "...", symbol: "...", align: 16, vector: 16, keep) extern(c|interrupt|naked|...)
packed folded into layout, naked into extern, option implies fixed, and the five toolchain words became one attribute with named arguments. Register access is data now, not keywords. D238 withdrew volatile with the pointer type it qualified [0850], and D239 withdrew big, little, weak, inline and noinline: byte order is converted where bytes cross [0750], inlining is the optimizer's decision rather than the source's [1310], and a whole program links one definition per name, with the compiler-owned vector image doing what weak default handlers do in C [1640].
LIFETIME AND ESCAPE
There is no borrow checker and there are no lifetime
There is no borrow checker and there are no lifetime annotations. Instead every pointer, slice and 'any' carries the origin of what it refers to: static, allocated, or frame. Frame-origin may not be returned, and may not be stored where something longer-lived keeps it.
bad: () -> (p: ptr u32) = x := 42 p = addr x -- error: a frame origin escapes end bad
Parameters are non-escaping by default
Parameters are non-escaping by default, so a callee may use a pointer freely but not keep it. Keeping it is declared. This applies to stores through caller-owned pointers, slices and inout fields as well as module bindings. An update within the same origin is permitted; retaining a reference from another parameter requires escaping. In push_front, both head and item are retained in storage reached through the other argument.
push_front: (escaping inout head: ptr mut node, escaping item: ptr mut node) -> none = item.val.next = head head = item end push_front build: (provider: type is mem.allocator, inout a: provider, escaping seed: ptr mut node) -> (head: ptr mut node) ! mem.out_of_memory = head = seed n := try mem.new(state: a, value: seed.val) push_front(head, n) -- allocated, fine mut local: node = seed.val push_front(head, addr local) -- error: a frame origin escapes end build
The other direction
The other direction. What a returned reference was derived from is written down, because the caller cannot otherwise know that the thing it came from has to hold still now. Write permission remains the return type's business [0430]. A writable result also owes the destination contract below; from does not grant permission to write.
used: (t: type, l: list(t)) -> (s: []mut t from l) = ... end
One accessor, not two. It hands out the widest permission the storage has, and a caller who wants less relaxes it by [0440] — xs: []t = vec.used(l). The pair of accessors that every language with deep const ends up needing is not needed here. Named from several parameters at once it borrows all of them. With no from clause the result is independent: it borrows nothing. That is what an allocator returns, and it is why two live allocations out of one allocator are unremarkable. Both halves are checked where the function is written. Returning something derived from a parameter without saying so is an error, and naming a parameter it did not come from is an error too, so the clause cannot drift away from the body. For [0480]'s optional pointer this exact comparison is made only on an edge that actually returns the pointer; an edge provably returning the empty atom has no reference origin at all. For a result that carries a writable reference, D222 also requires that its from clause conceal no known independent storage alternative. A helper cannot choose between its parameter and a module address while advertising only the parameter. Pass the fallback explicitly and name both sources:
choose: (source: ptr mut ptr i32, fallback: ptr mut ptr i32, flag: bool) -> (r: ptr mut ptr i32 from source, fallback) = r = if flag then source else fallback end if end choose
Calling choose(addr local_slot, addr module_slot, flag) keeps both possible destinations visible. Storing a non-escaping caller reference through that result is refused; choosing between two views of the same caller origin still permits a same-origin update [1910]. Wrapping this call in a helper that hides module_slot and declares only from source is also refused. Ordinary container accessors remain valid, including heap-backed views: from does not require storage physically inside the argument object.
This applies to writable references inside arrays, structs and variants, and behind read-only references: permission is not deep const. Erased state is checked conservatively because its hidden type may carry writable views. Origins within a reference-bearing aggregate join conservatively; a read-only field does not isolate its independent origin from a writable sibling. A result with no reachable writable reference keeps the dependency-only rule. An optional empty atom and an empty slice literal add no storage destination; constructors preserve that empty-storage fact, but the slice or aggregate still owes the ordinary exact from comparison. Independent results with no from remain valid. Explicit integer-to-pointer conversion keeps [0470]'s untracked boundary, but cannot erase a separately known module alternative. These are local checks of a written contract, not inferred interprocedural alias analysis.
In obligation it is the mirror of escaping. escaping says the callee keeps a reference to what the caller handed in; from says the caller keeps a reference to what the callee handed back. It is written rather than inferred, for the reason escaping is: inference across calls lets a distant body change a signature, and local compilation is worth more. Writing it also leaves nothing to carve out — function pointers, concept entries and the C boundary need no special rule here, because nothing was ever being inferred.
The borrow of reaches across the call from there
The borrow of [0830] reaches across the call from there, with the derivation supplied by the signature instead of being visible in one expression. No new checking, and the fix is the same one: take the view again afterwards.
xs := vec.used(numbers) try vec.push(numbers, a, 5) -- error: numbers is borrowed by xs use(xs)
Nothing is written at the call. A convention is part of the declaration and appears nowhere else: the compiler has it, and a caller that misuses what it got is told so at the next use, so a marker would buy legibility rather than safety. That is an editor's job, the same way the resolved error set is. escaping already works this way — it puts an obligation on the caller and lives only in the signature.
Integer-to-pointer conversion ends tracked derivation
Integer-to-pointer conversion [0470] ends tracked derivation. An integer that used to be a pointer carries no origin, so a pointer formed from it borrows nothing. Allocator internals use that conversion explicitly to return blocks without borrowing their allocator. The conversion does not establish that the storage is live or large enough; those remain the library's unsafe backing obligations [1720]. No special compiler privilege belongs to core.
address: usize = usize(state.base) + offset block: ptr mut u8 = ptr(address) -- no tracked derivation survives
This is the address conversion used by core/mem.arena_alloc after checking its arithmetic. [0500] records the omitted offset and base_of conveniences and the rejected slice_from; none supplies another derivation rule. The initialized-prefix state machine remains responsible for exposing only values that have actually been stored [0510].
Arenas are ordinary allocators with explicit backing
An arena is an ordinary library value, not a builtin type or lexical region. Import core/mem and write mem.arena, or accept any provider satisfying mem.allocator. Both formerly promised forms, arena name do and the otherwise-undeclared builtin type arena, are withdrawn by D212. Their named diagnostics explain the change; a declared type or ordinary identifier named arena remains valid.
mem.arena_over(base, size) supplies the backing address and byte capacity explicitly. Its escaping base parameter accepts backing that the caller may retain, including module storage and independently acquired storage; it rejects a frame array. The arena representation is private, so a struct literal cannot bypass that check. The handle retains the base's origin. Allocation aligns the absolute address and advances a monotonic offset; individual frees do nothing. A caller can supply a fixed array on a constrained target or explicitly acquired hosted storage, choosing the actual capacity rather than a hidden frame buffer. Overflow or an extent that does not fit reports mem.out_of_memory before changing the offset. A mem.failing provider adds an explicit successful-allocation budget [1360]. mem.arena_used and the mem.failing_* accessors report counters without exposing the private state.
import core/mem mut backing: [1024]u8 = zeroed report: () -> (code: i32) = mut scratch := mem.arena_over(addr backing[0], 1024) code = 1 first := mem.new(scratch, u8(7)) else (problem) _ = problem return end second := mem.new(scratch, u8(9)) else (problem) _ = problem return end code = i32(first.val) + i32(second.val) end report
For bulk hosted cleanup, core/region.new_region(addr provider) creates an ordinary allocator that records its acquired extents through that provider. An explicit defer region.release_region(program) releases them together. The region value borrows its provider; its allocations are still independent, and its bookkeeping also consumes the supplied provider's capacity. A finite caller-backed provider therefore remains finite, with no hidden heap fallback. This library operation is not the withdrawn lexical escape guarantee. Region allocations must stop being used before release_region, regardless of the parent provider's backing lifetime. Likewise, core/pool.over retains the backing origin in its handle, but its allocation results are independent: callers must keep the backing live until every use of those results ends. The checked arena constructor does not extend these guarantees to other providers or prove allocation lifetimes generally.
The allocator's independent no-from result permits both live allocations and useful helper results. It does not acquire a lexical frame origin when called inside a block. In particular, a helper can store an allocated result in module storage without returning it through that block. A checked arena therefore cannot take frame backing. mem.fail_over follows the same rule. For a local buffer whose use the programmer can keep entirely within its frame, mem.arena_over_unchecked and mem.fail_over_unchecked make the lifetime opt-out explicit at construction. Their allocation results remain independent, even through helpers; the caller must ensure every result stops being used before the buffer ends. Borrowing the mutable allocator for each result would also prevent its next ordinary allocation [0790].
The backing owner arranges its lifetime and explicit cleanup. A frame array used through an unchecked constructor ends with its frame; hosted allocations need explicit release, which defer can run on normal, failure, return and loop-transfer exits [1100]. Nested ordinary blocks and providers do not infer a shared region: disjoint backing has independent capacity, while overlapping backing and outstanding aliases remain the caller's responsibility. Do not use results after backing ends or is reused. Direct and helper-returned pointers, aggregates, slices, any and callback state all obey the ordinary origin rules; none gains an arena-specific escape check. The explicit integer-to-pointer conversion inside allocation is [0470]'s existing non-guarantee, not proof of a longer lifetime. The checked constructor prevents a tracked frame extent from reaching that conversion through the ordinary arena API. Tracked direct frame references and provider handles still cannot escape [0770] [0780].
A view derived from a local borrows it
A view derived from a local borrows it. While the view is still in use, that local may not be handed on as inout or sink, which is what stops a container from moving under a slice into it. The check is local to one function body, so there is nothing to annotate and the fix is always nearby: take the view again afterwards. It only applies where the storage can actually move, and an unchecked region [1120] does not turn it off: origins and escape are among the checks that stay.
grow_and_use: (inout l: list, v: i32) -> none = xs := l.items push(l, v) -- error: l is borrowed by xs use(xs) end grow_and_use
Origins join to the shortest-lived part
Origins join to the shortest-lived part. A struct holding one frame pointer is frame as a whole, and a value chosen between two branches takes the more restrictive of the two. Returns need no analysis across function boundaries at all: frame origin cannot leave a function, so whatever does leave is allocated or static by construction. A value that holds no references at all is unconstrained, which is another reason the idiom is handles and indices. A scalar computed from a view does not retain that view. In particular, saving lenof xs saves a number, not a borrow: the number may be used after xs or its container is changed. This does not exempt an operator's evaluated operands from their checks, or change [0370]'s unevaluated measurements. Taking an address or range is different: that reference still keeps the selected storage's origin [0430] [0790]. Writing through a known address of a local updates the local's tracked origins too. Joining an integer-created pointer with a tracked reference does not hide the tracked origin.
Volatile is exempt from the borrow rule and from every
Volatile is exempt from the borrow rule and from every aliasing assumption. A volatile access is an operation: compiler.volatile_load and compiler.volatile_store for a scalar, and D228's compiler.register_read and compiler.register_write for a register image [0740]. Each performs exactly one access of its width through an ordinary pointer, usually one built from an address [0460] and so outside the lifetime checks already [0470]. Hardware routinely needs several typed windows on one address — a byte view and a word view of the same register — and that is legal, deliberately. D227 says what each access orders and what it does not. D238 records why there is no volatile ptr type: a qualifier would be a second permission on every reference, relaxed and checked beside mut [0440], and the complete prototype-1 driver needed none.
The complete prototype-1 driver derivation uses these ordinary slices with an explicit synthetic device completion contract. Its memory barrier follows acknowledged drain; neither an interrupt nor interrupt masking grants permission to reuse active DMA storage. The derived driver program in compiler/tests/driver owns that executable library protocol, not a new language lifetime guarantee.
What this does not catch
What this does not catch, said plainly: two different arenas are indistinguishable, pointers that travel through a struct field and are read back later, and anything across the C boundary. A view taken through an accessor used to be on this list; [0790] took it off. What is left would need regions or generation counters, and both are deliberately out.
FUNCTIONS
A function is a value of a function type
A function is a value of a function type. No 'fn' keyword. '=' opens the body, 'end' closes it, always.
double: (x: i32) -> (r: i32) = r = x * 2 end double
A single-expression body still takes an end
A single-expression body still takes an end. The expression fills the named return.
triple_it: (x: i32) -> (r: i32) = x * 3 end
Nothing returned
Nothing returned, and never returns:
log_it: (m: utf8) -> none = ... end halt_it: () -> noreturn = loop do end loop end halt_it
none returns normally without a value. noreturn never returns normally and has no checked error set. It is part of function-type identity: a ()->none function cannot stand in for a ()->noreturn function or vice versa. Calls end the continuation, so a branch that calls halt_it() needs no joined value. A body must diverge on every reachable path; a conditional loop is not enough merely because it is expected to run forever.
A call does not unwind defer. A deferred nonreturning call, when its cleanup edge runs, stops later cleanup and the original transfer. Ordinary and C signatures may use this form within the target's supported surface; interrupt and naked signatures retain ()->none. D231 specifies the detailed contract.
A function type is an ordinary type
A function type is an ordinary type, and a function is an ordinary value of it, represented as a code address. There is no separate function-pointer type and addr is not used on functions: the type is written the way the signature is.
handler: type = () -> none mut current: handler = default_handler
The names written inside the signature describe its parameter and return positions; in a type they do not declare local names. Two function types agree by those positions' types, not by their labels. Function values may themselves be parameters and named returns, so that structural comparison is recursive. Convention and variadicness are part of that identity too: a C callback type writes extern(c) (parameters) -> returns, and an ordinary Landin function with the same visible arguments does not agree with it. A pointer to such a function type points to a cell holding the code address, not to C callback code itself. A module binding initialized by a named or anonymous function is a static code address; mutable local or module storage may later receive any address with the same complete signature. A callback is therefore a pair of that and a state pointer, written out because nothing is captured.
on_byte: type = struct call: (state: ptr u8, b: u8) -> none state: ptr u8 end on_byte
A function-valued field is called through its ordinary selection: callback.call(callback.state, byte). It keeps the complete structural signature of the field type through construction, assignment, aggregate copy, variant payloads, nesting and arrays of such structs. The root binding decides whether the field may be replaced; a call evaluates that selected code address before its arguments.
Anonymous functions
Anonymous functions. No capture: their routine may use module declarations, its own parameters, named return and body locals, but no local, parameter or return from the expression's enclosing routine. State travels as an explicit parameter. Forming one produces a static code address; it does not execute the body.
less_i32 := (a: i32, b: i32) -> (yes: bool) = a < b end
Three parameter conventions
Three parameter conventions, and they are about the value you were handed — never about what it points at, which the type says by [0450].
| convention | what it promises |
|---|---|
in | I will not change the value. The default, unmarked. |
inout | I may replace it, exclusively, and the change comes back. Implies mut. |
sink | consumed. The place the caller named is dead afterwards — see [0910], since a place is not always a binding. |
So a function that writes registers through a pointer it will never re-point takes it as 'in' and the pointer type carries the mut. That reads as what it does, which the older rule could not say. Outputs are not a convention: returns are named. Orthogonal to all three, 'escaping' says the callee may keep what a pointer, slice or 'any' refers to beyond the call, so the caller must prove it lives long enough. In obligation it is the opposite of sink: sink ends the caller's duty, escaping extends it. On a generic parameter escaping says the right thing at both extremes with no special case. For t = u32 it is vacuous, since [0840] already leaves a value holding no references unconstrained; for t = ptr node it is exact. The origin travels with the type, so one word covers both. An inout argument need not be a binding. A pointer target, c.inner.val, is an ordinary one. The same provable binding-rooted place cannot fill two inout parameters of one call. Distinct pointer and computed-index paths may still alias; that is beyond the local analysis and is not an exclusivity guarantee [1720].
process: (source: []u8, inout target: []u8, sink owned: buffer) -> (written: u32) = written = 0 end process
sink takes a place
sink takes a place, and a field of a binding is a place, so consuming a container's field needs no ceremony. The path has to be rooted in a binding and contain no dereference and no computed index, which is the line where the analysis is still provable: two pointers may name one place, so p.val.items is refused, and a computed index names none in particular, so xs[i].items is too. A literal fixed-array index stays within the binding's own storage. A slice index follows its backing reference, so it is refused even when its index is a literal. The slice descriptor itself, including one held in a field or fixed array element, is an ordinary place and may be consumed as a whole. Consumption happens when the call begins, after its callee and all arguments have evaluated left to right. Thus use(value, value.count) is allowed when use takes its first parameter by sink. If a later argument returns early, that call has not consumed value. Calls entered during argument evaluation still have their own effects. Each by-value argument keeps the value captured when it was evaluated; a later assignment does not change that captured value. At call entry each sink place must still be live. Repeated or provably overlapping sinks are refused, and no inout argument may then name a provably consumed place. [1910] and D223 give the exact entry rule. A place that was sunk is dead. Reading it before it is assigned again is an error, and that is what closes the window between releasing storage and repointing the field — the window a temporary binding would have left open. Reading the enclosing aggregate also reads that field. Assigning a replacement aggregate restores its fields; unrelated fields and array elements stay live. Say plainly what this is and is not. It is a use-after-consume check on one place. It is not ownership: the value is copyable, so a copy made before the sink is refused nothing, and consuming through both is not caught. Making it ownership means values that cannot be copied. Affine values remain parked with their resource-prototype trigger in ROADMAP.md's register. A place sunk out of an inout parameter must be assigned again before the function returns, or the caller would get its struct back with a dead field and nobody tracking it. This includes a failure the caller recovers from. Applicable cleanup runs before the check, so a defer or undo may restore the field. A failure needs no successful named result under [0930], but it still hands back inout storage.
consume: (t: type, sink items: []t) -> none = ... end consume reset: (t: type, inout l: list(t)) -> none = consume(l.items) -- l.items is dead from here l.items = [] -- and live again from here l.len = 0 end reset
A caller parameter is filled in by the compiler with the call site
A caller parameter is filled in by the compiler with the site of the call, so assertions and logging work without macros. Its type is an ordinary struct with exactly these three u32 fields, in this order:
site: type = struct file_id: u32 line: u32 column: u32 end site assert: (cond: bool, caller where: site) -> none = if not cond then report_failure(where) end if end assert
Used as: assert(count > 0).
The value is 12 bytes on both 32-bit and 64-bit targets. The file number belongs to this compilation; the line and byte column are one-based. Filenames live in a separate table that need not ship with the program. Even without it, an assertion can report a file number, line and column. Resolving the filename requires the table from the matching build. This follows [1670]'s reason for keeping filenames out of the required runtime data; its compiler-check handler retains its own numbered-site interface.
The parameter is immutable and omitted from an ordinary call. A wrapper preserves the original coordinates explicitly, by naming its own caller parameter as the complete argument:
checked: (cond: bool, caller where: site) -> none = assert(cond, where: where) end checked
Omitting where in that inner call deliberately reports the wrapper's call. A copied or constructed value cannot be passed into a caller position. The coordinates may otherwise be read, copied and saved as an ordinary struct.
Multiple named returns
Multiple named returns.
divide: (a: i32, b: i32) -> (quot: i32, rem: i32) = quot = a / b rem = a % b end divide
Two or more returns travel as one anonymous structural result aggregate. Their written order fixes its layout and transport; their names are its fields. A single return keeps its own type rather than being wrapped.
Every named return must be assigned before return
Every named return must be assigned before return. On the fail path they need not be, and the caller may not read them.
A return list is an anonymous struct
A return list is an anonymous struct, so a result can be bound whole and read by field, or destructured. Binding by name, never by position.
whole := divide(10, 3) sum := whole.quot + whole.rem (quot, rem) := divide(10, 3) (quot: q2) := divide(20, 3) (quot, _) := divide(30, 3)
The call is evaluated once. Names may be selected in any order, omitted, or renamed after :; _ explicitly ignores what is not bound. A whole result can also cross an if, match, or bare-block value when every fallthrough edge has the same names and field types. Function types compare result types in order but not these labels, so a call through a stored function uses the labels of its static function type.
Errors: a declared set of atoms in one dedicated register
Errors: a declared set of atoms in one dedicated register.
open_file: (path: utf8) -> (handle: u32) ! not_found | no_access = fail no_access when lenof path == 0 handle = 1 end open_file
Atom and error sets are structural declaration-identity sets: aliases flatten, source order does not matter, and a singleton may widen into a union containing it. In the first Linux x86-64 internal convention ordinary atom values use dense nonzero 32-bit codes, while %r10d carries the error outcome and zero means success. The ordinary result convention is unchanged.
Not everything that goes wrong belongs in that channel
Not everything that goes wrong belongs in that channel, and the question to ask is whether it can be determined from what you already hold. A syntax mistake is entirely in the bytes the parser is looking at, so it is checked, reported and recovered from — a parser that stops at the first mistake hides the other twelve. Out of memory, a file that is not there, a device someone unplugged hang on the world instead of on your data, and checking first would only be a race, so those are what fail is for. Put briefly: check what you can foresee, and where you can foresee it, prefer working around it to reporting it. fail is for what cannot be foreseen or cannot be dealt with where it happens. Reporting needs somewhere to report to, and that is an ordinary parameter. A diagnostics sink is a capability by [1680]: passing one makes the chosen destination explicit. The parameter list alone does not prove that the function cannot construct another sink or acquire a host world.
The parser-support library spells that capability core/diag.log. Its object-safe note entry accepts a byte position, severity and escaping byte slice, and declares io_failed: a bounded implementation never raises that outcome, while a streaming implementation must propagate a failed write. Bounded overflow is not failure. The logger stops retaining entries, increments its dropped count, and continues to observe whether an error was reported. Both implementations can be passed as any diag.log, so the producer does not know which retention or delivery policy its caller selected.
parse: (src: utf8, inout d: diagnostics) -> (tree: ptr node) ! out_of_memory | too_deep = ... end
Propagating: try, visible at the call or traversal site
Propagating: try, visible at the call or traversal site. '...' means: plus whatever my callees can fail with. Not allowed where the set must be concrete: function pointers, concept entries [1260], anything exported to C — and anything public. A public signature is a promise, and one that a change three modules down can rewrite silently is not a promise. Inside a module, where the compiler sees every caller anyway, '...' is a convenience and stays. Mutually recursive private routines are inferred together at their least fixed point; an empty inferred set is infallible. A generic template has no error signature of its own: each concrete argument tuple joins that same fixed point as a separate routine, so equal tuples share one answer and unequal tuples can keep different answers. Recovery and propagation see the finalized set for the selected instance. Passing a recovered error, or a local alias of it, to a generic call waits for that complete set. A circular dependency between selecting the generic instance and inferring its own effects is refused (D215), without guessing a set; ordinary recursive error inference remains supported. Function types and anonymous functions write a concrete set because their complete signature is their type.
read_config: (path: utf8) -> (data: []u8) ! ... = h := try open_file(path) data = [] end read_config
try for item in source do ... end for propagates a declared failure from any traversal step. The enclosing function must declare or infer the provider error atoms. A caller can recover them with the ordinary else clause on that function call.
Handling
Handling, not just propagating: an else clause on the call, binding the error atom. It either yields a value or leaves the function. try is sugar for the second line.
h1 := open_file(path) else 0 h2 := open_file(path) else (e) fail e end h3 := open_file(path) else (e) match e not_found: create(path) _: fail e end match end
The else clause yields a value, or transfers control out of the block that encloses it: return, fail, break, continue. That is the same rule an if-expression arm follows, so an arm that leaves needs no value and nobody has to invent a placeholder to satisfy the type. It does not assign to the binding it is initialising, which would need a rule about writing an immutable binding inside its own initialiser. The success and recovery paths may carry any enabled result shape: scalar, atom, function, fixed array, struct, or anonymous multiple-result aggregate through its ordinary caller-owned storage. A bound error name has the call's complete atom set and is visible only inside its recovery block. An exhaustive atom match may name every atom or end with one _ arm for those left over.
Being an expression, it also works in argument position:
use(read_config(path) else (e) default_config() end)
A call that can fail and whose result is discarded is an error. Write 'try f()' or discard through an else. else is for the error channel only, not for unions.
Calls: positional first, then named
Calls: positional first, then named. No default values. The positional prefix fills parameters in order; the named suffix may reorder the rest. Every runtime parameter is filled exactly once, and an unknown or repeated label is an error. Arguments still run in written order before their checked values are passed in formal order. A generic direct call may put its compile-time actuals in this same named list: copy(t: u8, n: 4, source: bytes). Once it names one static formal, it names every static formal; those entries neither evaluate nor fill a runtime position. A call through a stored or selected function uses the parameter labels of that value's static function type; changing those labels does not change function-type identity [1000]. Named arguments work in a call statement as well as in a call expression. A C variadic call additionally supplies an unnamed tail under [1580]'s default promotions; this does not add optional or defaulted fixed parameters.
r1 := divide(10, 3) r2 := process(source: src, target: dst, owned: buf)
CONTROL FLOW
demo_flow: (x: i32, items: []i32) -> (out: i32) = out = 0
Branch
Branch. 'then' closes the condition, which must be bool.
if x > 0 then out = 1 elsif x < 0 then out = -1 else out = 0 end if
Every construct has a one-line form
Every construct has a one-line form. No newline is ever required; 'end' closes.
if x > 0 then out = 1 end if
A declaration is allowed in a condition
A declaration is allowed in a condition. It is a plain type error unless it is bool. The initialized forms of an inferred or typed binding may be used by if, elsif, and while; the binding is visible in the one body that condition guards and nowhere after it. Its initializer is evaluated before the name exists, and a while evaluates and initializes the binding again before every iteration.
if ok := is_ready() then out = 1 end if while mut more: bool = has_more() do consume_next() more = false end while
A block has the value of its last expression
A block has the value of its last expression, and that is the whole rule: if, match, else clauses, bare blocks and loops are expressions wherever one is wanted. Rust needs a semicolon to decide whether the last line is the value; here discarding is already explicit with '_ =', so a bare expression at the end is unambiguous. Every reachable edge that falls through supplies that last expression with one complete type and shape. An edge that leaves by return, fail, break or continue supplies no value and needs no placeholder. Only fallthrough edges join their definite-assignment facts; an edge that has already left cannot prove a read on a surviving sibling. Statements and the final expression run in source order, after an if condition or match subject and only in the selected arm.
sign := if x > 0 then 1 else -1 end if doubled := begin a := expensive() a * 2 end
Bare block, for scoping
Bare block, for scoping: begin ... end. A label turns it into something a break can leave early — name: begin ... end name — with its cleanups run as a loop's are [1180].
begin tmp := x * 2 out = out + tmp end
defer runs at the end of its block, in reverse order
defer runs when its block is left, in reverse order. That includes ordinary fallthrough, a successful return, or declared failure through the block; a trap is a stop and does not unwind anything. A nested block runs its own entries before an outer block's, and an entry is active only after control has reached its statement. The complete call is evaluated where it runs, not where it was written: a direct or indirect callee, including a selected function field, and then its arguments. It names places, and reads them then. So a defer that sinks something sinks it at the end, which is why the thing stays usable in between — and if a named place has been re-pointed by then, the defer sees what is there now. The block's final value or failure atom is formed before these calls run. Registering one evaluates no callee or argument, costs nothing and reserves nothing.
defer cleanup()
undo is the same machinery under a condition
undo is the same machinery under a condition. It is registered when control reaches it and runs only if declared failure leaves its block — a direct fail, a try that propagates, or failure arriving through a deeper call. Nested blocks run inner entries first. Within one block defer and undo share one registration order, so the applicable calls interleave in lexical reverse order rather than forming two batches. Like defer, an undo evaluates its callee and arguments only when it runs, in source order, and therefore sees their late values.
Ordinary fallthrough and a successful return skip undo. A call-site else that recovers locally does not leave the caller's block and skips its undo entries; entries in the failing callee have already run before its failure arrives for recovery. Break does not select undo, and neither does a panic: panics stop and do not unwind anything.
It is its own word rather than a flavour of defer, because it is not the same thing: to defer is to do it later, and this may never be done at all. English calls it a contingency, or a compensating action.
undo release(handle)
Because entries are registered where control reaches
Because entries are registered where control reaches them, the triangular cleanup of several fallible acquisitions falls out of the order instead of being written: the first failing frees nothing, the second frees one, the third frees two.
new_buffers: (provider: type is mem.allocator, inout state: provider, count: usize) -> (first: mem.byte_buffer, second: mem.byte_buffer, third: mem.byte_buffer) ! mem.out_of_memory = first = try mem.new_bytes(state: state, count: count) undo mem.drop_bytes(state, first) second = try mem.new_bytes(state: state, count: count) undo mem.drop_bytes(state, second) third = try mem.new_bytes(state: state, count: count) -- All three owners are published by the successful return. end new_buffers
And the discipline it asks for, which has to be said out loud: undo cleans up what is still yours. Once a resource has been handed on, its cleanup is somebody else's, but the entry is still registered. So acquire everything fallible first, commit afterwards, and let nothing fallible stand between the commit and the end of the block. Where that order cannot be had, the entry would have to be called off, and there is deliberately no way to do that — a construct for calling one off turned out to be bookkeeping for a question the block's exit already answers. The pattern this replaces is a flag and a conditional defer. That is linear rather than quadratic, so it was never about the number of lines. It is that forgetting to set a flag frees storage that is still in use, and under an arena, where free does nothing, the mistake is silent until somebody runs the same container on a real allocator.
Checks may be switched off for a region, visibly
Checks may be switched off for a region, visibly.
unchecked begin out = out + items[0] end unchecked
The region is a statement and an ordinary block: it has its own scope, defer still runs on the way out, and nesting one inside another says nothing new. What goes is the edges the compiler emits and nothing else — integer overflow, an element index or a slice range, and the destination range of an integer conversion. What stays is everything the compiler decides rather than emits — types, definite assignment, permissions, origins — and the edges whose absence is not one thing on every machine: a zero divisor, a negative shift count, a conversion to bool or from a float, a text boundary, and a zero integer-to-pointer result after target-width conversion. It reaches only the code written inside it, so a function called from a region is checked as that function is written, and a deferred call is checked where you wrote it rather than where the exit that runs it stands. That is what keeps the word honest where you read it. D187 is where each of those is decided.
Conditional loop
Conditional loop.
mut i: u32 = 0 while i < 10 do inc i continue when i == 3 end while
Traversal
Traversal. Bindings default to in; inout implies mut. An integer range is ascending. Either typed integer bound supplies the type of an untyped integer bound on the other side; two untyped bounds take i32. Thus 0..<lenof items traverses usize values without a conversion. The bounds are evaluated once, left to right; ..< excludes the upper bound and .. includes it. The optional index is a usize beginning at zero. Collection traversal supplies the same binding shape, with writability decided by [1160]. A collection element may itself be a fixed array, slice or any C; the loop binding keeps the complete element shape or erased concept identity and evidence. For a struct or any C source, the loop selects one conformance to [1320]'s named iterable concept. The source expression runs once. first produces one cursor; at_end, item, and (after fallthrough or continue) next then drive each iteration in that order. The optional usize index still begins at zero and advances with next. For a source whose traversal can fail, write try for. It first selects [1320]'s fallible_iterable evidence; an ordinary iterable is also accepted. Each provider failure leaves the loop through the declared error channel, running active defer and undo cleanup. A failure does not enter complete or call any later provider. The loop body can use break or continue as usual; next runs only after body fallthrough or continue. The exact utf8, utf16, and cstring identities have distinct intrinsic conformances to that same four-operation contract. Their cursor is a private usize byte or UTF-16-code-unit offset and their item is the decoded Unicode scalar as u32, not an alias or encoded subview. cstring's first NUL is the end and is not an item. Foreign C text follows [0600]'s validation and trap boundary. The source still runs once, and provider order, index advancement, cleanup and loop transfers are the same as for ordinary iterable evidence.
for item in items do out += item end for for item, idx in items do item = item + i32(idx) -- items is []mut i32, so this writes end for for k in 0..<10 do out += k end for
There is no marker on a loop binding
There is no marker on a loop binding, because the type already decided: over a []mut t an element is a writable place, over a []t it is not. Over anything else that satisfies iterable the binding is a copy, since item at [1320] hands out a value — so assigning to it is an error rather than a silent write to nothing. The cursor and item keep their exact declared type identities. In particular, an any C item keeps C and its evidence, while an any C source remains an erased value passed to the providers; neither pair is interpreted as a slice. Text traversal likewise yields an immutable copied u32; binding mutability cannot make either the item or the hosted view writable. When t is a fixed array, slice or any C, replacing that whole element follows this same rule. Writing through a slice element still follows the mut permission carried by that inner slice. An any C element remains an erased value and evidence pair; it is not a slice.
xs := vec.used(l) -- []mut i32 for k in 0..<lenof xs do xs[k] = xs[k] * 2 end for
complete runs when the loop finished without break
complete runs when the loop finished without break.
for item in items do break when item == 42 complete out = -1 end for
Labels use the ordinary name form
Labels use the ordinary name form, on loops and bare blocks only. break and continue take one.
outer: for a in items do for b in items do break outer when a == b end for end outer
A break naming a bare block leaves it and whatever loops it crosses, running their cleanups on the way out. A bare block has no next iteration and yields no value, so continue and break with still name loops, and an unlabelled break or continue still means the innermost loop (D234).
search: begin for item in items do break search when item == 0 end for all_nonzero = true end search
break carries a value with 'with'
break carries a value with 'with', which is what makes a search an expression instead of a mutable variable and a flag. Every break of a loop used as an expression must yield the same type, and complete supplies the value for running out; loop do needs none, having no other exit. A loop whose result is unwanted uses an explicit discard [1020], as in _ = loop do break with 1 end loop. A statement loop uses plain break; it cannot silently discard a with value. 'with' is required because a bare identifier after break could otherwise be either a label or a value.
found := for item in items do break with item when item > 40 complete break with 0 end for out += found end demo_flow
Pattern matching
Pattern matching: constant patterns, case patterns with binding, and the wildcard. No fallthrough; list several labels instead. A missing case is a compile error. The constants a pattern names are atoms [0630]: a subject is an atom set, a variant part or a pointer union [0480], and an arm names an identity. A number has no identity to name, so it is compared with if and elsif rather than matched (D241).
area: (f: figure) -> (a: f32) = a = 0.0 match f.kind circle (radius): a = 3.14159 * radius * radius rectangle (width, height): a = width * height end match end area
A pattern binding carries a parameter convention
A pattern binding carries a parameter convention: in by default, inout to write into the payload that was matched. Nothing new is needed, because the conventions of [0900] are already the mechanism — and without them a [capacity]t payload would be copied in order to be read and could not be written at all. Both plain and inout payload bindings alias the matched storage, including scalar payloads. By [0830], an arm may replace the variant or a containing object only after the aliases' last use. Writing through an inout alias is a use, as is reading an address derived from it or evaluating a pending cleanup argument. A separate scalar copy keeps no borrow of the payload. A computed-value match has D134's separate temporary; replacing the original does not replace that temporary's payload. A direct case assignment selects its new tag before evaluating payload initializers (D76), so an initializer cannot use an alias of the old payload. For a whole containing construction, save needed scalar values before starting the construction too.
spill: (inout s: store, v: i32) -> none = match s.kind inline (inout buf): buf[0] = v spilled (heap): use(heap) end match end spill describe: (c: compass) -> (name: utf8) = match c north, south: name = "along" _: name = "across" end match end describe
CONCEPTS AND GENERIC CODE
A concept names a bundle of requirements on a type
A concept names a bundle of requirements on a type.
ordered: type = concept (t: type) less: (a: t, b: t) -> (yes: bool) end ordered
A conformance registers one type against one concept
A conformance registers one type against one concept.
i32 is ordered (less: less_i32)
A conformance for a parameterised type binds its variables
A conformance for a parameterised type binds its variables in front, because the conformance holds for every one of them. The binder is an ordinary parameter list — the same form functions take and the same form a parameterised type takes — so a variable may carry a constraint, and a fixed value parameter may appear among them.
(t: type) list(t) is iterable (cur: usize, item_type: t, first: list_first, at_end: list_at_end, item: list_item, next: list_next) (provider: type is allocator) counted(provider) is allocator (alloc: counted_alloc, grow: counted_grow, free: counted_free)
The functions supplying the entries are generic themselves. Instantiating the conformance supplies their type argument and leaves a function of exactly the concept's shape, so nothing beyond the binder is needed. Free variables are never quantified by inference. A name that is not in scope is a misspelling, not a new variable, which is the rule build options already follow at [1530]. Without this there is no generic container that can be traversed, sorted or handed to any other generic code.
A concept entry's error set has to be concrete
A concept entry's error set has to be concrete. The entry is reached through a table, [0960] forbids an inferred set there, and so a concept fixes the error set once for every type that will ever satisfy it. That is right for allocation — out of memory is out of memory — but it is a constraint on how concepts are designed, not a detail. The same holds one level up, for what an implementation may ask for. A concept with no allocator among its parameters means no implementation of it can allocate, however much one of them would like to. Resist widening the concept until it fits the hungriest: that hands every implementation the sum of everyone's needs. Either the hungry one picks a shape that does not need it, or there are two concepts.
The register is keyed by (type
The register is keyed by (type, concept, input types), so a type may satisfy the same concept more than once. Which concepts exist, and how finely they distinguish what an operation costs, is the standard library's business. The language checks nothing about cost and knows no vocabulary for it: a guarantee should not depend on how clever the compiler happens to be.
indexable: type = concept (t: type, idx: type, item_type: type) get: (s: t, i: idx) -> (item: item_type) end indexable utf8 is indexable (idx: usize, item_type: u32, get: utf8_nth) utf8 is indexable (idx: position, item_type: u32, get: utf8_at_pos)
Conformances may be declared anywhere
Conformances may be declared anywhere, and two for the same key are simply an error. There is no precedence, no orphan rule and no override: until a rule turns up that is worth its weight, the compiler reports the collision and somebody sorts it out. A wrapper through distinct, or passing the functions explicitly at the call, both work without anyone changing anyone else's library. The alternative that was tried and dropped had libraries declare weak conformances and applications override them with strong ones. It worked, but it meant an application could quietly change the behaviour of generic code inside a library, which is a strange thing for a language whose whole point is that costs and effects are visible.
The conformance register collects every source file before constrained instantiation. It matches labels to all input types and direct entries, checks concrete supplying functions against the substituted entry signature, requires separate conformances for every composed parent, and diagnoses an ordinary collision even across files. A parameterized conformance quantifies one complete nominal type family: the target applies the leading binder once, in the same positions and kinds as the nominal declaration. One such family owns the target-template/concept space, so another family or a concrete exception is a collision rather than specialization. Lookup binds that family and records one concrete key; the generic evidence schema then turns the retained generic supplying functions into physical evidence entries.
In a program of modules, “every source file” means every module reachable from the entry directory after ordered-root selection, including unused conformances and excluding unreachable and later-root-shadowed directories. Conformances are registrations rather than imported names: every reached one enters the single program register without public, while public on a conformance is refused because it declares no module member.
A generic function takes the type as an ordinary parameter
A generic function takes the type as an ordinary parameter, constrained by 'is'. Type and fixed parameters are compile-time, and their order in the list does not matter: a parameter may be used in the type of one that comes before it, exactly as declarations inside a module may. The compiler collects the names first and resolves the types afterwards, so report: (inout d: sink(capacity), fixed capacity: u32, ...) is as good as putting capacity first, and at the call site capacity is deduced from whatever argument pins it down. Concept entries are reached through the type parameter, so two constrained parameters never collide: left_type.less, right_type.less.
The kernel admits the same collected signature scope with unconstrained type formals and fixed integer formals. A direct call either leaves every static formal for deduction, or names every one explicitly in its single call list; partial explicit/deduced tuples are refused. A direct call recursively matches each written runtime parameter type against the argument's independently synthesized normalized type. Direct type formals bind complete descriptors, including the exact concept of an any value. Pointer and slice patterns match permission and ordinary view exactly, then match their complete referents recursively; deduction does not perform reference relaxation. Fixed arrays match exact bounds and elements; a direct fixed bound binds its length, while a computed bound such as capacity * 2 is never inverted and is checked only after another occurrence has bound capacity. Parameterized nominal patterns require the same source template and match their complete stored tuple, including phantom actuals. Parameterized aliases expand symbolically, and function patterns match parameter and result runs plus their error form while ignoring labels. Repeats must agree exactly. A saturated explicit static tuple bypasses deduction and validates the same patterns. No return context, conversion, constraint search or user code participates. The compile-time formals create no runtime parameters or ABI positions. A generic routine name is still template syntax rather than a standalone function value; a direct call is what selects an instance. Private ! ... is inferred for that concrete instance before its body is lowered. Constraints and evidence use the completed whole-program conformance register described in [1230]–[1280].
Inside a concrete routine instance, a fixed formal may also be used as an ordinary expression of its declared integer type. Each such use is replaced by the instance's compile-time value; it still creates no runtime argument or ABI position.
sort: (t: type is ordered, data: []mut t) -> none = for k in 1..<lenof data do mut j := k while j > 0 and t.less(data[j], data[j - 1]) do tmp := data[j] data[j] = data[j - 1] data[j - 1] = tmp dec j end while end for end sort
At the call site the type is inferred from the arguments
At the call site the type is normally inferred from the arguments. It may also be named explicitly in the same uniformly named list as runtime arguments; an explicit static list is saturated, while the runtime positional prefix and named suffix keep [0980]'s matching rule. Positional-only calls remain the all-deduction form.
sort_demo: (values: []mut i32) -> none = sort(values) sort(t: i32, data: values) end sort_demo
Evidence is the foundation; specialization is optional
Evidence tables are the semantic foundation, not an optimisation fallback that source correctness may depend on. D211 distinguishes the concrete instances required to represent source types from optional specialization of their dispatch. The bootstrap retains concrete representation bodies; it does not promise one erased body for every possible by-value representation.
A static T.entry selection names one declaration across its concept and inherited constraints. A shared ancestor reached twice is still one concept; two different concepts declaring that name make the selection ambiguous. Reordering parents does not select an operation, and a direct child entry does not override an inherited one. An unused colliding name does not prevent the concept from being declared or conformed to; D221 records the exact boundary.
--optimize=none|size|speed and --specialize=off|auto|all are independent axes, defaulting to size and auto, independent also of --build-mode. Specialization needs proof of actual incoming evidence, not merely the table expected for a semantic instance. Unknown incoming tables, address-exposed instances and heterogeneous any calls keep indirect dispatch. A single eligible normalized instance bypasses profitability, not proof; all does the same for every eligible instance. No call is specialized merely because its source type is known.
The deterministic policy counts proved entry-call sites and their source loop depth, represented target bytes and weighted IR growth. D211 states the caps, weights and thresholds: size raises the bar and speed lowers it. none turns off simplification and allocation, not independently requested specialization; its automatic specialization uses the size threshold. Identical emitted bodies may share only when their complete machine meaning and address identity permit it. A smaller body does not authorize changing a C or Landin convention.
The evidence includes target size and alignment as well as functions. A checked concrete routine instance keeps hidden evidence positions only for tables read by direct constrained member selections in its body. An erased-only any construction uses a static table and needs no hidden evidence position. A proved direct entry call retains the positions selected by that body and any aggregate-result position; specialization does not erase a used ABI position. --build-report=PATH writes factual decisions, retained ABI/fallback, layout savings and frame/register/stack-traffic evidence separately from source diagnostics. It introduces no runtime report storage. Measured object bytes come from the Linux object-quality harness, not an IR cost estimate.
The bootstrap checks a constrained routine against its full direct and constraint/parent closure, but a concrete call passes only the table positions selected by direct constrained member calls in that instance's checked body. If two type formals use the same conformance, selecting one passes one hidden pointer; selecting both retains their separate positions. Those pointers follow depth-first concept declaration order within each formal, then generic-formal order. A body with no such selection has no hidden evidence arguments, even if it constructs any values. A table begins with the represented type's target usize size and alignment, then carries direct concept functions in concept declaration order. t.entry(...) loads that function word and uses the ordinary indirect-call and error conventions; the static type formal still occupies no source ABI position. Linux x86-64 emits pointer-width table cells, while the same semantic positions are laid out from the synthetic 32-bit target facts. The baseline backend folds two instance symbols onto one machine body only after a deliberately narrow IR comparison proves every retained operation has the same physical meaning; differing representations or signed operations remain separate concrete bodies rather than making sharing a correctness assumption. D211 adds optional proved entry-call specialization without weakening that foundation.
Traversal is a concept
Traversal is a concept. This is what 'for x in s' uses.
iterable: type = concept (t: type, cur: type, item_type: type) first: (s: ptr t) -> (c: cur) at_end: (s: ptr t, c: cur) -> (yes: bool) item: (s: ptr t, c: cur) -> (v: item_type) next: (s: ptr t, c: cur) -> (c2: cur) end iterable
The conformance supplies cur and item_type as associated type inputs. Those identities and the four infallible signatures above are exact. Provider labels may be written in any order, but calls use concept declaration order.
A streaming source supplies an error atom type as a third associated input:
fallible_iterable: type = concept (t: type, cur: type, item_type: type, errors: type) first: (s: ptr t) -> (c: cur) ! errors at_end: (s: ptr t, c: cur) -> (yes: bool) ! errors item: (s: ptr t, c: cur) -> (v: item_type) ! errors next: (s: ptr t, c: cur) -> (c2: cur) ! errors end fallible_iterable
The errors input is an atom set. All four signatures are exact and use that same set; a provider may declare an error it never raises. try for requires one unambiguous conformance. If both contracts are available, it selects fallible_iterable; plain for selects only iterable. The source expression is evaluated once and retained as a private value. Each provider receives a read-only pointer to that value, so a large struct is copied once for traversal rather than on every call. item returns a fresh loop binding value rather than an alias into it. A provider result declared from s does not match this source-free requirement. Containers that expose storage-derived references can instead return an initialized slice view and use the built-in slice traversal, as for value in vec.used(list) does.
Integer range traversal uses bounds
In for i in 0..<10 do, the two integer bounds are evaluated once and the loop traverses them directly [1150]. Neither 0..<10 nor 0..9 is an expression or an ordinary value from core; the spellings also occur between slice bounds [0360]. The range header does not select [1320]'s iterable conformance.
A type that needs custom stepping can expose an ordinary struct value with its own iterable conformance. A library could provide a step function returning such a value, then callers could write for i in step(0, 10, 2) do ... end for. This describes a possible library API; core does not currently provide step or step_range.
Concepts compose
Concepts compose. A combined concept is named, so a table is generated per (type, concept) and the value stays two words. Conformance is declared explicitly, even when the body is empty.
drawable: type = concept (t: type) draw: (self: ptr t, target: ptr mut canvas) -> none end drawable clickable: type = concept (t: type) click: (self: ptr mut t, x: u32, y: u32) -> none end clickable widget: type = concept (t: type) is drawable, clickable focus: (self: ptr mut t) -> none end widget button is drawable (draw: button_draw) button is clickable (click: button_click) button is widget (focus: button_focus)
All three, because the composed declaration supplies only what it adds. Nothing is inherited by having the parts.
Types take parameters through a declaration form of their
Types take parameters through a declaration form of their own, not through a function that returns a type — that would be the compile-time evaluation this language does not have. Substitution, not execution.
list: type (t: type) = struct items: []t len: usize end list
An alias body may also unite atom sets after substitution. Declared atom names supply their singleton types, aliases flatten, and ordering or repeating members does not change the set. The same normalized result can name a function's error set or a generic actual. missing | ptr t retains the one-atom optional-pointer representation [0480]; substitution does not enable larger tagged unions.
The enabled declaration form takes type parameters, each optionally carrying one direct concept constraint, and fixed integer parameters. Applications are fully applied and positional. A type actual may be any enabled concrete identity; whether it is legal as a field, array element or payload is checked after substitution. Fixed actuals are integer literals, or fixed formals forwarded by another template, and must fit their declared integer type. D142 checks each constrained concrete application through the whole-program conformance register. D138 applies the same substitution model to direct generic routine calls through exact argument deduction or a saturated named static list, and D139 selects module declaration lists with a closed fixed condition; none of the three mechanisms executes user code.
map: type (key_type: type is hashable, value_type: type) = struct ... end map small: type (t: type is zeroable, fixed capacity: u32) = struct ... end small
The two constrained examples above use D142's enabled direct constraints. The rest uses the same substitution model: no user routine runs, each canonical nominal instance gets the target layout of its substituted fields when a value site requires one, and neither the template nor its formals acquire per-instance runtime state. Repeating an application whose substituted layout fails repeats that application-local diagnostic; it does not create a second nominal identity. Before any application exists, the same walk keeps transient symbolic nominal obligations long enough to see a used formal pass one recursively by value through another template. A phantom formal or function-signature mention does not promote that obligation, and no symbolic walk guesses an actual or annotates the template. An alias application may also normalize to one of those nominal instances. The alias adds no identity of its own; mem.storage(t) can therefore publish a name for a private raw(t) identity while the private template still decides whether its fields are accessible.
Allocation is an ordinary concept
Allocation is an ordinary concept, so the same container runs on the heap, in an arena, or on a fixed buffer with no dynamic allocation at all. A failing allocator makes the out-of-memory paths testable, which almost nobody bothers with in C because it is too awkward.
allocator: type = concept (provider: type) alloc: (inout a: provider, size: usize, alignment: usize) -> (p: ptr mut u8) ! out_of_memory grow: (inout a: provider, p: ptr mut u8, old_size: usize, new_size: usize, alignment: usize) -> (grown: bool) free: (inout a: provider, p: ptr mut u8, size: usize) -> none end allocator push: (t: type, provider: type is allocator, inout l: list(t), inout a: provider, escaping v: t) -> none ! out_of_memory = ... end
The allocator is threaded, not stored in the container, and the reason is stronger than visibility: a stored allocator makes the type list(t, provider), so a list in an arena and a list on the heap become different types and no function takes both. Threading keeps the type parameterised by t alone, and costs one argument at every call that can allocate. grow may extend the same block to a larger byte extent without moving it. A refusal returns false with the block and provider unchanged, so a container can try a fresh allocation. An arena can use this when the block is its latest allocation; providers without in-place growth return false.
The parser-support modules use this exact interface. core/mem.arena is a monotonic provider over an explicit extent. The separately imported core/pool provider divides caller backing into uniform aligned slots and uses a caller-supplied initialized metadata slice as its explicit finite bookkeeping capacity. Exact frees reclaim slots for lowest-index-first reuse; there is no hidden heap or fallback. A caller sizes the slots for its largest allocation and supplies enough metadata for its maximum simultaneous live set, including the six extents a transactional map rehash may need.
core/failing.counted(provider) retains a mutable pointer to any supplied allocator and gives it a deterministic allocation-attempt budget. Calls within the budget are delegated, including failures from the inner allocator; later calls report out_of_memory without delegation. Attempts, delegations, successes, both kinds of failure, frees and live allocations remain observable across a reset budget. Because free has no result, its live count is exact under valid-free use and cannot discover an inner provider's rejection of a malformed free.
The separately imported hosted core/heap provider reaches libc through [1975]'s hosted C bridge, returns independent aligned blocks, and really releases each block. It accepts every usize alignment, treats zero and one as byte alignment, gives a successful zero-byte request a distinct non-null freeable token, and reports an unrepresentable request or host refusal as out_of_memory. core/vec.list(t) stores an honest mem.storage(t): reserve copies its initialized prefix into a private replacement, rolls that replacement back on failure, drains and frees the old allocation only after the copy succeeds, and publishes last. push, pop, indexed get, length, capacity and release are the minimum parser slice. A non-zeroable pointer element is its executable case.
core/map.map(key_type, value_type) is the ordinary open-addressed map. key_type is hashable uses the separately declared equatable and composed hashable conformances; composition does not synthesize the parent conformance [1340]. Construction, insert, get, remove, length, capacity, entry enumeration and release are its public operations. get returns value_type from map, while retained pointer keys and values enter through escaping parameters. The implementation keeps fully initialized bucket records beside dense initialized key and value prefixes, so neither key_type nor value_type needs a zero image. A removed entry remains initialized storage until its tombstone is reused, rehashed away or the allocation is released; resource ownership of elements remains manual.
entries() creates an enumeration cursor. next_entry receives the map and an inout cursor and returns a key/value entry(key_type, value_type) from map, or reports end_of_entries. A complete walk follows only live bucket links in insertion order, so its work follows the number of live entries even after removals. References in a returned entry still derive from the map; scalar copies do not retain a view [0840]. The cursor is a manually managed position, not a checked map/generation identity: restart after any mutation, and do not resume a cursor on a different map. Local reference checks do not replace that protocol.
map is a public struct composition, not an encapsulated or deep-safe object. Its bucket, key and value storages, counters and live head/tail are public fields. The private identity of the bucket record and the opaque representation of mem.storage do not make the map private: mem operations expose the typed initialized key/value prefixes, including removed dense entries, and inferred views can copy and overwrite whole bucket records. Code that composes below the map operations must manually preserve equal storage capacities, a fully initialized bucket array, paired key/value prefixes, exactly one used or dead bucket with a valid index for each dense position, counters equal to the numbers of used and dead records, and a linked walk through exactly the used buckets. The compiler does not enforce those container invariants.
The supplied equality must be an equivalence relation. Equal keys must produce the same hash, and equality and hash results for a stored key must remain stable for as long as it is in the map. That stability obligation includes state reached through a pointer or reference inside a key: mutating such a referent can invalidate lookup just as directly as overwriting the exposed key prefix. These are semantic caller obligations, not compiler proofs.
Every lookup, removal and insertion probe is bounded by capacity, including a full or all-tombstone table. Insertion first searches for an equal key and updates its dense value without consulting the allocator, even with a preceding tombstone. This same search remembers the first tombstone and free bucket; an absent key uses the remembered position without probing again unless growth rebuilds the table. Hashes are reduced modulo capacity as u64 before conversion to usize, using an equivalent full-width mask for power-of-two capacities. Load pressure uses checked-equivalent arithmetic that cannot overflow. An absent key reuses an available dead or free bucket while tombstones remain, without allocation. A crowded tombstone-free table grows. Growth rehash preflights all byte extents, acquires bucket, key and value storage in that order, migrates only used records into a private replacement, and publishes only after every fallible step. Failure of acquisition one, two or three consequently releases zero, one or two replacement allocations while leaving the old map unchanged. Successful rehash retires all three old extents exactly once.
Vector reserve checks that its capacity times the item size fits usize before calling the allocator, and push checks geometric capacity growth before doubling. An impossible request reports out_of_memory without changing the old list or calling the provider. Copy and drain use ordinary loops, so stack use does not grow with the initialized count. Zero-sized items keep logical length and capacity: each nonzero-capacity allocation is a zero-byte request paired with a zero-byte free. Releasing capacity zero, including repeated release, makes no allocator call. These rules preserve the raw initialized prefix and publication order rather than exposing spare capacity as a slice.
core/small.small(t, capacity) is the corresponding inline-capacity shape, with the written t is zeroable constraint [0550]; a pointer item is therefore rejected even though core/vec accepts one. Its public wrapper contains private storage: an [capacity]t array whose unused slots have no readable item image, a count for its initialized prefix, a separate core/vec.list(t) spill descriptor, and a spilled flag. A first spill reserves a fresh list, copies the used inline prefix through an escaping inout container parameter, admits the new value, then publishes the descriptor and flag. Later growth uses core/vec growth. Zero inline capacity uses eight as its first nonzero capacity; otherwise first spill doubles capacity after a checked usize bound. Failed first spill or later growth leaves the published storage, length, capacity and initialized values unchanged.
small.used takes the container inout and returns its initialized writable prefix from that place, whether storage is inline or spilled. The ordinary live-view rule [0830] consequently blocks spill and release until the view's last use. pop removes the tail without moving a spilled allocation; release frees an owned spilled extent exactly once and restores the empty inline state by resetting metadata, without clearing the array. This is still [0860]'s shallow local guarantee: storing references through some other alias would not establish whole-program escape safety.
The two core/mem arena providers align the absolute returned address, not merely the offset within their caller-supplied extent. An alignment of zero is the same request as byte alignment, and a size of zero is valid: it returns an aligned point, may consume the padding needed to reach that point, and counts against a failing allocator's successful-allocation budget. Exhaustion and an unrepresentable address, rounding step, or allocation end all report out_of_memory before changing the monotonic offset, budget, or counters. These are provider contracts rather than stronger guarantees for the unsafe backing pointer and stated extent [1720].
RUNTIME DISPATCH
'any C' is a value of some type satisfying concept C
'any C' is a value of some type satisfying concept C, decided at run time: a data pointer plus a concept table, two words. That pair is an ordinary copyable value and can sit in a variable, a field or a slice. What it points at is never copied, because its size is unknown; the pointee has to live somewhere the pair outlives, typically an arena. The pair needs no permission marker of its own: the concept's entries already carry it. An entry declared with self: ptr mut t can only be satisfied by a pointer that has the permission, so a stateful implementation behind runtime dispatch works and nothing had to be invented for it. What the pair does carry is the origin of the pointee for [0840], so an 'any' over something with frame origin cannot be put in a list that outlives the frame.
Building one is explicit
Building one is explicit. The concept comes from context where it can; otherwise name it.
make_widget: (provider: type is mem.allocator, inout a: provider) -> (item: any widget) ! mem.out_of_memory = initial: button = (text: "OK") b := try mem.new(state: a, value: initial) item = any(b) end make_widget
Calls go through the table
Calls go through the table, with the data pointer as the first argument. This is the only place that reads like a method call.
paint: (items: []any widget, target: ptr mut canvas) -> none = for w in items do w.draw(target) end for end paint
The enabled form reserves any and gives any C the identity of that direct concept, not of one hidden concrete type or one of C's parents. any(pointer) uses the contextual C; without one it requires exactly one collected exact conformance. Every erased entry has an object-safe first self: ptr t or self: ptr mut t, and hidden t appears nowhere else in the entry's runtime signature. Construction from a read-only pointer therefore cannot make a table that exposes mutable self. Once made, binding mutability controls replacing the two-word pair, not the authority already carried by its pointer.
Composition keeps its separate conformances while the erased physical table flattens their object-safe function words in deterministic declaration order. The pair remains exactly data pointer then table pointer on every target. Its origin is the pointee's origin, including through copies, aggregate fields, arguments and results; the implicit self participates in the same from, escaping and local-origin checks as a written pointer argument (D145--D147).
This is what generics cannot do
This is what generics cannot do: one array holding values of different types. []t is always one t.
MODULES
A module is a directory
A module is a directory. Every file in it sees the others with no import. Two levels of visibility: module-internal (the default) and public. No separate interface file.
The compiler reads every direct .ldn child in bytewise filename order. Other files are ignored and subdirectories are modules of their own. An empty directory is therefore a legal empty module. Declaration order does not affect visibility. Canonical file order fixes identities and diagnostics and determines the order of active linker.library directives [1590]. Public functions, bindings, atoms, types and concepts may be named through an import; a variant case inherits whether its containing type is public. A public declaration may mention a private declaration, whose identity can flow through that public surface but remains unnameable by an importer.
An import path is a directory path, and nothing cleverer
An import path is a directory path, and nothing cleverer. 'import net/http' looks for a directory net holding a directory http, under each of the import roots the compiler was given, in order, and takes the first that has it. From that directory it takes every source file — not recursively — and follows their imports in turn. A subdirectory is a module of its own, reached by its own path. What is bound is the last segment. Which is why a segment has to be a plain identifier: lowercase letters, digits and underscore, not starting with a digit. Lowercase because a case-sensitive and a case-insensitive filesystem must both agree on what a name is; an identifier because the last segment becomes a name in the importing file, so a hyphen would need an alias every time. Nothing else — no dots, no spaces, no case, no characters that some filesystem somewhere will not carry. The separator in source is always '/', on every host. The compiler turns it into whatever the filesystem wants.
The module loader requires each directory entry to have the exact lowercase spelling in the import even on a case-insensitive host. It loads the entry module first, then its sorted files, follows imports in source order and loads newly reached modules first-in-first-out. A module is loaded once, so cycles are legal and terminate. A later root is not merged after an earlier one matches.
A plain import binds only its final segment as a namespace in that source file. import net/http therefore makes http.get, http.response and http.drawable qualified declaration references. The same qualification is available wherever a value, type, type application, concept, conformance, error atom or match case is named. The first dot after an imported namespace selects a public declaration; later dots are ordinary value selections. The namespace itself is not a value, type, place or first-class object.
import net/http
Alias, for collisions
Alias, for collisions.
import net/http as h
The alias replaces the last segment as this file's namespace name: this import binds h, and does not also bind http. as is contextual, so it remains an ordinary identifier outside this suffix.
Pull selected names into scope, by name
Pull selected names into scope, by name. No wildcard.
import net/http (get, post)
This import binds only get and post, not the http namespace. Every selected name must be public and must exist, even if the file never uses it. Each keeps its original declaration's identity, type and mutability. The list is nonempty, with no trailing comma, wildcard or renaming; selection and an alias are alternative import forms and cannot be combined.
Imports are per file, so every file reads on its own
Imports are per file, so every file reads on its own.
They form a prelude before every declaration. One file's imports are neither visible in a sibling nor re-exported. A repeated import name, including the same import twice or a collision between a selection and an alias, is an error in that file. A parameter or local may shadow an import; the import in turn shadows a same-named declaration from the shared module scope for qualified lookup. A namespace import leaves the module's unqualified value visible; a selected name shadows that value too. Selecting a private member is diagnosed separately from selecting a member that does not exist, and points back to the private declaration.
Values at module level must be known at compile time
Values at module level must be known at compile time. Nothing runs before the entry point. Immutable ones can stay in flash; mutable ones cost RAM and are conspicuous. Ordinary integer arithmetic here folds in [1940]'s wider kernel range before checking the final image against its type. For example, a module value: u8 = 200 + 100 - 100 holds 200. Inside a function the same u8 addition overflows and traps before the subtraction.
table: [4]u32 = [1, 2, 4, 8] mut call_count: u32 = 0
A function value in a module struct image is likewise known: it is a static routine address, not a call or compile-time execution. Such a field may name a declared or no-capture anonymous function, and a module struct containing one needs an explicit image because no zero function address exists [0540].
A package is a named collection of modules with a version
A package is a named collection of modules with a version and an origin. Names have two levels, owner and package, and a directory under a search root is the package it names. The intended rule is one version of a package name per program: duplicated code is untenable at 32 KB, the types are nominal, and there is one conformance register. The current compiler does not read package versions or origins. It selects the first matching module directory from the ordered roots [1420], even if a later root contains another version. The future companion tool will solve versions and report a conflict as a hard error, requiring an upgrade.
The roots are the project
The roots are the project, then the user's landin home, then the system-wide one, and vendoring is just the first of those. Whether the leading segments of a path mean an owner and a package is a convention for people and for the companion tool; the compiler sees directories under roots, as [1420] says, and nothing else. So it receives an ordered list of roots and no more than that. Fetching, version solving, lock files and naming authority all live in a companion tool that ships alongside but stays separable — a compiler you can read is worth more than one that can download things. Arranging the roots so that only one version of each package name is reachable is that tool's job. Until it does so, [1470]'s one-version rule is not enforced by the compiler. core and landin are reserved, and both are used. The bare tool namespace names compiler, assembler and linker cannot be declared or bound by an import. Their built-in paths are implicitly available; an explicit import of landin/compiler, landin/assembler or landin/linker is refused by name before searching roots. core is the standard library: core/mem, core/text, core/vec. landin holds the toolchain modules of [1560] — landin/compiler, landin/assembler, landin/linker — which are available without an import, and that is why the bare names compiler, assembler and linker are taken. Naming authority remains deliberately deferred to the companion tool and ecosystem successor in ROADMAP.md. The search path is project-first, so ordinary source-module naming collisions can be overridden locally. The three compiler-owned modules retain their reserved identities.
The bootstrap request spells that narrow seam as repeated --root=DIR options followed by one entry-module directory. With no root option it retains the earlier explicit-file compatibility mode as one synthetic module. Root defaults and environment policy remain the companion tool's work. The compiler keeps that explicit contract: it neither consults a user's home nor inserts a system root, and the supplied order alone decides precedence.
COMPILE TIME
Conditional compilation
Conditional compilation selects declarations in the module, not statements or a lexical block. Every arm is parsed, but only the selected arm contributes declarations to the one whole-program module scope; an inactive arm has no name, type, template, identity or IR effect. Arms may nest and may be empty.
fixed if compiler.arch == arm64 then word_bits: u32 = 64 elsif compiler.arch == cortex_m0 then word_bits: u32 = 32 end if
The compiler exposes compiler.arch, whose compiler-owned values are x86_64, arm64, rv64, cortex_m0 and synthetic_32, compiler.word_size in bits, compiler.byte_order (little or big), and compiler.build_mode (debug or release), plus compiler.c_sysv_lp64, compiler.c_darwin_lp64 compiler.c_aapcs64_lp64 and compiler.c_riscv_lp64d, bools identifying the selected C ABI rather than inferring it from pointer width or architecture: Linux and Apple arm64 share compiler.arch == arm64 and answer differently here. compiler.os names the target's hosted system, linux, darwin, freebsd or freestanding, independently of its C calling convention. Linux and FreeBSD share a C transport on each architecture, but libc record layouts can differ, so a library binding can select those records by this fact. A build also assumes a CPU feature level of its target, selected with --level= and defaulting to the oldest processor of the architecture, and compiler.feature.NAME is a bool saying whether that level has the feature NAME, such as bmi2 on x86-64, lse on arm64, idiv on the M profile or xtheadba on RV64. RV64 levels are ISA strings, default rv64gc; optional zba and xtheadba extensions combine independently, keeping LP64D. A feature of another architecture is false, so the test needs no compiler.arch before it:
fixed if compiler.feature.idiv then divide_cycles: u32 = 12 else divide_cycles: u32 = 40 end if
A level changes the instructions a build may use and never a layout or a calling convention, so code built at two levels links together. Build mode is an explicit request value, defaulting to debug; it does not change runtime checks or optimization policy. Conditions also see the program's declared options [1530]. They use a closed no-execution fold: literals, parentheses, unary -, mathematical integer + - * / %, integer comparisons, equality, not, and and or, plus sizeof and alignof of the enabled scalar types. Measurements count target bytes; compiler.word_size == 8 * sizeof usize holds on both 32-bit and 64-bit targets. User calls, runtime names, nominal-type measurements and aggregate operations remain outside it, including in a short-circuited operand. The selected target's constructor, not its label text, supplies the architecture. public fixed if, a trailing name and fixed conditionals in blocks, structs, signatures and templates are not forms of this construct.
Compile-time assertion
Compile-time assertion. Not a keyword: it is a builtin call like the rest of [1560], because the compiler is what has to know it, and that leaves the word 'assert' to the library function at [1040] where it belongs.
compiler.assert(sizeof usize == 8)
An assertion is a module directive, also permitted in a selected fixed if arm. It uses the same closed fixed expressions as [1500], requires bool, and rejects false at the source site. Inactive assertions have no effect.
A compile-time value parameter
A compile-time value parameter.
make_buffer: (fixed capacity: u32, t: type) -> (b: [capacity]t) = ... end
Your own build switches
Your own build switches. Declared, typed, with a default, settable from the build description. A misspelt name is a compile error, never a silent false. Names are global, so each is declared exactly once.
option log_level: u32 = 0
Options inhabit a program-wide configuration namespace, visible by bare name in fixed conditions, option defaults and compiler assertions. Their supported types are bool and the enabled integer scalars, with usize and isize bounded by the selected target. An option is declared unconditionally at module level; placing it in any fixed arm is refused because switch discovery precedes arm selection. Its name cannot collide with an active module declaration or import binding. The compiler-owned configuration atoms (x86_64, arm64, cortex_m0, synthetic_32, little, big, debug and release) also keep their names.
All reached options are collected before defaults are evaluated. Defaults may refer forward to other options, whose effective overridden values are used; cycles are refused. An override still requires a valid declared default. The compiler accepts repeated --option=NAME=VALUE arguments, with true or false for bool and signed decimal integer text for integer options. Unknown or repeated names, malformed values and values outside the declared type are errors. --build-mode=debug or --build-mode=release supplies the separate built-in mode value.
There are no compile-time loops and no compile-time
There are no compile-time loops and no compile-time function calls. That line is deliberate. The builtin modules at [1560] look like an exception and are not one: their members are compiler-recognized operations, not user functions run during the build. Some inputs must be fixed, such as assertion conditions, library names and assembly text; atomic data and assembly input operands can be runtime values. Nobody can write another builtin module. Nothing of yours runs while the program is being built. What that costs is a generated table or an SoA layout, which comes from a program that writes source and is run by the build — twice so far, and a third would be worth taking seriously.
THE TOOLCHAIN, C, AND THE MACHINE
Landin has its own native backends
Landin has its own native backends. A verified, target-neutral intermediate representation takes QBE's IL as a design influence without freezing one flat or serialised stage shape before implementation evidence exists. The compiler emits deterministic assembly text and relies on the assembler and linker of the platform. Linux x86-64 comes first, native macOS arm64 second, and emulator-first Cortex-M third. The frame pointer is always set up for ordinary routines and interrupt handlers. Explicit naked routines [1570] own their frame instead. The whole program is compiled together [1480], with optional private caches that do not create a stable separate-compilation interface. Not LLVM and not C: LLVM is a dependency larger than the language and C loses the calling convention, the traps and the debug information that the design spends its precision on. The cost is owned deliberately — every target is work that nobody else does.
The cortex_m0 backend emits ARMv6-M Thumb assembly at its default armv6-m level. Selecting armv7-m or armv7e-m lets it emit instructions from that higher M-profile level, which the processor must support. All three levels share the little-endian, soft-float target contract. The firmware request emits its own reset, vector image and constrained linker script, copies initialized RAM and RAM code, and clears BSS before entering source code. Its tests execute that path separately from the older external backend harness. Freestanding library delivery remains a separate roadmap item; [1990] specifies the exact firmware boundary.
Three builtin modules are the way to the tools
Three builtin modules are the way to the tools. They are modules of the reserved landin package — landin/compiler and its siblings — and are in scope without an import, which is why their bare names are not available to anyone else.
| module | what it reaches |
|---|---|
compiler | target, word size, byte order, build mode, CPU features, and the atomic, volatile and register intrinsics |
assembler | inline assembly |
linker | libraries, sections, entry |
Their members are builtin and cannot be written by hand. The required fixed inputs depend on the operation: compiler.assert takes a fixed condition, linker.library a fixed name, and assembler.block fixed instruction text. Atomic and volatile pointer and value operands, and assembly inputs, can be runtime values; atomic ordering operands remain fixed compiler atoms. The hosted slice enables the compiler facts and assertions above and linker.library below. Scalar atomics and barriers follow D227. Cortex-M0 also enables declaration placement annotations and an explicit firmware-entry request, which hosted targets refuse. assembler.block is checked on every target [1630]. Other tool operations still receive named refusals. Where the line runs: something is builtin when the compiler has to know it. Atomics are, because opaque assembly in a hot loop wrecks the register allocation around it. Masking interrupts is not, because being opaque is exactly right there — a critical section wants nothing reordered across it. So cpu.disable_interrupts and its kin are an ordinary core module per target, written with assembler.block behind a fixed if and inlined, not a fourth builtin module whose contents would change with the target. If one of them later turns out to exist on every target and mean the same thing everywhere, the way the atomics do, it can be promoted into compiler then. The rule decides it, not a list. Vector operations are not on it: fixed arrays are the vector type and take element-wise operators [0590], so D240 withdrew the vector intrinsics this table once listed.
Calling conventions are a growing set of atoms behind one
Calling conventions are a growing set of atoms behind one spelling rather than a keyword each: extern(c) today, extern(interrupt) for handlers whose entry, exit and vector placement differ, extern(naked) for no prologue at all, and room for extern(aapcs), extern(sysv), extern(win64) later. Fortran's numeric libraries and Swift's separate error channel make direct interop candidates. Neither convention is specified or scheduled; the Language evolution register in ROADMAP.md gives each a trigger. Zig, Odin, Rust and Python all speak C, so there is nothing to gain there.
The enabled Cortex forms are distinct function types: both machine conventions require nongeneric () -> none definitions with no declared failures. Their values can be retained in data or named by a vector, but cannot be called or converted as ordinary/C routines. Interrupt handlers use an ordinary Landin frame on top of the hardware exception frame and return through EXC_RETURN. Naked bodies are one assembly block with no compiler prologue; the programmer owns stack, registers, calls and return instructions. Falling off that block traps. Neither convention implies keep.
Importing from C
Importing from C. refine reads Landin, not headers. The bootstrap supplies a separate deterministic clang-AST generator for declarations and explicit C adapters, with policy for facts a header does not say: nullability, ownership, from, retention and incoming-varargs extraction. That policy is not a handwritten replacement signature. [1975] defines the selected boundaries, Linux x86-64 SysV AMD64 LP64, Apple's arm64 variant of AAPCS64 and Linux arm64's standard AAPCS64, shared with FreeBSD, and RV64 Linux LP64D; the normative rules alone are not treated as evidence that a boundary or its code generation is complete.
A C pointer may be null and a Landin pointer may not. A foreign declaration that permits absence therefore names [0480]'s one-atom pointer union, which costs no extra bits. A small declaration illustrates the generated shape:
none_returned: atom allocation: type = none_returned | ptr mut u8 extern(c) malloc: (n: usize) -> (p: allocation) extern(c) free: (p: ptr mut u8) -> none
Match the result before using the pointer. ptr(0) is refused, including a closed folded expression or a value that becomes zero at target usize width. Dynamic construction checks the converted address and traps on zero, even in unchecked; losing origin is not permission to create null. Absent allocator backing uses a named union, not a fake ptr(1) allocation.
The ordinary core/c aliases name C's signed and unsigned integer widths, c_size, c_ptrdiff, c_float, c_double and c_bool. Its assertion of compiler.c_sysv_lp64 or compiler.c_darwin_lp64 or compiler.c_aapcs64_lp64 or compiler.c_riscv_lp64d admits the four explicitly supported hosted ABIs. Equal pointer widths alone do not admit another ABI. Plain c_char is the ABI's plain char, a numeric byte and not a Unicode scalar: i8 under SysV and Apple's ABI, u8 under standard AAPCS64 and RISC-V LP64D.
C-compatible values include integers, bool, pointers, f32/f64, C function values and recursive nonempty layout(c) structs, including nested struct, nonempty fixed-array and fixed C-callback fields. Variadic function values may be stored and called within Landin but do not fill a C callback field or signature position. Arrays themselves are not by-value C arguments or results. C enums retain their integer values; unions, bitfields, globals, TLS and nullable callbacks use explicit generated representations and adapters, not new native grammar or a pointer to a stored function value. Selected extended/x87 and 128-bit scalars, complex/vector/atomic/volatile types, old-style or non-C-convention functions, packed/overaligned/flexible/zero-size by-value records, anonymous unaliased declarations, unsupported arrays and va_list forwarding, and unsafe, stale or missing policy are refused explicitly rather than guessed. The required enum, union, bitfield, global/TLS, nullable-callback and incoming-schema categories cannot be refused wholesale as a shortcut.
An outgoing variadic signature writes , ... after at least one fixed parameter. Both direct and indirect variadic calls are positional-only, including the fixed prefix. The unnamed tail admits scalars, pointers and fixed C callbacks, not structs, arrays, slices, atoms or any. C's default promotions make f32 into f64 and bool/narrow integers into C int; untyped literals take i32 or f64. This is not an inferred error set. Native definitions receiving arbitrary varargs are refused; generated C entry adapters require an explicit extraction schema.
Linking a static library
Linking a static library, next to the declarations that need it rather than in a separate build file.
linker.library("m")
The module directive takes a fixed text literal naming an archive. Names use ASCII letters, digits, underscore, hyphen and dot, are nonempty, do not begin with a hyphen, and cannot consist only of dots. Active directives reach the platform driver after the program assembly in canonical source/declaration order, including repetitions. The Linux adapter selects archives for these libraries while leaving the hosted runtime's linkage to the driver. Darwin asks the selected driver to resolve libNAME.a and passes that existing file; a missing archive fails even when a dylib exists. With Apple's default driver, a returned bare filename must exist in the invocation directory. An inactive directive adds nothing. Moving an active directive among module declarations can therefore change archive resolution.
Exporting to C
Exporting to C. extern(c) chooses the convention; it does not mean bodyless, public or imported. A bodyless declaration imports a C routine. A Landin body defines one, and may remain private when only its callback address is needed. public separately exposes a definition. Conversely, [1610]'s standalone link(symbol: text) leaves an ordinary definition on the native Landin convention; linkage never selects C. C signatures are nongeneric, use in parameters and at most one return. No declared or inferred Landin error channel crosses the boundary; recover inside the body and return the explicit C status/value promised by the interface. Neither foreign exception unwinding nor longjmp across Landin frames is promised.
public extern(c) my_add: (a: i32, b: i32) -> (r: i32) = r = a + b end my_add
A symbol name the language's identifiers cannot spell
A linker spelling independent of the Landin name and function convention. It may stand alone before a native function name, or follow extern(c) for a C import or definition:
link(symbol: "native_identity") identity: (value: i32) -> (result: i32) = result = value end identity extern(c) link(symbol: "foreign_add") add: (a: i32, b: i32) -> (r: i32)
The standalone form still requires the ordinary = body end and keeps the native Landin convention: link never implies C, bodylessness or export. In either form it changes neither lookup nor signature nor visibility. The decoded link name has the precise safe ASCII shape [A-Za-z_.$][A-Za-z0-9_.$]*: its first byte is a letter, underscore, dot or dollar, and only a later byte may additionally be a digit. It is the logical external name before the platform's prefix, not assembler operand text. ELF keeps it unchanged; Darwin prepends one underscore, so foreign_add becomes _foreign_add and _entry becomes __entry. The same rule applies to native and C link names [1975]. Whitespace, @ suffixes and arbitrary assembler expressions are excluded. The backend quotes the identity where target assembly syntax requires it after applying that prefix. Compatible bodyless C declarations may share one spelling and one definition, but incompatible signatures and multiple definitions are refused. C imports and public C definitions default to their declared name; private C definitions without an override use collision-safe internal names. This C boundary does not enable Cortex C signatures. D229 separately enables Cortex module-data symbols, machine conventions and the placement in [1640].
Atomics are builtins
Atomics are builtins, not assembly, so the compiler knows which memory they touch and can still allocate registers around them. The ordering is a compile-time atom. A pleasant type wrapping them belongs to the Broader standard library successor roadmap rather than to core: Cortex-M0 has no read-modify-write atomics at all, so what a portable wrapper offers is a library design question, answered when a program needs one (D240).
bump: (p: ptr mut u32) -> none = _ = compiler.atomic_add(p, 1, compiler.acq_rel) end bump
D227 in spec.md defines the supported scalar operations, exact orderings, alignment traps, happens-before and race limits. Atomics synchronize coherent CPU memory; volatile accesses preserve individual accesses but do not synchronize threads. Compiler barriers invalidate memory knowledge without ordering hardware. Device ordering and completion require the target's barriers and the device's own protocol. Ordinary DMA slices stay ordinary: after certified completion, the driver establishes hardware/cache visibility and a compiler memory boundary before reading them. Interrupt masking alone does not stop DMA.
Inline assembly, for what has no builtin
assembler.block takes fixed quoted or raw text in a routine body, and after it the operands that carry values in and out. An operand names its direction, its name, its type and the register it is placed at:
disable_interrupts: () -> (previous: u32) = previous = assembler.block("mrs {mask}, primask\ncpsid i", out mask: u32 at general) end disable_interrupts restore_interrupts: (previous: u32) -> none = assembler.block("msr primask, {mask}\nisb sy", in mask: u32 at general = previous) end restore_interrupts
A register is a name the target answers for, never text. general asks the compiler to choose one, and {mask} in the text is where the chosen register goes, spelled at the width the operand's type selects. A fixed register is the architecture's own full-width name, and the text may use it directly. in carries a value in, out carries one out, and inout does both through one register. The outputs are the block's value, as a function's named returns are its: one is a scalar, several are an anonymous result you destructure.
(low, high) := assembler.block("rdtsc", out low: u32 at rax, out high: u32 at rdx)
An operand is an integer scalar that fits one register. A pointer crosses as usize(p) and comes back through ptr(u), whose zero check is then written where it happens; a flag comes back as x <> 0. Inputs are evaluated once, left to right, before the block runs.
Every block may overwrite the registers a call may, and the flags. A register a call must preserve is declared, as an operand or as a discarded output out _ at rbx, and that is what makes the routine save it. The frame, stack and link registers are never named, so a block cannot break a backtrace, and the language reserves no register of its own. Every block is opaque to the compiler: memory knowledge is invalidated and memory accesses cannot be reordered across it, which is exactly what a critical section wants. It is not itself a hardware barrier. An ordinary block is straight-line code with no labels or calls. [1990] gives each target's registers, the text rules and the programmer's obligations; D248 records the design.
The earlier Cortex-M0 form stays as a shorthand: assembler.block(text, value) passes one u32 in and out of r0, and the text names r0 itself.
core/cpu.disable_interrupts() returns the prior PRIMASK value; restore_interrupts(previous) restores it, so nested critical sections do not enable interrupts prematurely. These functions can run in thread or handler mode. They do not stop DMA or mask NMI/HardFault. wait_for_interrupt() issues DSB then WFI; waking is a reason to check the device condition again, not proof of completion. compiler_barrier, device_barrier and completion_barrier expose the existing distinct boundaries without a cache or scheduler API.
A naked body owns its control flow. For example, this selected entry runs after compiler reset has initialized data and stacks:
extern(naked) start: () -> none = assembler.block(""" 1: wfi b 1b """) end start
The request --target=cortex-m0 --firmware-entry=start --emit=exe names the source entry. A naked entry can set up PSP or call explicitly linked ordinary code, but must meet that code's eight-byte stack alignment, r9 and frame obligations. none is not a promise never to return. The compiler traps on entry return or naked fallthrough; it does not invent a hosted caller.
Kept against section garbage collection, and placed
Calling convention, symbol spelling, placement and retention are independent. The selected firmware compiler owns the fixed vector image: its first word is the initial SP, its second is compiler reset, reserved slots are zero, and source annotations supply typed handler relocations. This avoids treating stack addresses, reserved words and machine function types as interchangeable.
link(vector: 16) extern(interrupt) device_handler: () -> none = assembler.block("nop") end device_handler link(section: ".rodata.firmware_mark", align: 16, keep) firmware_mark: [4]u8 = [55, 48, 49, 0]
Slot 16 is IRQ0 in the selected profile; it is not a portable peripheral name. Alignment and vector indexes are decimal integer literals; ordinary digit separators are allowed, while nondecimal prefixes and expressions refuse. A vector annotation requires a firmware-entry request. The kept vector image references the handler, so section garbage collection retains it. An interrupt routine with no reference or keep can still disappear. keep retains its containing section; multiple objects deliberately placed in one section share that retention. Linkage alone does not retain anything.
Functions can select .text.* in flash or .ramtext.* copied to RAM at reset. Immutable data selects .rodata.*; mutable data selects .data.* or zeroed .bss.*. Placement alignment is a power of two through 256 and does not change type layout. The 32 KiB flash/16 KiB RAM image reserves the top 4 KiB of RAM for stacks. The linker checks bounds and overlap; the compiler rejects reserved vectors, mismatched sections, duplicate attributes and symbol collisions. There is no user-code module initialization [1460].
Entry point
Entry point. Hosted, main follows the system C ABI at the machine boundary. The no-argument Landin form is the ordinary one, because argc and argv in the C shape cannot be indexed without slice_from, which is not enabled — so the hosted world retains the argument table and offers bounded indexed pointer-and-length views instead. The selected Linux backend calls its emitted hidden C entry void _landin_host_initialize_arguments(int argc, char **argv); with the real incoming carriers before that no-argument source body begins.
A C-owned main that drives exported Landin routines calls the same entry explicitly before hosted.host() acquires an argument capability and before starting a thread that may acquire one. It need not do so for startup-independent bridge work merely because core/io is linked, and no ordinary export or callback initializes or resets the root. Those views derive from the resulting world; a caller that must mutate the same backing-aware provider while retaining one first copies what it needs into its own storage. Freestanding there is no main; the explicit compiler request names an infallible ordinary () -> none or () -> noreturn definition, or a naked () -> none definition, in the entry module. Compiler reset initializes RAM before calling it. Both normal return and failed checks trap without a hosted exit service. [1990] defines this constrained firmware path; general build/package orchestration is outside it.
And this is where capabilities come from
The entry point is a usual place to mint roots and pass them down — an allocator, an Io, a diagnostics log. The language does not reserve host constructors for the entry point: another routine can call them too [1680]. The hosted root imports core/io/hosted; a caller-backed root needs only core/io. The derived hosted example's main delegates to app.entry, whose essential flow is:
public entry: () -> (code: i32) = mut host := hosted.host() world: any io.world = any(addr host) mut heap := heap.host() mut program := region.new_region(addr heap) defer region.release_region(program) stream: io.file = world.err() mut logger := diag.to(addr world, addr stream) log: any diag.log = any(addr logger) kept: usize = run_logged(program, world, usize(4096), log) else (problem) _ = problem code = 1 return end _ = kept code = if log.failed() then 1 else 0 end if end entry
The actual entry also reports kept and write failures before choosing its status. build_logged, called by run_logged, copies the world's arguments into the supplied program allocator before retaining configuration.
hosted.host() acquires the real argument-table capability; it does not create or copy that storage. The first nonnegative-count, non-null-table startup call establishes one exact (argc, argv) root. Repeating that exact pair is harmless; using the argument services before initialization or trying to replace either carrier traps. The C owner therefore keeps the table and the strings it names readable for as long as a derived world, view or callback can use them. The published sequence starts at argv[1]; an ordinary C invocation with only its program name consequently gives Landin an empty user-argument sequence.
Which is what makes the same run testable and portable without it knowing: hand it a different world and it does not learn the difference. The memory provider takes caller-owned file, argument, output and error storage; the test must supply an input path in its argument table for the application's configuration.
test_drops_debug: () -> (ok: bool) = ok = false source: []u8 = "DEBUG a\nERROR b\n" mut data: [16]u8 = zeroed for at in usize(0)..<lenof source do data[at] = source[at] end for file: io.memory_file = (name: "in.log", contents: data[0..<16], length: lenof source, cursor: 0, readable: true, writable: false, opened: false, writing: false) mut files: [1]io.memory_file files[0] = file level: []u8 = "--level" threshold: []u8 = "ERROR" path: []u8 = "in.log" arguments: [3]io.argument = [ (data: addr level[0], length: lenof level), (data: addr threshold[0], length: lenof threshold), (data: addr path[0], length: lenof path)] mut output: [128]u8 = zeroed mut errors: [128]u8 = zeroed mut memory := io.memory_world(files[0..<1], arguments[0..<3], output[0..<128], errors[0..<128]) world: any io.world = any(addr memory) mut backing := heap.host() mut program := region.new_region(addr backing) defer region.release_region(program) mut logger := diag.new_log(capacity: 4) log: any diag.log = any(addr logger) kept: usize = app.run_logged(program, world, usize(4096), log) else (problem) _ = problem return end ok = kept == 1 and not files[0].opened end test_drops_debug
For that to work at all, Io has to be a concept and not a type, so something else can satisfy it. It travels as 'any io' rather than as a type parameter: an indirect call in front of a system call costs nothing, where an allocator is threaded generically because it sits in hot loops. Same machinery, [1690], chosen per case.
The system provider captures errno immediately after a libc failure and keeps the exact terminal value in explicit state, available through hosted.last_errno. Success clears it; a local refusal fabricates no host errno. Open/read/write retry EINTR only when the selected host guarantees that attempt made no progress. world.write completes the whole slice, continuing from each positive partial transfer; if a later attempt fails, the completed prefix remains. world.write_some reports a positive accepted count for a nonempty attempt, including a short success, and leaves suffix delivery to its caller. A failed write_some attempt transfers no bytes. An empty slice succeeds without a host call, and write_some returns zero. Close consumes the handle once even on failure, and never blindly retries EINTR. The public failure atoms remain payload-free; detailed diagnostics use ordinary state rather than a new exception mechanism.
world.same_file(left, right) checks whether two paths identify the same file, following symbolic links and recognizing hard links. The left path must exist; failure to inspect it is an error. An absent right path returns false, but other lookup failures are errors. The log filter checks this before truncating an output. Names must remain stable during the check and open: this is not an atomic operation. Custom world providers must implement both same_file and write_some when migrating to this interface.
The bounded library provider is core/io.memory, constructed with memory_world(files, arguments, output, errors). Its caller supplies every file name, content buffer, descriptor and output extent. Files must already exist in the table; opening for writing truncates after checks. Reads stop at the initialized length. Memory write completes the slice or reports failure while preserving a completed prefix; memory write_some returns a positive count on a nonempty success, possibly short, and transfers nothing on failure. An empty write_some returns zero without a write attempt or counter increment. Configurable chunk limits and one-based failure counts make short reads, partial writes and cleanup reproducible. A zero read limit with unread data reports an error; it does not signal EOF. Empty transfers are no-ops. A close error still leaves a valid handle closed.
The argument table omits the executable name. written and written_errors return source-derived views of the two output prefixes. The provider retains its supplied slices and performs no allocation or host call. Its public representation and copied handles remain subject to manual invariants: backing must remain valid, transfer ranges must not overlap, counters need headroom, and stale handles can address a reopened slot. D153 records these bounds. The complete derived hosted application, examples/derived_hosted, uses this same world interface, copies retained arguments, assembles complete kept lines and selects heterogeneous filters and destinations at runtime. A leading level filter discards rejected lines from a bounded prefix. Its derivation manifest records exact argument, buffering, retry and cleanup policies.
Both providers expose each user argument as a pointer and byte length, never as a guessed C string or forged slice. copy_argument(argument, scratch) checks the exact capacity before writing, copies into caller-owned initialized storage, and returns that genuine scratch prefix. Source backing must stay readable with representable addresses and must not overlap the destination. Passing the result to text.from_bytes is the checked route from world arguments to UTF-8. The system and memory sequences both begin at the first user argument; neither contains argv[0].
A failed check calls a fixed, never-returning symbol
A failed check calls a fixed, never-returning symbol. Two scalars, no strings: 'site' is a number the compiler assigns to the source operation and check family. The file and line live in an optional off-target table; constrained builds need no filenames or reporting storage. D232 fixes selection and numbering for every implemented target.
import core/panic public panic_handler: (kind: panic.panic_kind, site: u32) -> noreturn = loop do end loop end panic_handler
The entry module's public ordinary panic_handler replaces the compiler's terminal default. It must have this exact infallible return form and canonical atom domain; a same-spelled linker symbol is not replacement. Checks do not unwind the failed computation's cleanup. A second handler entry traps, including a check inside the handler. --panic-map emits a build-bound source map; source-location.py --panic-site requires matching artifact identity. Caller coordinates remain a separate value. Hardware faults and naked assembly retain their machine obligations.
THE PRINCIPLES BEHIND THE DECISIONS
Require a capability, do not track an effect
Require a capability, do not track an effect. Where another language would record in a type that a function performs input and output, allocates, or reads the clock, Landin interfaces normally pass the thing: an allocator, an Io, a peripheral handle, a random source. There is no effect system. The argument list identifies the capabilities a caller supplies. It does not bound the effects of the call tree: the public, zero-argument host I/O and heap constructors can be called by any ordinary hosted routine, including one given a different world or allocator. A driver can also form a peripheral pointer from an address literal [0460]. Passing roots from the entry point [1660] is a convention, not a restriction checked by the language. Substituting an in-memory world or bounded allocator therefore does not by itself exclude host I/O or heap allocation below the call. Restricting host minting to the entry module remains a possible later tightening; it would not close the address literal route needed by drivers. The chosen boundary is deliberate: provider parameters let callers substitute an in-memory world or bounded allocator for code that uses those parameters. They do not certify a whole call tree. Establishing that stronger claim requires inspection of its calls and imports. A checked exclusion rule would add privileged-module or call-graph machinery, while an entry-module rule alone would leave foreign calls and address literals. When trusted code needs a static isolation guarantee, or when untrusted code must run, the roadmap reconsiders that cost and the full set of authority paths. D258 records this choice.
One mechanism, two readings, is better than two mechanisms
One mechanism, two readings, is better than two mechanisms. Concept-constrained generic code is checked against evidence that its type satisfies a concept. A concrete instance carries hidden table arguments only for direct constrained member selections in its checked body; an erased-only any construction uses a static table and can need none. When the compiler proves the concrete incoming evidence it can specialize dispatch, retaining those selected hidden ABI positions. Otherwise dispatch remains indirect; an any C value carries evidence with its erased type. Static generics and runtime dispatch share a foundation, not two unrelated features.
Atoms are the same idea wherever they appear
Atoms are the same idea wherever they appear: identity without payload. Enumerations, error sets, variant tags, register encodings and panic kinds are uses of atoms, not separate categories. There is no 'enum' in the language because there is nothing left for it to be.
How a new feature earns its place
How a new feature earns its place. When a program cannot be written cleanly, in order:
| ask, in order | then |
|---|---|
| can an existing mechanism express it? | a library |
| can the compiler work it out itself? | no syntax |
| must the programmer say it, and does saying it generalise or remove another mechanism? | a candidate |
| does it only solve this one case? | not yet |
A new mechanism should let two old ones leave the building.
What the language claims, at this version, plainly
What the language claims, at this version, plainly. It performs local checks: origins and escape, consumption [0910], reference permission [0430], bounds, conversions and arithmetic. It is not a memory-safe language and not a resource-safe one. Pointer-to-integer conversion and the C boundary leave the checked model altogether [0470], a copy taken before a sink is refused nothing [0910], distinct pointer or computed-index paths may alias across inout [0900], and two arenas are indistinguishable [0860]. That is a smaller claim than 'safe' and a larger one than C's. D148's guarantee register now puts every implemented observable failure boundary in one of four columns and links it to executable evidence. New operations have to enter that register as they are implemented, and the registers were closed over the final feature-complete matrix. Read the claim as: deliberately unsafe, with static help that is worth having. Checks stay on by default. unchecked [1120] removes the edges D187 names, in the region where it is written, and grants an optimiser nothing at all: it emits fewer checks and makes no fact available to a later pass. D211 retains that rule under optimization: no new undefined behavior, no-alias, overflow or floating-point assumption follows from unchecked. Existing scalar outcomes and observable side-effect and trap order remain the contract.
Check once, then carry the proof
Check once, then carry the proof. A successful test yields a value that stands for what was checked — a buffer that passed the alignment test becomes a dma_buffer, and the DMA interface asks for nothing else. distinct types, range subtypes and error sets are enough to do this; it needs no feature of its own, only the habit. For a range subtype, the proof starts with a checked subtype value, not with a guard on an ordinary integer (D188).
WHAT LANDIN DELIBERATELY DOES NOT HAVE
no classes, inheritance, methods or runtime type information no exceptions, no stack unwinding, no catchable panics no garbage collection and no reference counting no destructors no closures that capture no implicit conversions no null no positional tuples: anonymous records with named fields exist no function name overloading and no multiple dispatch no user-defined operators and no macros no compile-time execution no separate interface files no header parsing inside refine (the separate binding generator reads headers)
WHAT IS STILL OPEN
ROADMAP.md, and not a second list here. spec.md is the normative authority for language semantics and this file explains them; ROADMAP.md is the sole durable authority for open work, implementation dependencies, phase gates and the register of work that waits for a trigger. Every item the bootstrap inherited was traced to the construct, prototype finding or archived review section it came from, and each was given a terminal disposition: implemented, rejected with evidence, or transferred to one named successor family with what would bring it back. What this file keeps parked or deferred, structure-of-arrays [0620], affine values [0910], restricting where roots are minted [1680] and a third generated-source case [1540], is held by the Language evolution successor.
The bootstrap compiler now exists: refine checks and lowers a program, emits Linux x86-64, Darwin arm64 and Cortex-M0 assembly, links hosted executables and compiler-owned firmware, and runs all four complete derived programs, the parser, containers and hosted application on the hosts and the driver on Cortex-M. Every construct was audited and every inherited item dispositioned before the first roadmap closed.
That compiler is feature-complete pre-v1. Production status, release versioning, package acquisition, competitive optimization and self-hosting are outside the current roadmap. A future roadmap may replace tested Ada stages incrementally; no self-hosting work or serialized cross-language stage protocol is scheduled now.
WHAT WAS TRIED AND DROPPED
The revision log this replaces recorded every change across a long sequence of versions. Most of it was arrival. What is worth carrying is the other kind of entry: the thing that was designed, sometimes built, and then taken out again — because a reader who does not know that will propose it back, and because a design is partly defined by what it refused.
- an ambient environment, carried in a register, holding the allocator and the diagnostics and the Io — Odin's context. Designed in full, then removed. Once the argument was that every property it needed made it visible anyway, the only thing left was its implicit flow, and that was the thing worth losing. It became an ordinary parameter and the ABI lost a reserved register [1680].
- compile-time execution, refused twice. It would have given generics, macros and configuration from one mechanism, and it costs an interpreter inside the compiler. The accepted price is that generated tables and vendor bindings come from generator programs [1540].
- witness tables as the only story, then monomorphisation as the only story: both refused. The table is the foundation and specialising is an optimisation weighed per instantiation [1310].
- cost annotations on a conformance, and the concepts that went with them. Removed entirely: how finely a concept distinguishes what an operation costs is a library's business, and no language guarantee should rest on how clever an optimiser happens to be [1270].
- a concept for ranges. Integer range traversal now uses bounds in a
forheader, and left-exclusive forms went with the concept [0360]. - a second type with the shape of a fixed array and different operator rules. Fixed arrays are the vector type [0590].
- transposed collections, designed and deferred: they touch aliasing, generics, slicing, addr, layout, debug information and the optimiser at once, and no program has yet needed one [0620].
- weak conformances, built and removed. Libraries would declare weakly and applications override strongly, which worked — and meant an application could quietly change the behaviour of generic code inside a library. A collision is simply an error [1280].
- labels on if and match, removed; they earn their place on loops and bare blocks only [1180].
- errdefer, refused, and then arrived at from the other side. A cancellable defer was tried first and dropped, because the cancel always sits where the block succeeds and is hand-made bookkeeping for a question the exit already answers. defer with an argument was dropped because it reads as a call. What went in is undo, its own word, because to defer is to do it later and this may never be done at all [1110].
- read-only reference types spelled as a second form of every reference — considered twice and refused both times, then arrived at anyway from the other end. What was refused was deep const with an implicit widening; what went in is permission in the type with one stated relaxation, and it removed two mechanisms rather than adding one [0430].
- permission derived from where a reference came from, which lasted one version. It rescued the register case and could not express the commonest signature in a driver, so it was replaced by permission in the type [0430].
- inferred derivation for the from clause, refused with a counterexample rather than an argument: mechanically an allocator's result does come out of the allocator, so inference forbids a second live allocation [0790].
- braces, which were never a decision. They crept into two register examples and a set literal and were taken out again; the language has none [0730].
- a keyword for the compile-time assertion. The collision with the ordinary assertion was resolved by subtraction: it is compiler.assert now, a builtin call like the rest, and the language has one keyword fewer [1510].
- sets as a kind of their own, with a literal, three operators and a membership operator, all of which were designed and then not needed: set(X) generates a packed struct of bool, so membership is a field read [0730].
- affine values, which cannot be copied. Not refused — parked, with the condition that would bring them in, and with sink honestly described in the meantime as a use-after-consume check on one place rather than as ownership [0910].
- async and await, and the stackless coroutines under them, refused before either was built. Concurrency is not a property of a function here; it is a capability the caller hands down, so the same code blocks or does not depending on the Io it was given [1660]. One implementation of that Io ships first and it blocks, which is a scheduler not yet written rather than a limit in the language. Stackless is the refusal proper: cutting a function into a state machine is a compiler project of its own, and it colours every function type that reaches one — the same objection that removed the ambient environment above [1680]. Stackful fibres are the route to take instead, and the design already pays for them: the frame pointer is always present, the callee-saved discipline is explicit, and nothing rides in a reserved register.
- a volatile pointer type, a generated register wrapper carrying read, write and reset modes, and a
set(X)type former, all sketched before any driver ran. The complete driver needed none of them: an operation says a device access happens, the modes are its arguments, and the generator writes the per-register functions and the named bool fields. A qualifier would have been a second permission on every reference [0740] [0850]. - per-field byte order and the weak, inline and noinline attributes, listed and never used. A big-endian field makes every access a conversion and its address unreadable through an ordinary pointer; inlining is the optimizer's business; one definition per name leaves weak nothing to do [0750] [0760].
- vector intrinsics beside element-wise array operators. The table listed both and only one was ever needed: fixed arrays are the vector type [0590] [1560].