Types and values
Partly written. §1 to §10 were written with milestones M3 to M8a of the Lyric 5 plan and the catch-up block N (2026-10-07); what later milestones add lands here spec-first: each rule with its conformance case before or with its implementation. Source of the decisions: 02 Value model, 03 Type system.
Scope
Scalars and their aliases, widening and coercions, overflow, as, structs and classes, mutability, optionals, arrays, Slice<T>, StringView, ranges, tuples, enums, generics by monomorphization.
1. Scalars
The number tower, bool and char
(03 T1, T2).
Conformance. conformance/cases/03-types/.
1.1 The scalar types
- The integer types are
int8,int16,int32,int(signed, two's complement) anduint8,uint16,uint32,uint(unsigned), of the width their name says;intanduintare 64 bits wide. The float types arefloat32andfloat, IEEE 754 binary32 and binary64.boolholdstrueandfalse.charholds one Unicode scalar value. int64,uint64andfloat64are aliases ofint,uintandfloat: the same type under a second name, not a distinct type. A diagnostic names the type asint,uint,float.- There is no
usize: at the C boundarysize_tisuint(13).
1.2 Literals
- An unsuffixed integer literal takes the integer type its position expects — an
initializer with a written type, an argument, a return, an operand beside a typed operand;
without one it is
int. An unsuffixed float literal likewise takes the float type its position expects,floatotherwise. - A literal that does not fit its type is an error, not a wrapped value:
let x: uint8 = 300is refused, and so is-129as anint8. - An integer literal takes a float type only when the float holds its value exactly:
let f: float = 9007199254740993(2⁵³ + 1) is an error, never a silent rounding. - An integer literal never takes
char:let c: char = 65is an error. Acharis written as a character literal, or reached throughuint32(§1.4).
1.3 Widening
- At a coercion site — an initializer with a written type, an assignment, an argument, a
return, an arm under a written context type — an integer value stands where a wider integer
type is expected when the wider type's range contains the narrower one's:
int8 → int16 → int32 → int,uint8 → uint16 → uint32 → uint, and an unsigned type into every signed type of greater width (uint8 → int16,uint16 → int32,uint32 → int).float32stands wherefloatis expected. The value is converted; nothing else changes. - Nothing else is implicit: no narrowing, no signed into unsigned, no
uintintoint, no integer into a float (let f: float = nwithn: intis an error: an integer stays an integer), no float into an integer, nofloatintofloat32, nothing into or out ofcharorbool. - Widening happens at coercion sites only, never inside an expression: the operands of an
operator have one type, and
a + bwitha: int8andb: intis an error. Arms of aniformatchwithout a context type unify exactly. Coercion follows the inference, it does not take part in it (03 T8).
1.4 as
x as Tconverts between any two number types and always succeeds:- integer to integer: the value modulo 2ᵂ of the target width, reinterpreted in the
target's signedness (
300 as uint8is44,-1 as uint8is255,128 as int8is-128); a widening is exact; - integer to float: the nearest representable value, ties to even;
- float to integer: toward zero; a value outside the target's range gives the nearest
bound; NaN gives
0; - float to float: IEEE rounding to nearest, ties to even.
- integer to integer: the value modulo 2ᵂ of the target width, reinterpreted in the
target's signedness (
charconverts touint32and fromuint32and to no other type.c as uint32is the scalar value.n as charyields the character with that scalar value and panics withLYR-RT0009whennis a surrogate (0xD800..0xDFFF) or above0x10FFFF(13 §1.4). Acharmeetsintthroughuint32:c as uint32 as int.boolconverts to nothing and from nothing byas.asis the one conversion operator. It never fails on numbers, so it never answers an optional; a checked narrowing is a method (03 T1d, form with the standard library).- Between any other two types
asconverts nothing (LYR-SEM0006; 04 D6): a conversion is a method,T.from(v)orv.into()through aFrom(12), and noFrommakes a cast of it.
1.5 char and bool are not numbers
charhas no arithmetic and no bit operators, and does not compare with a number:c + 1,-c,c << 1,c++,c < 98,c == 97are errors. Twocharvalues compare with==,!=and the four orderings, by scalar value.boolhas the logical operators and equality, and nothing of arithmetic.
1.6 Arithmetic and overflow
- On integers,
+ - * / %and unary-panic on overflow withLYR-RT0002, in every build profile that keeps its overflow checks — every one does unless it says otherwise (rule 6): a result outside the type's range, an unsigned result below zero,MIN / -1,MIN % -1,-MIN. Division and remainder by zero panic withLYR-RT0001./rounds toward zero and%takes the sign of the dividend, as C does. <<and>>panic withLYR-RT0002when the count is negative or not below the width of the left operand's type. Otherwise bits shifted out are discarded,>>on a signed type is arithmetic (the sign is kept) and on an unsigned type logical. The count is an integer of any type, whatever the left operand's (the review's A9f):a << nwith anint8and anintis anint8, and the count is checked as it is, in its own type — 259 is not below eight. The result has the left operand's type, and the left operand is what it would be without the shift: a literal there takes the type its position wants, as a literal alone does — a typed binding, a parameter, the other operand of an operator:flags & (1 << bit)is the flags' type —, or is anint; it does not take the count's. The other binary operators keep one type on both sides.- The wrap operators
+%,-%,*%(and the compounds+%=,-%=,*%=) compute the result modulo 2ᵂ of the type's width and never panic. They bind like+,-and*. They are defined on integer types only: a float or acharoperand is an error (08 Y4). & | ^ ~ << >>and the wrap operators take integers; the arithmetic operators take integers and floats.- On floats,
+ - * / %and unary-are the IEEE 754 operations in round-to-nearest-even: there is no overflow and no division fault —1.0 / 0.0isinf,0.0 / 0.0isNaN— and%is the remainder of the truncated division (fmod),3.0 % 2.5is0.5. An implementation computes each operation as written: no contraction ofa * b + cinto a fused multiply-add, no reassociation, no fast-math, unless a build asks for it explicitly — a profile withfastMath(15 §4; 01 L10). The same program computes the same bits on every Tier 1 target. - A profile with
overflowChecks = false(15 §4) takes the overflow checks out:+,-,*and unary-compute as the wrap operators do. A division, a remainder and a shift keep their checks — a zero divisor or a shift past the width is no overflow to wrap. No profile is without the checks unless it says so.
1.7 The text of a number
What an f-string hole and the converters of std.string write (12 §1):
- An integer is its decimal digits, with a leading
-when negative. - A float is the shortest decimal text that reads back as the same value (design 10 B5
Z5):
0.1,0.30000000000000004,1.5. It is written plain for a decimal exponent from -4 up to below 16, an integral value with.0—1.0,-0.0,100.0,1000000000000000.0—, and with an exponent beyond, its sign always and two digits at least:1e+16,1e+21,1.5e-07,5e-324.inf,-infandnanstand for the values that are no number. Afloat32is written as its own shortest text — the one that reads back as the samefloat32(the review's M8a-9):0.1as afloat32prints0.1; widened to afloatit prints0.10000000149011612, the value it holds. boolistrueorfalse;charis the character itself, UTF-8 encoded.
2. Structs and classes
Two forms of a composite type, chosen once at the type (02 M1–M5, M9, M10, M14).
Conformance. conformance/cases/03-types/.
2.1 Values and references
- A
structis a value: it is stored where it is declared — in a local, inline in the object or the struct that holds it — and a binding, an assignment, an argument and a return copy it. A copy is shallow: a reference held in a field is copied as a reference. - A
classis an object reached through a reference: a binding, an assignment, an argument and a return pass the reference, and every holder sees the one object. - A struct has no identity. A class has: two references are the same object or they are not (§2.5).
2.2 Fields and places
- A field is fixed once the value holding it is built, unless it is declared
var:var balance: int. The rule is one for structs and classes. - An assignment, a compound assignment,
++and--write a place. A fielda.fis a place that may be written whenfis declaredvarandamay be changed in place:- a local is a place when it is bound with
var; aletfreezes a struct to the bottom — no field of it, and no field of a struct it holds, is written through it; - a parameter is a
let— a place parameter is the caller's place (§2.3a); - a reference starts the chain anew: when
ais of a class type, the object is the place, whatever the binding or the field that holds the reference says —let c: Cdoes not keepc.count = 1from writing the object, and neither does a fixed field of class type on the way; - an element of an array is a place in the array, whatever holds the array;
thisis a place inside amut fnand nowhere else (§2.3).
- a local is a place when it is bound with
- A temporary is not a place: the result of a call that answers a struct cannot be
written into, and no
mut fncan be called on it. The program is refused; the value is never copied in silence so that the write has somewhere to go. A call that answers a class answers a reference, and that is a place. - Writing what is not a place is
LYR-SEM0019; the diagnostic says which part of the chain refuses: the field withoutvar, theletor the parameter at the root, the method withoutmut, the temporary.
2.3 Methods and mut fn
- In a method of a struct,
thisis the caller's value, not a copy of it: what a method writes throughthis, the caller sees, and a write that happened before a panic stays. - A method that writes
this— a field of it,thisas a whole, or by calling amut fnon it — is declaredmut fn, on a class as on a struct. A method without the word that does so is refused (LYR-SEM0019). - A
mut fnis called on a place (§2.2): on a struct bound withlet, on a parameter of a struct type — a place parameter aside (§2.3a) — and on a temporary the call is refused. On a class it is called through any reference. this = valuein amut fnof a struct replaces the caller's value as a whole. Thethisof a class method is the reference the caller holds and is never replaced.- A
mut fnof a struct has something to write: the struct has avarfield, or the method replacesthis, or it calls amut fnonthis, or an interface of the struct declares the methodmut. Otherwise the declaration is refused (LYR-SEM0023).
2.3a Place parameters
(03 T12)
- A parameter written
&x: Ttakes a place of the caller, not a value: the function reads and writes the caller's variable, field or element itself, and what it wrote stays written however the call ends — a return, an error, a panic. Inside the functionxis a place of typeT, written like avar. - The argument is marked as the parameter is —
swap(&a, &b)— and is a place the caller may write (§2.2): avarlocal, a module-levelvar, avarfield under a root that may be changed, an element of an array, a view or an inline array, a place parameter of the caller's own,thisinside amut fn. Alet, a value parameter, a fixed field, an element of a container and a temporary are refused (LYR-SEM0156); so are a place parameter's argument without the mark and a value parameter's with it (LYR-SEM0157). The place's type is the parameter's exactly: nothing widens and nothing is wrapped into an optional (LYR-SEM0001). - A place parameter has no default and is no variadic parameter: either would hand it a value
the call makes (
LYR-SEM0156). - The place of an optional,
&x: ?T, lets the function writenullinto it; the place of a class value lets it bind the caller's variable to another object. - A function type writes the mark at the parameter:
fn(&int, string) -> voidis a type of its own, notfn(int, string) -> void. An implementation of an interface member takes a place where the member does (LYR-SEM0042). 5a. A lambda takes a place as a function does, the mark before the parameter's name:(&n) => …,(&n: int) => …, and in a trailing block's parameter list{ &n => … }(§8.2); its type isfn(&T) -> R. The mark is the lambda's to write, as the argument's is the call's: where the position hands a place over, the parameter is marked, and where it hands a value, it is not (LYR-SEM0157) — so the implicitittakes no place, and a block that is handed one names its parameter. The mark stands before a name, not before a pattern (LYR-PAR0058), and not in the bare formx => …. - A place is the call's: no lambda captures a place parameter — a function's or a lambda's
own — and a coroutine, whose body runs after the call has returned, takes none: neither a
coroutine function nor a generator lambda (
LYR-SEM0158). - Two places of one call may be the same place (
swap(&a, &a)); what the function then reads after a write is not specified.
2.4 Construction
- The initializer
T { field = value, … }builds a value of a struct or a class. Every field without a default is given; one diagnostic names all that are missing (LYR-SEM0106). The written values are evaluated in the order they are written, then the defaults of the omitted fields in the order of their declaration. A default does not seethis. It is checked with its type's parameters in scope —items: List<T> = List<T>.new()— and evaluated for the instance that is built. - A field of an optional type
?Thas the defaultnullwithout saying so. - There are no constructors. A type name in call position,
T(args), is the callT.new(args)of the type'sstatic fn new— an ordinary static function, which may validate, answer any type, and hand out an object that exists already. A type that declares nostatic fn newis not callable (LYR-SEM0013). - A name alone is its field (02 I3; N2c):
Point { x, y }isPoint { x = x, y = y }, the value the read of the binding of that name, and the two forms mix,Point { x, y = 0 }. A struct variant's initializer takes it too,Shape.Rect { w, h }and.Rect { w, h }.
2.5 with
p with { x = 3, y = 4 }is a copy of the structpwith the named fields replaced, ofp's type;pis unchanged. It is the way to change a fixed field: novaris needed, since nothing is written in place (02 M6).- The values are evaluated left to right and see the old
p:p with { x = p.y, y = p.x }swaps. A field named twice (LYR-SEM0070) and a field the type does not have (LYR-SEM0015) are refused. - A path reaches into a struct the value holds by value:
line with { to.x = 9 }isline with { to = line.to with { x = 9 } }. Every segment but the last names a struct (LYR-SEM0116). withstands on a struct only: on a class — an object is changed in place, or cloned (05) — and on a tuple, which has no field names, it is refused (LYR-SEM0116).withis a postfix:p with { x = 1 }.xreads the copy'sx, andq + p with { … }isq + (p with { … }). As a statement it has no effect and is refused like any other value (LYR-SEM0022).
2.6 Identity and equality
same(a, b)answers whether two references are one object. Both arguments are references — class values, arrays, coroutines — of one type. On a value it is an error (LYR-SEM0003), notfalse: a struct has no identity to ask about.==and!=mean value equality and nothing else (05): a class without it has no==, and never falls back to identity.
3. Optionals
A value or nothing (03 T4, 01 V5).
Conformance. conformance/cases/03-types/.
3.1 The type
?Tholds a value ofTor nothing, writtennull.Tis any type, an optional included:??Tis a type, and its three states are distinct — nothing, a?Tthat isnull, a?Tthat holds a value. Levels do not collapse: a generic?TatT = ?intis??int.- At a coercion site (§1.3) a value stands where an optional of its type is
expected, and is wrapped once for every level the expected type has more: an
intwhere??intis expected is a present?intthat holds the value; a?intthere is a present??intwhatever it holds.nullis the absence at the outermost level of the type expected. - A coercion reaches through an optional: at a coercion site a
?Tstands where a?Uis expected when aTstands where aUis —?Circlewhere?Shapeis,?int8where?intis (03 T3). Anullstaysnull; a value is coerced as a value of its own and wrapped again. Wrapping (rule 2) goes first: a?intwhere??intis expected is a present??int, whatever it holds. - There is no implicit way down: a
?Tdoes not stand where aTis expected, nor a??Twhere a?Tis. The ways down are a test (§3.2),??and!(§3.3). - (Informative.) An optional of a class value or of a string occupies the one word of the reference, null when there is no value. Any other optional holds its value and a flag.
3.2 Tests and narrowing
x == nullandx != nullask whether the outermost level ofxholds a value. They are asked of an optional only: on a type that is nevernullthe test is refused (LYR-SEM0059).- A bare type parameter is opaque: on an expression of type
Tthere is no== null, no??, no??=, no!and nonullpattern, whatever an instantiation bindsTto. The body of a generic is checked at its declaration; it asks these of a?T. - Where a test has proven a name present, the name has the type one level down: inside
the
thenofif (x != null), in the body ofwhile (x != null), on the right ofx != null &&, afterif (x == null) { return …; }and the other forms that leave. A second test narrows again: a??intproven present is a?int, and proven present once more anint. A negated test proves what the test refutes and refutes what it proves:!(x == null)narrows asx != nulldoes,if (!(x != null)) { return …; }leavesxpresent behind it, and a negation around&&or||follows from their own rules — where!(a != null && b != null)is false, both are present. - Narrowing belongs to the binding. An assignment to a
varends it: from there on the name has its declared type again. A module-levelvarand a place parameter (§2.3a) are not narrowed at all: a call — and for a place, a write through another name — may change them while the test holds.if (let v = x)(rule 5) reads one once. if (let v = e)andwhile (let v = e)bindvto the value of the optionalewhen there is one, one level down, and take the branch only then.- A narrowed name is the value where it lies, not a copy of it: through a narrowed
varof type?S, withSa struct, avarfield of the struct is written in place (§2.2). - Equality.
a == bon two?Tis true when both hold nothing, or both hold a value and the values are equal throughT's own equality (05 §12 rule 5 — the scalars natively);!=is its negation. ATon one side coerces up to?T(§3.1):x == 5withx: ?intasks whetherxholds a 5. ATwithoutEquatablegives its optional no equality (LYR-SEM0059). There is no ordering on an optional:<and its siblings are refused (LYR-SEM0003), andnullorders with nothing.
3.3 ??, ! and ?.
a ?? bis the value ofawhen it holds one, one level down, andbotherwise.bis evaluated only whenaholds nothing. The operator is right-associative. On a left side that is nevernullit is refused (LYR-SEM0005).a ??= bassignsbtoawhenaholds nothing, and evaluatesbonly then.x!is the value ofx, one level down, and panics withLYR-RT0004whenxholds nothing (13 §1.4). On a type that is nevernullit is refused (LYR-SEM0005).a?.mreads the membermof the value ofawhen there is one, and isnullotherwise. Chains flatten: the result is an optional of the member's type, and when the member's type is itself an optional, it is that type —a?.b?.ccan be absent in one way only and is a?C, not a??C.a?.f(args)calls the method under the same rule and evaluates the arguments only whenaholds a value.- Nothing is assigned through
?.:a?.f = vis refused (LYR-SEM0019). Narrow first.
3.4 Equality and hashing
- An optional is
EquatableandHashablewhere its value's type is (N2a):nullequals onlynull, and a?intis a set's element. It isDebugwhere its value's type is (M8c S16): its debug text isnullor the value's own, with no wrapper —{d:?}, andassertEq('7'.toDigit(10), 7)names both sides. It is noDisplay: a hole asks for a value, and anullis tested first (LYR-SEM0006).
4. Enums
One of several variants, each with a payload or none (01 V5, V6, 02 M13, 03 T9).
Conformance. conformance/cases/03-types/.
4.1 Variants and payloads
- An
enumdeclares its variants, each of which is a unit variant (Red), a tuple variant with positional payload (Num(int),Tagged(string, Point)) or a struct variant with named fields (Rect { w: int, h: int }). One enum may mix the three forms. After the variants, separated by;, an enum declares methods,mut fnincluded, as a struct does (§2.3), and constants,static let, which are the enum's under its name as a struct's are (04 §1 rule 2):Level.fallback. The;is what ends the variants — a member where the next variant's name is expected is refused (LYR-PAR0057). - A value of the enum is exactly one variant with that variant's payload. A unit variant is
written by its name,
Signal.Red; a tuple variant is called,Shape.Num(3); a struct variant is built with the initializer,Shape.Rect { w = 1, h = 2 }, under the rules of §2.4: a payload's field may write a default, checked against its type with the enum's parameters in scope, and an initializer that leaves the field out takes it (the sweep after M10, X2). A variant is not a type:Numnames no type, and a payload is read by a pattern (09) and in no other way. - An enum is a value (§2.1): stored inline, copied by a
binding, an assignment, an argument and a return, without identity.
thisin a method is the caller's value; amut fnreplaces it whole withthis = value(§2.3), which is how a method moves a value to another variant. - A value type cannot hold itself: an enum whose payload holds the enum by value — directly,
through a struct, a tuple or an optional — is refused (
LYR-SEM0056); the diagnostic names the cycle. A recursive payload lies behind a reference:Box<T>ofstd.core, a class of one's own, or an array.enum Tree { Leaf(int), Node(Box<Tree>, Box<Tree>) }. - (Informative.) An enum occupies a tag and the union of its payloads, inline. An optional of an enum is the enum with a tag no variant has (§3.1).
4.2 The enum the position names
.Redis the memberRedof the enum the position expects, the enum unnamed: a unit variant as it stands, the callee of.Num(3), the head of.Rect { w = 1, h = 2 }. The position expects an enum where a value of one is required: an initializer with a written type, an assignment, an argument, a return, an arm under a context type, a field of an initializer, a default, and either side of==and!=when the other side has the enum type. An expected?EnamesE:let o: ?Signal = .Greenis a present optional.- Where the position expects no particular type, or a type that is not an enum, the form is
refused (
LYR-SEM0113), and the diagnostic says to name the enum. The qualified formSignal.Redstands everywhere, whatever the position expects. - In a pattern the same form tests the variant (09 §2); a bare name never does.
5. Arrays, views and inline arrays
Elements in a row: behind a reference, through a view, or as a value (01 V10, 03 T13, T14, 10 C2, C7, N8).
Conformance. conformance/cases/03-types/.
5.1 T[]
T[]is an object of a fixed length holding its elements inline and contiguous — a struct element lies in the array, not behind it — reached through a reference (§2.1): a binding, an assignment, an argument and a return share the one array. The length is a property of the value, not of the type.[a, b, c]builds an array of its elements;[]takes its element type from the position — an array's, or a view's, where an array stands as a view of itself (§5.2 rule 4):take([])fortake(s: Slice<uint8>). A position that gives none — the target of an index or a member,[].length(); an operand of+; a hole of an f-string — refuses it (LYR-SEM0060). Where aFromArrayLiteral<T>ofstd.coreis expected at a coercion site (§1.3), through an optional as every coercion (§3.1), the literal is that type'sfromLiteralof a freshT[]of its elements, each checked againstT(10 K4):let s: Set<int> = [1, 2, 2];is a set of two,let m: Map<string, int> = [("a", 1)];a map — there is no map literal — and[]takesTfrom the type. A literal inside such a literal is checked againstTin turn. A type of the program's own is one as the library's are; a type that is not one refuses the literal (LYR-SEM0001).[x] * nbuilds an array ofnelements each a copy ofx— a value copied, a string shared — and panics withLYR-RT0007whennis negative;xs * nis the only order.xs + ysbuilds a new array of the elements of both (10 C7); so does a view plus a view,xs[1..] + ys[..], through std.core'sAddonSlice<T>. An element that is or holds an object — a class, an array, a view, a function value, an interface value, directly or inside a struct, an enum, a tuple, an optional or an inline array — is repeated byclone(05 §14), never shared into every slot: its type conforms toClone, or the repetition is refused witharrayOfas the way out (LYR-SEM0136). A string is shared; nobody can tell. In a generic body an element whose type a type parameter or an associated type decides counts as one that holds an object:[x] * nwithx: Trepeats bycloneunderT :: [Clone]and is refused without it, whatever the instances are — the body is checked once (§9; M8a S16).xs.length()is the length, a call with its parentheses like every length in the language (10 N8); the bare name is refused (LYR-SEM0012). The length and the index are the two primitives of an array; every other member is written in the standard library.xs[i]is the element ati. The index is anint: a narrower integer widens to it as at any coercion site (§1.3), auintor auint64is converted withas, nothing else stands there (LYR-SEM0007). An index outside0 ≤ i < length()panics withLYR-RT0003, in every profile — a negative index like any other; there is no counting from the end by sign. An element is a place in the array, whatever holds the array (§2.2):xs[i] = vandxs[i].f = vwrite the array through aletas through avar.^ninside the brackets counts from the end:xs[^1]is the last element,xs[^n]stands forxs[xs.length() - n]withxsevaluated once. It is sugar for that subtraction and nothing more — no type, no value of its own: outside[…], or as a part of a larger index expression, it is refused (LYR-SEM0114).^0as an index panics aslength()does; as the end of a range (§5.2) it is the length.
5.2 Slice<T>
Slice<T>is a view of the elements of an array — a pointer into them and a length, a value of two words that is copied by a binding and shares the elements: what is written through a view is written in the array, and what is written in the array is seen through every view of it.Sliceis a primitive ofstd.core, visible without an import.xs[a..b]is the view of the elements fromaup to but not includingb;xs[a..=b]includesb;xs[..b],xs[a..]andxs[..]leave the start, the end or both open. The bounds are indices as in §5.1,^namong them, and satisfy0 ≤ a ≤ b ≤ length()or the expression panics withLYR-RT0003, in every profile;xs[n..n]is the empty view,xs[length()..]too.- A view is indexed, measured and counted from the end as the array is (§5.1), its elements
are places in the array,
v[a..b]is a view of the same array, and an array pattern matches it (09 §2). - At a coercion site an array stands where a view of its element type is expected and gives a
view of itself, whole; a view never stands where an array is expected (
LYR-SEM0001). A receiver is such a site for the members written onSlice<T>(design 03 A2): where the array's own blocks give no member of the name,xs.m()is the view's member, called on a view of all ofxs, its constraints checked as for the view (LYR-SEM0134); a blanket member comes after it (05 §13 rule 7). The library writes such a member once. An array literal at such a site checks its elements against the view's element type, as against an array's (§5.1):take([1, 2])withtake(s: Slice<uint8>)builds auint8[]and passes a view of it. - There is no lifetime: the array lives as long as any view of it does, and a view kept in an object or returned from a function is as good as one in a local (01 L1).
StringView, the view of a string's bytes, is written with the strings (12).- A type that gives a view of itself conforms to
AsSlice<T>ofstd.core, whoseasSlice()is aSlice<T>of its elements;List<T>does. On such a valuex[a..b]isx.asSlice()[a..b], rule 2 over the view, its bounds and^nthe view's — a view of the elements, which a write through it writes (design 03 "Review 2026-10-09" S6). Itsx[i]stays its ownIndex: a second conformance for ranges would be twoindexof one arity (04 D4). A range on a value of any other type has no view to take (LYR-SEM0007).
5.3 T[N]
T[N]is a value of exactlyNelements,Na positive decimal literal, laid out inline where it lies — in a local, in a struct, in an object, in an element of an array — with the size and the identity of a struct (§2.1): a binding, an assignment, an argument and a return copy it. The length is part of the type:int[3]andint[4]are two types, and neither stands forint[]norint[]for them.T[N][M]is an inline array of inline arrays;T[N][]is an array of inline arrays.- It is built from a literal of exactly
Nelements, or from[x] * Nwith the literal countN, where aT[N]is expected; another length is refused (LYR-SEM0001). - It is indexed, measured and counted from the end as an array is (§5.1), with the same
check. An element is a place when the array is one — a
varlocal, avarfield of a place, an element of an array — as a field of a struct is (§2.2); aletfreezes it (LYR-SEM0019). - A view of an inline array (§5.2) is taken where the array lies in the heap — a field of
an object, an element of an array or of a view, a struct inside those — never of a local,
a parameter, or a field of a struct local, whose frame may end before the view
(
LYR-SEM0115); such an array is copied or passed whole. A value type cannot hold itself through an inline array (§4.1). - An array pattern of the one length covers the type (09 §3).
- An inline array has
length(),isEmpty()andtoArray()wherever it lies (the review's M4-2);toArray()is a newT[]holding copies of its elements. Where it lies in the heap (rule 4) it has the members of a view too, called on a view of all of it, as an array has them (§5.2 rule 4):board.cells.contains(x), andboard.cells.sort()sorts the array itself. Where it lies in a frame no view of it exists, and such a member is refused where it is called, with the way out — a copy,xs.toArray().contains(x)(LYR-SEM0115). An extend block does not target an inline array (LYR-SEM0047): a member written onSlice<T>is the one that reaches it. - Equality and hashing.
T[N]isEquatablewhereTis, and==compares the two element by element; it isHashablewhereTis, andhashwrites its elements into the hasher in order — nothing after them, as the length is the type's (design 10 C8). Wherever it lies:d == sha256(x), and auint8[16]is a key of aMap. An element type that is neither leaves the array neither (LYR-SEM0059for==). - Text.
T[N]isDebugandDisplaywhereTisDebug, as an array is (12 §Arrays and views):[1, 2, 3], each element in its debug text, the display text the same —{x.toBytesLE()}, and a width pads it. Wherever it lies:names.show()on an array in a frame is its own member, not a view's (M8c S16).
6. Tuples
Positional elements in one value (03 T16, 01 V11).
Conformance. conformance/cases/03-types/.
(A, B)and on, from two elements — there is no tuple of one and no empty tuple (voidstays what it is) — is a value of positional elements laid out inline where it lies, copied like a struct (§2.1); it has no identity.(1, "a")builds one. Its elements are not written; the tuple is replaced whole.t.0,t.1, … read the elements by position; a position the tuple does not have is refused (LYR-SEM0012).- A written type may label its elements:
(x: int, y: int), andp.xreadsp.0. A label is a name for the reader and no part of the type:(x: int, y: int)and(int, int)are one type, a value of either stands where the other is expected, and the label is seen through the type of the binding it was written on, not through another's. A label twice in one type is refused. - A tuple is taken apart by a pattern (09 §2) and by
let (a, b) = t;. Equality, hashing and display: rule 6. - A tuple literal takes the types the position expects of its elements (T8): where a tuple
of its length is expected — a binding with a type, an assignment, an argument, a return, a
field — each element stands where its element type is expected, as a binding's value does:
return (0, null);for a(int, ?int),(1, [])for a(int, int[]),(.Red, 1)for a(Color, int). An element that does not fit is the element's error (LYR-SEM0001). A tuple that is no literal is one value of its type and is converted by nothing: an(int, int)is no(int, ?int). - A tuple of two to eight elements is
Equatable,Hashable,DebugandDisplaywhere each of its elements is (04 D7, T16; the audit of 2026-10-07 found none built):==compares the elements in order,hashfeeds them in order,{t:?}writes(1, "x")and{t}(1, x)— so(1.5, 2) == (1.5, 2)holds and(int, string)is a set's element. std.core gives them through blocks, each under its own condition (05 §13 rule 6); a longer tuple has none of them.
7. Ranges
The values a..b and a..=b
(03 T13 A3).
Conformance. conformance/cases/03-types/.
a..bis a value ofRange<T>anda..=bofRangeInclusive<T>, structs ofstd.coreholding their bounds asstartandend;Tis the bounds' one type, a number type orchar— two characters bound a range of characters,'a'..='z'; other bounds are refused (LYR-SEM0003). The open formsa..,..band..are the structsRangeFrom<T>,RangeTo<T>andRangeFulland are written inside[…]only, where they take a view (§5.2).- In a
forhead,for (i in a..b)andfor (i in a..=b)with the range written there are the counted loop: no value is built, andfor (i in ..b)is refused. A range held in a binding is walked as anIterator(05), and so is a range of characters in a head: its walk passes over the surrogates (12 §Iteration). - A range is an ordinary value everywhere else: bound, stored in a field or an array, passed
and returned. A range is not an integer (
LYR-SEM0001).
8. Function values
Functions as values, and the lambdas that make them (03 T17, 01 V8, 02 M8, 08 Y11, F6, F8, F9).
Conformance. conformance/cases/03-types/.
8.1 The type
fn(A, B) -> Ris the type of a function value takingAandBand answeringR;fn() -> voidtakes nothing and answers nothing;fn(A) -> R throws Emay throw, its set part of the type (06 §8). A free function, a lambda and a method bound to its object are values of it alike (V8): what a value of the type is made of is not observable. The type has no variance:fn(int) -> intandfn(int8) -> intare two types. That a value throwing less fits a type throwing more is a coercion of its own (06 §8 rule 3), not a variance.- A value of a function type is called with
f(a, b); it is copied by a binding, held in a field, an array or an optional, passed and returned. Two function values are not compared. - (Informative.) A function value is a pointer to the code and the environment it runs in, two words; a function without an environment costs no allocation.
8.2 Lambdas
- A lambda is written in three forms (Y11 F1): parenthesized,
(n: int): int => n + 1, with the types and the return type as written or, when the position expects a function type, taken from it (F11); bare,x => x * 3, one parameter, its type from the position; trailing,xs.map { it * 2 }— a block after a call's parentheses, or after the callee alone, is the call's last argument, its one parameter the implicitit, or none when the position expects a function of none. - A trailing block may name its parameters before
=>:fold(xs, 0) { acc, x => acc + x },{ (k, v) => v }with a pattern (F2),{ &n => n += 1; }with a place (§2.3a rule 5a). The body is then the rest of the block. - The body is an expression or a block; a block's last expression without
;is its value (F3). An empty block is a body of no statements, a trailing one too:labelled("row") { },() => { }.returnleaves the lambda, never the enclosing function;breakandcontinuebelong to loops inside the lambda (F5) — but in a lambda an inline function's call sets in place (§8.5). - Without a position that expects a function type, the parameters of a bare or trailing
lambda have no type and are refused (
LYR-SEM0045); the parenthesized form writes them. - A brace after a name alone is an initializer's (§2.4) or a trailing
block's, and what it holds says which
(08 Y4). An
initializer holds nothing, or begins
name = valuefollowed by a,or the closing brace, or is a list of names (§2.4 rule 4),{ x, y },{ x, y = 0 }, that no=>ends — the names before a=>are a trailing block's parameters,{ acc, x => … }. One name alone,S { v }, is the shorthand initializer whereSnames a struct, a class or a struct variant — a type takes no trailing block, its factory is called with parentheses — and a trailing block's everywhere else,run { v }; after type arguments,Box<int> { v }, it is an initializer. Anything else is a block — and a block may begin with an assignment: a;behind that first value makes it a statement,run { total = 5; }. An assigning block without its;therefore reads as an initializer of the name before it, and is refused where that name is no type (LYR-SEM0011). An initializer begins no statement — its value would be dropped (LYR-PAR0055) — and a call with a trailing block does.
8.3 Captures
- A lambda uses the bindings of the scopes around it. A captured
let— a parameter included — lies in the closure as a copy taken when the lambda is made, which nothing can tell from the original since it cannot change; a capturedvarlies in a box the closure and the enclosing scope share, from its declaration on: what either side writes, the other reads (M8). - A loop binding is fresh per pass: the closures of
for (i in 0..3) { fs = fs + [() => i]; }answer 0, 1 and 2 (M8 C1). thisof a class is captured as the shared reference;thisof a struct as a copy of the value (M8 C2).
8.4 Functions and methods as values
- A free function named without a call is a value of its type; a static function
named through its type,
Point.new, likewise (F10). A function of another module is a value by every name that reaches it (04 §4): the name a selective import binds — its own, or another,import app.util { twice as double }—, the module's,util.twice, and those of a module that passes it on. A module's binding that holds a function value is called by the name an import gives it, as the function is. A generic function is not a value until instantiated (§9.4). obj.methodwithout a call is a closure bound toobj(F10): it holds the object — a class shared, a struct copied at the binding — and calls the method on it; its type is the method's without the receiver. It is called like any function value, and a call through it on a struct's copy changes the copy.arrayOf(n, f)ofstd.corebuilds an array ofnelements, the element atithe value off(i)— fresh elements each, where[x] * ncopies one (§5.1).
8.5 Inline functions
inline fn(08 F6) declares a function whose body is set in place at every call: a free function, a type's method or static, a block's, an enum's. The words stand[static] inline [mut] fn;inline staticis refused (LYR-PAR0032). The call is a call as any other (08 §1): its arguments are evaluated first, left to right, each once, its type arguments are bound as a generic call binds them, and the body'sreturngives the call's value. It has no frame of its own and is nothing to point at, and what runs in the body stands at the call: a panic in it names the call's line in the trace (M9a S12b;assert's does), a lambda set in place its own.- A lambda literal at one of its function parameters is set in place where the body calls the
parameter — no closure is made. Its
returnleaves the function that wrote it, through the body'sdefers; itsbreakandcontinuereach that function's loops and never the body's; what it throws leaves through the body. A block gives its value as its last expression (LYR-SEM0046where a value is wanted and none stands). Soxs.iter().forEach { if (it > limit) { return it; } }leaves the function that wrote it. A function value at the parameter — a binding, a function's name — is called as any value is. - A function parameter of an inline function is called, or passed on as the argument of
another inline function's function parameter, which sets the same lambda in place there; kept,
returned, captured or passed to anything else it is refused (
LYR-SEM0181). - An inline function that reaches itself through calls of inline functions is refused at the
call that closes the cycle (
LYR-SEM0182): setting it in place would not end. An inline function is called and is no value (LYR-SEM0183). - What a body set in place cannot hold is refused at the declaration (
LYR-SEM0184): an interface's member — it is called through its conformer's table —, a function without a body, amut fn— its receiver would be the caller's place —, a place parameter (§2.3a), a body that yields — a coroutine's body runs where it is pulled, not where it is called. - (Informative.) Kotlin's
inline funis the model: a lambda set in place is code of the caller, which the C compiler sees whole.std.core'sforEachis inline (12 §Iteration).
8.6 Local functions
- A local
fn(08 F8) in a block is a named closure,fn fact(n: int): int { return if (n <= 1) 1 else n * fact(n - 1); }. It binds its name as aletbinds one (07 §4) — from its statement on, once in its scope — and the name is in its own body: it calls itself. It captures as a lambda does (§8.3), avarit writes in the box the scope shares; it is a value of its function type, itsthrowsincluded; itsreturn,breakandcontinueare its own. - A closure is one function (08 F9): a local
fntakes no type parameters, and its parameters take no default and no variadic rest (LYR-PAR0060).
9. Generics
Type parameters, their inference, and what an instance is (03 T5–T8, T17, T18, 01 C3).
Conformance. conformance/cases/03-types/.
9.1 Type parameters and instances
- A function, a struct, a class, an enum, an interface and a type alias (§10)
may declare type parameters,
fn ident<T>(x: T): T,struct Pair<A, B> { … }; a parameter may carry constraints,<T :: [Show, Eq<T>]>, the interfaces an argument must satisfy (05) — one also without the brackets,<T :: Show>, where a,after it begins the next parameter. Only an interface bounds a parameter, and each bound is written once —From<int>andFrom<string>are two (LYR-SEM0078, as in a conformance list). Inside the declaration a value of typeThas the members its constraints give it and no others (T18): the body is checked once, at the declaration, not per instance. - An instance names the arguments:
Pair<int, string>,ident<int>. Two argument lists that differ are two types with no relation between them (T3): nothing coercesPair<int, string>toPair<int8, string>, and aT[]atT = int8is anint8[], laid out as one. An instance is an instance of one declaration: what else carries the name — the module's function beside a type's static, the statics of two types, two of a name that differ in their count (08 §1.2) — is another function under the same arguments too.Small.make<int>,Tiny.make<int>andmake<int>are three. - The number of arguments is the number of parameters (
LYR-SEM0026); an argument that does not satisfy its parameter's constraints is refused (LYR-SEM0028), written or inferred alike. A written instance is checked where it is written — a parameter, a result, a field, an annotation —, once for the place:fn f(s: Set<NoHash>)is the signature's error, not first that of a call that builds the set. A block's target names the type with the block's own parameters, and those carry the type's constraints with them:extend<I, E> MapIter<I, E>is checked asMapIteris declared. An alias's right side is checked where the alias is used, with the use's arguments in place:type Table<K> = Map<K, int>asks nothing ofK, andTable<NoHash>is the use's error. - Every instance is compiled as code of its own — monomorphization —
with
Treplaced throughout: anint8field takes one byte, a call on aTis a direct call. The compiler collects the instances of a program and compiles each once (C3). The set must be finite (T19): a chain of instances, each body asking for the next, is refused at a depth of 128 (LYR-SEM0197), the message naming the chain by its first links and its last —fn grow<T>(x: T, n: int)that callsgrow(W<T> { v = x }, n - 1)asks forgrow<W<T>>at every level and is refused, whatevernis at run time; a program that asks for more than 20 000 instances is refused alike. - A type parameter may carry a default (T18):
interface Add<Rhs = Self> { … },class Map<K, V, H = DefaultHasher>. A use that writes fewer arguments takes the defaults for the trailing ones, resolved where the type is declared —Selfthere is the conforming type, soVec2 :: [Add]isVec2 :: [Add<Vec2>], and the constraintT :: [Add]is satisfied by either spelling. - A constraint may fix an associated type of its interface,
T :: [Iterator<Item = int>], and the declaration names one as a type path,T.Item(05 §8). - A type parameter may stand for
void— a lambda that returns nothing binds it so,run(() => { println("x"); }). A value of typevoidis nothing: a parameter, a local, a field or an element of that type holds nothing, a?voidholds only whether one was given, and a call of a function that returns nothing gives that value wherever one is expected — passed, stored, wrapped, returned. (Informative: such a place costs one byte.) Written,voidis a type like any other —?void,void[]— but a name for its one value says nothing: a binding that holds avoidand a parameter written with it are warned about (LYR-SEM0173; the review's M6-5). In generic code there is no warning: the binding is aT.==and!=on avoidare refused (LYR-SEM0003): there is nothing to compare. - A function's type parameter that nothing in it names — no parameter, the result, the body
— is warned about (
LYR-SEM0200): no call can infer it. A type's is not (T18): a type may carry a parameter only its name uses, a phantom —struct Id<T> { raw: int }makesId<User>andId<Order>two types.
9.2 Inference at a call
- A call of a generic function may omit the type arguments:
ident(3)isident<int>(3). Each argument's type binds the parameters that occur in the parameter type it is passed to, at any depth —firstOf([1, 2])bindsT = intthroughT[]— and a lambda's type binds throughfn(T) -> Uonce its own parameters are known from the position. A parameter a fixation names takes the answer of the parameter it constrains, as in a block (05 §13 rule 2): infn countOf<I :: [Iterator<Item = T>], T>(it: I)the argument bindsI, andTisI'sItem; a fixed structure,Item = (K, V), binds what it names by matching. A plain value where?Uis asked bindsUwith it, the widening an assignment makes:orNone(3)bindsU = int, and a lambda whose body gives anintatfn(string) -> ?Uis afn(string) -> ?int—mapNotNull((s) => try int.parse(s)).nullbinds nothing. An array literal at a parameter that still holds a type parameter is checked after the other arguments, with the type they bound as its target (T8): inassertEq(x.toBytesBE(), [2u8, 1u8])the literal is auint8[2], and so it is inassertEq([2u8, 1u8], x.toBytesBE()); where they bind nothing, it binds as any argument. - What the arguments leave open, the expected type of the call's position binds (T8):
let xs: int[] = empty();,return empty();,count(empty()). The arguments bind first and the position never overrides them:let xs: int[] = ident("x");is the assignment's error, not a different instance. - A parameter that occurs in no argument, in no fixation of a bound parameter's constraint, and
not in the expected type is not inferred:
let xs = empty();is refused (LYR-SEM0060), with the instruction to write it. - Type arguments may be written,
empty<int>(), at a method as at a function:it.collect<List<int>>(),s.parse<int>()— without a::before them. Written ones bind before any argument, and an argument that does not fit them is the argument's error,ident<int>("x"); they meet their constraints with the parameters of the block the method stands in bound by its receiver (collect'sC :: [FromIterator<I.Item>]withIthe receiver's iterator). After a member,<opens type arguments where a(follows its>; else it is a comparison. A placeholder_stands for one argument the inference fills (T8):collect<_, string>(1, f)writesUand infersT. A placeholder nothing determines is refused as in rule 3. _stands only in a list of type arguments — of a call, of an initializer (§9.3), of an instantiated function (§9.4). Anywhere else,let p: Pair<_, int> = …included, it is refused (LYR-SEM0117).- A static function of a generic type, a variant that takes values and a static of a
generic block (05 §13) may be called through the type's bare name —
Cell.of(3),Opt.Some(7),Map.new()—, and the call binds the type's parameters as it binds its own (the review's M8a-2). For the type's parameters the expected type comes first, as for an initializer (§9.3): inlet a: Atomic<?int> = Atomic.new(1);the1is checked as a?int; then the arguments bind what is open. A parameter neither determines is refused (LYR-SEM0060), with the form to write,Cell<…>.none(…). Nothing else determines it — not a statement before, not a use behind. Without a call the bare name of a generic type names no member (LYR-SEM0063).
9.3 Generic types in an initializer
- An initializer of a generic type takes its arguments from what is written,
Pair<int, string> { first = 1, second = "x" }, else from the context — a binding with a type, an argument, a return, a field —let p: Pair<int, string> = Pair { first = 1, second = "x" };, else from its field values, each binding the parameters in its field's type as an argument binds a call's (§9.2; the review's M8a-2):Pair { first = 1, second = "x" }is aPair<int, string>. The context comes before the values —let c: Cell<?int> = Cell { v = 3 };is aCell<?int>. A parameter no field value determines is refused (LYR-SEM0060), with the form to write. - A written list may hold placeholders:
Pair<_, string> { first = 3, second = "x" }. A placeholder is filled from the context when the position names an instance, else from the field values, each checked with as much of its field's type as is known; a value that fixes no type of its own ([],null) fills nothing (LYR-SEM0060).
9.4 Instantiated functions as values
- A generic function with its arguments written,
ident<int>, is a value of the instance's type,fn(int) -> int(T17): held, passed and called like any function value (§8). A static method through its type likewise,Counter.make<bool>, and through an instance of a generic type,Pair<int>.of; an instance method through its type is no value (LYR-SEM0055) — it becomes one through its object,obj.method(§8.4). - A placeholder in the list is filled from the function type the position expects:
let s: fn(string) -> string = ident<_>;,apply(ident<_>, 4)withapply(f: fn(int) -> int, …). Where the position expects no function type, or one still holding an open parameter of its own, the argument is written. - The bare name of a generic function is no value (
LYR-SEM0052): a function value is monomorphic. - The arguments of the value form follow §9.1 rule 3 —
their number, also for a function that is not generic (
plain<int>), and their constraints.
10. Aliases
A type alias names a type (03 T15).
type Name = T;namesT: the alias andTare one type — a value of either is a value of the other —, and the alias is as visible as its word says (04).opaque typeis refused (LYR-SEM0175; the audit of 2026-10-07 found it taken in silence). A struct of one field takes its place: it has the layout of its field (03 T15), and its conformances,withand the field reach what an opaque type reached throughastwice.- An alias may take type parameters,
type Pair<T> = (T, T);(03 T15; N2b): a use names the arguments, andPair<int>is(int, int). They follow §9.1 — as many as the alias has parameters (LYR-SEM0026), a default for the trailing ones, the constraints checked at the use (LYR-SEM0028). An alias has no associated type of its own, so a named argument fixes nothing (LYR-SEM0128). An alias defined through itself is refused (LYR-SEM0064);Pair<Pair<int>>is none — the argument is expanded first.