Compile errors
Every error message the test suite pins: 368 distinct messages across 451 programs the compiler must reject. Each entry is the message, why the rule exists when the fixture says, and the program that provokes it. Find an error by searching this page for the text the compiler printed.
This page is generated from tests/errors/ by scripts/gen-error-catalog.ts. Improve an entry by improving the fixture or its leading comment, then regenerate. For warnings, which have names and flags, see Warnings & errors.
Index
'?' error type mismatch: 'Silent' cannot be boxed as 'Heap<Error>' because it does not satisfy interface 'Error' (needs message(self: &Self))'?&mut T' is only legal on a parameter of an 'extern' or '@externalLinkage' function'@!wrapping' takes no arguments'@copy' on 'Plain' does nothing: no variant of it carries a raw pointer'@copy' on 'Point' does nothing: no field of it is a raw pointer'@copyOnly' on 'Plain': it has no type parameters to constrain'@copyOnly(U)' on 'pick': 'U' is not one of its type parameters'@derive' is not supported on methods'@mustUse' takes no arguments'@pure' takes no arguments'@wrapping' on extern fn'@wrapping' takes no arguments'&mut' is implicit on a method receiver; write 'v.push(1)''&mut' marks an argument to a '&mut' parameter; it is not a value'&mut' on an argument to a '&P' parameter of 'show'; only a '&mut' parameter takes '&mut''&x' is not an expression: shared borrows are implicit (pass 'x' bare). Only a '&mut' argument is spelled out: 'f(&mut x)''add' in 'impl Add for Res' takes 'self: Res' by value; the trait 'Add' declares 'self: &Self''andThen': callback must return an Option, got i64'andThen': callback's error type must be string, got i64'area' in 'impl Shape for Sq' returns 'i64'; the trait 'Shape' declares 'f64''arenaGet<Res>' is not allowed: 'arenaGet' copies its element out, and 'Res' carries Drop'asciiIsDigit' is defined as a function in 'std/string.milo' and as a global in'b' is a nullable extern reference and must be unwrapped before use'b' may reallocate here while 'p' still points into its buffer (from 'b.bytes.ptr()' on line 26)'break' outside of loop'bump' in 'impl Counter for Tally' takes 'self: &Tally' by shared reference; the trait 'Counter' declares 'self: &mut Self''Cells<Vec<i64>>' is not allowed: 'Cells' is @copyOnly and 'Vec<i64>' is not a Copy type (it owns heap memory)'clone' would copy 'Res' out of the Vec: it carries Drop'context' error type mismatch: 'Silent' cannot be boxed as 'Heap<Error>' because it does not satisfy interface 'Error' (needs message(self: &Self))'continue' outside of loop'e' shadows an outer binding'f' returns i64 but can reach the end of its body without a 'return''fn Counter.get' is defined twice in this file'fn shade' is defined twice in this file'fold' callback parameter 2 is declared'get' argument 1: expected HandleB, got HandleA'get' is not available on 'Arena<Res>': 'get' copies its element out, and 'Res' carries Drop'get' would copy 'Res' out of the HashMap: it carries Drop'id' shadows an outer binding'if' is an expression'main' cannot be imported from './lib/frameCore''main' must return i32 or void'merge' in 'impl Merge for Box<i64>' takes 'other: &Box<string>'; the trait 'Merge' declares 'other: &Self''Meters' is not a generic type'n' shadows an outer binding'nonexistent' is not exported by 'lib/math''Ops.read' is a C function pointer and cannot be used as a value'Option' is a builtin enum and cannot be redeclared'orElse': callback's ok type must be i64, got string'p' used after its source 'v' was moved (from 'v.ptr()' on line 15)'parallelMap<Conn>' is not allowed: 'parallelMap' is @copyOnly and 'Conn' is not a Copy type (it implements Drop)'parallelMap<string>' is not allowed: 'parallelMap' is @copyOnly and 'string' is not a Copy type (it owns heap memory)'Point' is not generic, so 'Point<...> { … }' has no type arguments to take'ptr' is not available on Heap<Shape>'S' is not imported's' may reallocate here while 'p' still points into its buffer (from 's.cstr()' on line 9)'saturatingSub' expects 1 argument'scale' in 'impl Scale for Pt' takes 3 parameter(s); the trait 'Scale' declares 2'schedulerYield' can park this task while 'p' still points into 'g's buffer (from 'g.ptr()' on line 15)'schedulerYield' can park this task while 's' is a view into 'g's buffer'schedulerYield' can park this task while the loop variable is a reference into 'g's buffer'shatter' is private to shard.milo'sign' returns i64 but can reach the end of its body without a 'return''square' is not imported'step' can park this task while the loop variable is a reference into 'g's buffer'strlen' is not imported'struct Point' is defined twice in this file'total' can park this task while it holds a reference into 'g's buffer'Unit' is defined as a struct in 'std/prelude.milo' and as a struct in'unwrapOrElse' on a non-Copy Result<string>'v' goes out of scope before 'p', which would still point into its buffer (from 'v.ptr()' on line 13)'v' is borrowed mutably and shared in the same call'v' is reassigned here while 'p' still points into its buffer (from 'v.ptr()' on line 9)'v' is written here while 'p' still points into its buffer (from 'v.ptr()' on line 9)'v' may reallocate here while 'base' still points into its buffer (from 'v.ptr()' on line 10)'v' may reallocate here while 'p' still points into its buffer (from 'v.ptr()' on line 11)'v' may reallocate here while 'ps' still points into its buffer (from 'v.ptr()' on line 11)'v' may reallocate here while 'q' still points into its buffer (from 'v.ptr()' on line 10)'Vec' cannot be instantiated with 'void''w' may reallocate here while 'p' still points into its buffer (from 'v.ptr()' on line 9)'writer' writes the global 'g' while 'p' still points into 'g's buffer (from 'g.ptr()' on line 15)'x' shadows an outer binding'yieldNow' can park this task while the loop variable is a reference into 'g's buffer(via 'work' → 'record')@cName on 'Point.kind': only an 'extern struct' field has a C namea declaration does not match the C header it claims to describea JSON object's keys are stringsa nested fixed arrayambiguous From conversionan array of references is not expressiblean interface value has no cloneand is passed a reference into 'G' hereargument 'p' is passed to a '&mut' parameter without '&mut'argument 1 of 'loadUser': expected Id<UserTag>, got Id<OrderTag>array element has type string, but the array is declared [i64; 2]assert() condition must be bool, got i64assert() expects 1-2 arguments, got 3because 'v' is being iteratedborrowed mutably and shared in the same callborrowed mutably twiceC header disagreescallback parameter 1 is declaredcalling 'adopt' requires an unsafe blockcalling 'withRaw' requires an unsafe blockcalling a C function pointer requires 'unsafe' blockcan only be used as a pointercan't appear inside an expressioncannot assign to 'h.data' because 'h' is borrowedcannot assign to 'lb.buf' because 'lb' is borrowedcannot assign to 'n' because it is borrowedcannot assign to 's' because it is borrowedcannot assign to immutable variable 'p.x'cannot assign to immutable variable 'STORE.n'cannot assign to immutable variable 'x'cannot be instantiated with 'void'cannot be passed in a variadic positioncannot call 'add' on 'b' because it is borrowedcannot call 'feed' on 'lx' because it is borrowedcannot call 'push' on 'b' because it is borrowedcannot call 'push' on 'items' because it is borrowedcannot call 'push' on 's' because it is borrowedcannot call 'push' on 't' because it is borrowedcannot capture 's' in a closurecannot carry a payloadcannot cast [u8; 4] to i64: only to a pointercannot clear an immutable Veccannot cross the C ABIcannot derive 'Describe'cannot derive Clone for 'E': variant 'F' payload 0cannot derive Clone for 'Fd': it implements Dropcannot derive Clone for 'Handle': it holds a raw pointer ('p') and is not @copycannot derive Clone for 'Handle': it implements Dropcannot derive Clone for 'Q': field 'f' of type 'Option<move (i32) => i32>' has no clonecannot derive Clone for 'Q': field 'h' of type 'H' has no clonecannot have type 'void'cannot infer 'R' for 'Holder.make'cannot infer 'T' for 'Mk.empty'cannot infer the type arguments of 'Arena' for 'Arena.new(...)'cannot infer type for parameter 'x'cannot infer type parameter(s) 'T' for Option.Nonecannot infer Vec element typecannot insert into immutable HashMapcannot move '{ … }.name' out of 'R', which implements Dropcannot move 'b' because it is borrowedcannot move 'd.a' out of the borrowed 'd'cannot move 'd.text' out of the borrowed 'd'cannot move 'needle' out of a loopcannot move 'r.name' out of the borrowed 'r'cannot move 's' out of a loopcannot move 'self.inner.s' out of the borrowed 'self'cannot move 'toks[...].e' out of 'toks': the element stays in the container, so the move would leave a zeroed 'e' behindcannot move 'toks[...].text' out of 'toks': the element stays in the container, so the move would leave a zeroed 'text' behindcannot move 'u.name' out of the borrowed 'u'cannot move out of globalcannot move out of global 'GE'cannot move the borrowed value out ofcannot move the borrowed value out of 'line'cannot opencannot open 'nonexistent.txt'cannot pass 'b' because it is borrowedcannot pass 'items' because it is borrowedcannot pass 's', a shared '&' reference, as a '&mut' argumentcannot pass a closure that captures 'n' by reference to 'Promise.blocking', which keeps itcannot pass a closure that captures 'n' by reference to 'Task.spawn', which keeps itcannot pass a closure that captures 'n' by reference to 'wrap', which keeps itcannot pass immutablecannot pass immutable 'x' as a '&mut' argumentcannot push to immutable Veccannot replace through an immutable placecannot return 'v.ptr()': 'v' is freed when this function returns, so the pointer would danglecannot return a closure that captures 'n' by referencecannot return a closure that captures 's' by referencecannot return a referencecannot return a reference stored inside 'Option'cannot return a view of 'other'cannot return a view of 'tmp'cannot send 'p' of type '*u8' across threadscannot send 'u' of type 'Unsafe' across threadscannot send 'wrapped'cannot store a closure that captures 'n' by referencecannot store a reference in a Veccannot take 'Fd' out of a container by index: it carries Dropcannot take 'Handle' out of a container by index: it carries a raw pointer field ('p')cannot take 'Res' out of a container by index: it carries Dropcannot take 'Texture' out of a container by index: it carries @noCopycannot take a view of a temporarycannot take type parameterscarries a payload in 'Num'casts only to an integer typeclosure returns string but can reach the end of its body without a 'return'collides with the built-in @derive(Eq)constant expression '2147483647 + 1' overflows i32crosses the C ABI by value but is not declared 'extern struct'declared as *u8 but got **u8declared as string but got Resultdecreases clause must be an integer measuredepends on itself through another globaldid you mean 'clampF64' or 'clampI64'?did you mean 'toUpper'?discriminant 2147483648 is out of range for i32does not implement trait 'HasValue'does not implement trait 'HasValue', required by 'Wrap<T: HasValue>'does not satisfy interfaceensures clause must be boolenum 'E' is recursive by value and has infinite sizeenum 'List' has infinite size due to recursive fieldexpected '='expected 'IDENT'expected *HandleA, got *HandleBexpected a type, but the file ended hereexpected an integer lengthexpected NodeId, got EdgeIdexpected two string argumentsexpects 1 args, got 2extern 'fcntl' declares 3 fixed parameters but C fixes only 2field '_data' of 'Sealed' is private to 'std/seal.milo'field '_x' of 'S' is private tofield 'a' of 'Pair': expected i64, got stringfor range start must be an integerfrom "std/json" import { Json }has more than one @iter fieldhas no static method 'knew'has signature extern (*u8, i64) => i32, expected extern (*u8, i32) => i32if condition must be bool, got Result<JwtClaims, JwtError>infinite sizeinstantiation exceeded depthinteger literal 200 overflows i8integer-repr enum 'Bad' cannot be genericis @pure but calls 'helper', which is notis @pure but calls extern fn 'cbrt'is @pure but calls the function value 'f'is @pure but contains an 'unsafe' blockis @pure but touches the mutable global 'counter'is a copy of the matched payload — the write would be discardedis a directoryis a mutable global, and this code runs on a real OS threadis already unwrapped here — a second '&mut Bump' to the same object would alias the firstis borrowedis borrowed mutably and shared in the same callis borrowed mutably twice in the same callis bound by a pattern of an immutable subjectis incompleteis not a C function signatureis not a C header pathis not a C identifieris not a constantis not a library nameis not a nullable extern reference, so 'let q = … else { … }' has nothing to unwrapis not C-representableis not iterableis not supported on a struct fieldis not supported on enumsis not what unistd.h declaresit is not inside a '@fields { … }' blockjsonStringify: field 'tags' has unsupported typelet binding 'x' has no value after '='let-else block must divergemanual 'Send' implementation for 'Handle' must be unsafemark 'Conn' @copy if it does not own what the pointer points atMathException.kind (C 'type'): Milo says 8 bytes, C header disagreesMilo declares a 4-byte returnMilo declares i64 (8 bytes)Milo has no '++' — write 'i += 1'Milo says offset 0Milo writes through a *u32 (4-byte pointee)mismatched typesmissing required method 'eq'moves a captured value out of itself when it runsmust be an integer literalmust diverge (return/break/continue) — it runs when C passed nullno definition here to give linkage tono entry pointno whitespace allowed between '@' and attribute namenon-exhaustive matchnon-exhaustive match: missing variantnon-exhaustive match: missing variant 'Plain'not hashableold() may only appear in an 'ensures' clauseold() takes a scalaronly 'extern struct' has a C layout to verifyonly a top-level 'fn' can be stored in a C function-pointer fieldonly an 'extern fn'only an 'extern fn' has a C signature to verifyonly an 'extern struct' field can be C-invisibleonly an integer constant can be checkedonly valid as the iterable of a 'for ... in' loopOptionOption<Option<T>> has no distinct JSON encodingout of rangeoverflows u8passes struct 'Pt' by valuepointer field access requires 'unsafe' blockpointer parameter 'c' is stored in the global 'GS', which outlives the buffer it points intopointer parameter 'p' is stored in the global 'G', which outlives the buffer it points intopointer parameter 'p' is stored in the global 'GV' by 'push', which outlives the buffer it points intopush: expected i32, got boolreading 'T' by value through a raw pointer copies it bitwise; 'T' may own memoryreferences cannot be stored in a collectionreferences cannot be stored in structsrequires an unsafe blockrequires clause 'lo <= hi' violatedrequires clause 'x >= 0.0' violatedrequires clause must be boolrequires function to returnrequires impl 'Eq' for 'Score'requires numeric typeshadows a standard-library functionshadows an outer bindingstring.push: expected u8, got i64struct 'Direct<i64>' is recursive by value and has infinite sizestruct 'P' has no field 'nope'struct 'Ping<i64>' is recursive by value and has infinite sizestruct 'Plain' has no JSON codecswap: operands have different typestakes 2 parameters, the Milo declaration takes 3takes string by value, but each passes &stringtargetOs() takes no argumentsthe ranges 0..2 and 1..3 overlapthe struct does not derive Jsontwo fields map to the JSON name 'id'type '[i64]' has no method 'push'type 'Handle' has no method 'clone'type 'Math' has no static method 'minimum'type alias 'A' is cyclictype alias 'Loop' is cyclictype alias 'Pair' takes 1 type argument(s), got 0type alias 'Pair' takes 1 type argument(s), got 2type does not implement Sendtype mismatchtype mismatch in '-': u32 vs i32type mismatch in '+': i64 vs i32type mismatch: 'a' declared as i32 but got i64type parameter 'T' cannot carry a boundundefined function 'adoptHeap'undefined function 'adoptVec'undefined function 'foo'undefined function 'rawSlice'undefined variable 'y'unknown attribute '@drive'unknown attribute '@mustUse' on 'S'unknown escape sequence '\q'unknown module directive '@!foo'unreachable codeunsafe impl is only supported for the Send and Sync marker traitsunsupported representation 'u32'unwrap it with 'match'use of moved value 'p.a'use of moved variableuse of moved variable 'a'use of moved variable 'box'use of moved variable 'd'use of moved variable 'data'use of moved variable 'f'use of moved variable 'o'use of moved variable 'r'use of moved variable 's'use of moved variable 'value'use of moved variable 'x'value -1 is out of rangevalue 60000 is out of rangevariable 'a' already declared in this scopevariant 'Some' has 1 fields, but pattern has 2 bindingswas already moved out of itwhich implements Dropwrite 'self: &mut Self'write 'self: &Self'write 'self: Self'writes the global 'G', which is being iterated here
'?' error type mismatch: 'Silent' cannot be boxed as 'Heap<Error>' because it does not satisfy interface 'Error' (needs message(self: &Self))
? boxes an error into Heap<Error> only when its type can answer message(); a bare struct has nothing to say and is rejected at the ?, not at the caller's match.
struct Silent {
code: i32,
}
fn f(): Result<i32, Silent> {
return Result.Err(Silent { code: 1 })
}
fn g(): Result<i32, Heap<Error>> {
let v = f()?
return Result.Ok(v)
}
fn main(): i32 {
let _ = g()
return 0
}tests/errors/propagateNotError.milo
'?&mut T' is only legal on a parameter of an 'extern' or '@externalLinkage' function
Parameter position only — the same restriction &mut T already has, for the same reason: a returned reference outlives the call whose duration is the C contract.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn find(b: ?&mut Bump): ?&mut Bump {
return b
}
fn main() {
print(1)
}tests/errors/nullableRefAsReturn.milo
A closure has no C signature. @externalLinkage publishes ONE symbol, and its parameter list is the only place the spelling means anything.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn run(b: ?&mut Bump): i32 {
let f = (c: ?&mut Bump): i32 => {
return 0
}
return f(0 as *Bump)
}
fn main() {
print(1)
}tests/errors/nullableRefInClosureParam.milo
A field is storage, and a nullable extern reference has no storage form: it exists only between the call boundary and the unwrap. Same rule that stops a plain &mut T field.
extern struct Bump {
x: i32,
}
struct Holder {
b: ?&mut Bump,
}
fn main() {
print(1)
}tests/errors/nullableRefInStructField.milo
?&mut T is a spelling for a C T * at a signature seam, not a type. An ordinary Milo fn has no C caller to accept null from, so the nullable spelling would only be a way to smuggle a maybe-reference into the language.
extern struct Bump {
x: i32,
}
fn plain(b: ?&mut Bump): i32 {
return 0
}
fn main() {
print(plain(0 as *Bump))
}tests/errors/nullableRefOutsideExtern.milo
Not even nested inside an otherwise-legal extern parameter: a Vec<?&mut Bump> is storage for a maybe-reference, which is the escape the whole design exists to avoid.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn each(items: Vec<?&mut Bump>): i32 {
return 0
}
fn main() {
print(1)
}tests/errors/nullableRefTypeArg.milo
'@!wrapping' takes no arguments
@!wrapping(foo)
pub fn main() {}tests/errors/moduleWrappingArgs.milo
'@copy' on 'Plain' does nothing: no variant of it carries a raw pointer
@copy claims a pointer payload is not owned; with no pointer payload there is nothing for it to say.
@copy
enum Plain { A, B }
fn main(): i32 {
let p = Plain.A
return 0
}tests/errors/enumCopyAttrPointless.milo
'@copy' on 'Point' does nothing: no field of it is a raw pointer
@copy says "this struct holds a raw pointer it does not own". On a struct with no pointer field it is a no-op that lies about why the type is Copy, so it is rejected rather than ignored.
@copy
struct Point {
x: i64,
y: i64,
}
fn main() {
let p = Point { x: 1, y: 2 }
print(p.x)
}tests/errors/copyOnPointerFreeStruct.milo
'@copyOnly' on 'Plain': it has no type parameters to constrain
The attribute restricts what a generic's type parameters may be instantiated with. On a concrete type it would be a claim about nothing, and an annotation that does no work is the silent-failure class the attribute registry exists to close.
@copyOnly
struct Plain {
n: i64,
}
pub fn main(): i32 {
let p = Plain {
n: 1
}
print(p.n)
return 0
}tests/errors/copyOnlyNoTypeParams.milo
'@copyOnly(U)' on 'pick': 'U' is not one of its type parameters
@copyOnly(T) names the parameters it constrains, for a generic like parallelMapWith<T, S> whose per-worker state S never crosses the raw pointer. A name that is not a type parameter of the declaration constrains nothing, and an annotation that does no work is exactly what the attribute registry exists to catch.
@copyOnly(U)
fn pick<T>(v: &Vec<T>, i: i64): i64 {
return i
}
pub fn main(): i32 {
var v: Vec<i64> = Vec.new()
v.push(1)
print(pick(v, 0))
return 0
}tests/errors/copyOnlyArgNotTypeParam.milo
'@derive' is not supported on methods
struct Pt {
x: i64
}
impl Pt {
@derive(Clone)
fn get(self: &Pt): i64 {
return self.x
}
}
pub fn main() {
let p = Pt { x: 1 }
print(p.get())
}tests/errors/unknownAttrOnMethod.milo
'@mustUse' takes no arguments
@mustUse(x)
fn f(): bool {
return true
}
fn main(): i32 {
let _ = f()
return 0
}tests/errors/mustUseArgsRejected.milo
'@pure' takes no arguments
@pure(always)
fn square(x: i64): i64 {
return x * x
}
pub fn main() {
print(square(2))
}tests/errors/pureWithArgs.milo
'@wrapping' on extern fn
@wrapping
extern fn sqrt(x: f64): f64
pub fn main() {}tests/errors/wrappingOnExtern.milo
'@wrapping' takes no arguments
@wrapping(foo)
fn f() {}
pub fn main() {}tests/errors/wrappingWithArgs.milo
'&mut' is implicit on a method receiver; write 'v.push(1)'
A method receiver borrows implicitly; &mut goes on non-receiver arguments only.
fn main(): i32 {
var v: Vec<i64> = []
(&mut v).push(1)
return v.len() as i32
}tests/errors/explicitMutOnReceiver.milo
'&mut' marks an argument to a '&mut' parameter; it is not a value
&mut x is a call-argument marker, not an expression: a borrow cannot be bound or stored.
fn main(): i32 {
var x: i64 = 1
let r = &mut x
return r as i32
}tests/errors/explicitMutNotAValue.milo
'&mut' on an argument to a '&P' parameter of 'show'; only a '&mut' parameter takes '&mut'
&mut is spelled only where the parameter is &mut T; a shared borrow stays implicit.
struct P { n: i64 }
fn show(p: &P): i64 { return p.n }
fn main(): i32 {
var p = P { n: 1 }
let a = show(&mut p)
return a as i32
}tests/errors/explicitMutOnSharedParam.milo
'&x' is not an expression: shared borrows are implicit (pass 'x' bare). Only a '&mut' argument is spelled out: 'f(&mut x)'
&x is not an expression: shared borrows are implicit at the call site.
fn total(v: &Vec<i64>): i64 { return v.len() }
fn main(): i32 {
let v: Vec<i64> = [1, 2]
return total(&v) as i32
}tests/errors/borrowExprRejected.milo
'add' in 'impl Add for Res' takes 'self: Res' by value; the trait 'Add' declares 'self: &Self'
H7: a + b hands both operands to add by reference because that is what the trait declares. A by-value impl read a pointer as a struct and then dropped it.
var made: i64 = 0
var gone: i64 = 0
struct Res { n: i64 }
impl Drop for Res {
fn drop(self: &mut Self): void { gone = gone + 1 }
}
fn mk(n: i64): Res {
made = made + 1
return Res { n: n }
}
impl Add for Res {
fn add(self: Res, other: Res): Res { return mk(self.n + other.n) }
}
fn main(): i32 {
var v: Vec<Res> = Vec.new()
v.push(mk(3))
v.push(mk(4))
let r = v[0] + v[1]
print(r.n)
return 0
}tests/errors/implMethodByValueVsTraitRef.milo
'andThen': callback must return an Option, got i64
andThen stores the callback's return value into the result slot verbatim, because it is already an Option. A bare value there would be read back as a tag+payload pair — so the shape has to be checked, not inferred. Use map when the callback returns a plain value.
fn main() {
let o: Option<i64> = Option.Some(4)
let _bad = o.andThen((n) => n * 2)
}tests/errors/optionAndThenNotOption.milo
'andThen': callback's error type must be string, got i64
andThen forwards the receiver's Err payload verbatim on the short-circuit path, so there is nowhere to convert an error type. A mismatched E must be rejected up front — otherwise codegen would store an i64 into a slot the reader interprets as a string.
fn main() {
let ok: Result<i64, string> = Result.Ok(8)
let _bad = ok.andThen((n) => {
let r: Result<i64, i64> = Result.Ok(n)
return r
}
)
}tests/errors/resultAndThenErrMismatch.milo
'area' in 'impl Shape for Sq' returns 'i64'; the trait 'Shape' declares 'f64'
trait Shape {
fn area(self: &Self): f64
}
struct Sq { side: i64 }
impl Shape for Sq {
fn area(self: &Self): i64 { return self.side * self.side }
}
fn total<T: Shape>(s: &T): f64 {
return s.area()
}
fn main(): i32 {
let s = Sq { side: 3 }
print(total(s))
return 0
}tests/errors/implMethodWrongReturnType.milo
'arenaGet<Res>' is not allowed: 'arenaGet' copies its element out, and 'Res' carries Drop
The free-function spelling of the same accessor is rejected at the call, on the caller's line, rather than inside std/arena where the copy actually happens.
from "std/arena" import { Arena, Handle, arenaGet }
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
pub fn main(): i32 {
var a: Arena<Res> = Arena<Res>.new()
let h = a.alloc(Res { id: 1 })
let got = arenaGet(a, h)
print(got.isSome().toString())
return 0
}tests/errors/copyOutArenaGetFn.milo
'asciiIsDigit' is defined as a function in 'std/string.milo' and as a global in
Globals share the value namespace with functions — both become @name in the IR. Before the resolver checked this, the only signal was clang rejecting generated IR the user never wrote ("redefinition of function '@asciiIsDigit'").
let asciiIsDigit: i64 = 5
fn main(): i32 {
print(asciiIsDigit)
return 0
}tests/errors/globalShadowsStdlibFn.milo
'b' is a nullable extern reference and must be unwrapped before use
Option<&mut T> is an enum with a reference payload, which nestedRef already rejects as storage. A nullable extern reference must never become one: it is not a value, and the only thing that may be done with it is the unwrap.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn keep(b: ?&mut Bump): i32 {
let _maybe = Option.Some(b)
return 0
}
fn main() {
print(1)
}tests/errors/nullableRefIntoOption.milo
match is deliberately NOT a second spelling for the unwrap. It would need Option.Some(p) / Option.None patterns, which is exactly the Option<&mut T> mental model the design refuses to build: an enum with a reference payload, rejected as storage everywhere else. let p = b else { … } names no enum and no variant, so it is the only spelling, and this diagnostic points at it.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn readX(b: ?&mut Bump): i32 {
match b {
Option.Some(p) => {
return p.x
}
Option.None => {
return -1
}
}
}
fn main() {
print(1)
}tests/errors/nullableRefMatchNotSupported.milo
The parameter is a maybe-null pointer until the null test runs. Reaching a field through it is the exact bug the spelling exists to make impossible.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn bumpX(b: ?&mut Bump): i32 {
b.x = b.x + 1
return b.x
}
fn main() {
print(1)
}tests/errors/nullableRefNotUnwrapped.milo
Handing it to another function is a use like any other. The pointer crosses exactly one boundary — the one whose signature declared it — and is unwrapped there or nowhere.
extern struct Bump {
x: i32,
}
fn takes(_p: *Bump): i32 {
return 0
}
@externalLinkage
pub fn forward(b: ?&mut Bump): i32 {
return takes(b)
}
fn main() {
print(1)
}tests/errors/nullableRefPassedOn.milo
'b' may reallocate here while 'p' still points into its buffer (from 'b.bytes.ptr()' on line 26)
from "std/os" import {
strlen
}
struct Buf {
bytes: Vec<u8>
}
impl Buf {
fn grow(self: &mut Buf) {
var i = 0
while i < 100000 {
self.bytes.push(66)
i = i + 1
}
}
}
pub fn main(): i32 {
var b = Buf {
bytes: Vec.new()
}
b.bytes.push(65)
b.bytes.push(0)
let p = b.bytes.ptr()
b.grow()
print(strlen(p))
return 0
}tests/errors/ptrOutlivesMutSelfMethod.milo
'break' outside of loop
fn main(): i32 {
break
return 0
}tests/errors/breakOutsideLoop.milo
'bump' in 'impl Counter for Tally' takes 'self: &Tally' by shared reference; the trait 'Counter' declares 'self: &mut Self'
trait Counter {
fn bump(self: &mut Self): void
}
struct Tally { n: i64 }
impl Counter for Tally {
fn bump(self: &Self): void { print(self.n) }
}
fn main(): i32 {
var t = Tally { n: 0 }
t.bump()
return 0
}tests/errors/implReceiverModeMismatch.milo
'Cells<Vec<i64>>' is not allowed: 'Cells' is @copyOnly and 'Vec<i64>' is not a Copy type (it owns heap memory)
The diagnostic names the type the user WROTE. Monomorphization calls this instance Cells_Vec_i64, and that spelling used to leak into messages (backlog Tier 1 #33); nobody typed it, so nobody should have to read it. A nested generic is rejected on the same rule: Vec<i64> owns heap memory, so it is not Copy. The struct's body is beside the point; the attribute is a promise about how T is handled, not a scan.
@copyOnly
struct Cells<T> {
first: T,
len: i64,
}
pub fn main(): i32 {
let c: Cells<Vec<i64>> = Cells {
first: Vec.new(), len: 0
}
print(c.len)
return 0
}tests/errors/copyOnlyNamesWrittenType.milo
'clone' would copy 'Res' out of the Vec: it carries Drop
The builtin container clone is structural: it never runs the element's own Clone impl, so a Vec<Res> cloned this way released each resource once per copy.
var made: i64 = 0
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
impl Clone for Res { fn clone(self: &Self): Self { made = made + 1; return Res { id: self.id } } }
pub fn main(): i32 {
var v: Vec<Res> = Vec.new()
v.push(Res { id: 1 })
let w = v.clone()
print(w.len.toString())
return 0
}tests/errors/dropElementVecClone.milo
'context' error type mismatch: 'Silent' cannot be boxed as 'Heap<Error>' because it does not satisfy interface 'Error' (needs message(self: &Self))
.context boxes the error the way ? into Heap<Error> does, so it has the same gate: an error type with no message() cannot become a cause.
struct Silent {
code: i32,
}
fn f(): Result<i32, Silent> {
return Result.Err(Silent { code: 1 })
}
fn main(): i32 {
let r = f().context("calling f")
let _ = r
return 0
}tests/errors/contextNotError.milo
'continue' outside of loop
fn main(): i32 {
continue
return 0
}tests/errors/continueOutsideLoop.milo
'e' shadows an outer binding
A match-arm binding that shadows the function's own parameter must point at the ARM's binding (the offending site), not print bare with no location. This is the exact shape that once cost a downstream user several minutes of grep archaeology: a match arm rebinding e inside fn cloneExpr(e: Expr), with no file/line/caret on the diagnostic at all.
enum Expr {
Spread(i32),
Lit(i32),
}
fn cloneExpr(e: Expr): i32 {
match e {
Expr.Spread(e) => { return e }
Expr.Lit(v) => { return v }
}
}
fn main(): i32 {
return cloneExpr(Expr.Spread(3))
}tests/errors/shadowFnParamMatchArm.milo
'f' returns i64 but can reach the end of its body without a 'return'
A break in expression position (a match arm used as a value) leaves the while true, so the loop is not the function's exit and the body can fall off.
fn f(v: i64): i64 {
while true {
let x = match v {
0 => 1
_ => { break }
}
print(x)
}
}
fn main(): i32 {
print(f(1))
return 0
}tests/errors/fallOffEndBreakInMatchExpr.milo
'fn Counter.get' is defined twice in this file
Two inherent impl blocks defining the same method mangle to one symbol. Without the resolver check the only signal is LLVM's "invalid redefinition of function".
struct Counter {
v: i64,
}
impl Counter {
fn get(self: &Self): i64 {
return self.v
}
}
impl Counter {
fn get(self: &Self): i64 {
return self.v + 100
}
}
fn main() {
let c = Counter { v: 1 }
print("{}", c.get())
}tests/errors/duplicateImplMethod.milo
'fn shade' is defined twice in this file
A duplicated block (bad merge, botched scripted edit) used to compile: the flat namespace kept the last body and every call site silently ran it.
pub fn shade(a: i64): i64 {
return a * 2
}
pub fn shade(a: i64): i64 {
return a * 3
}
fn main() {
print("{}", shade(2))
}tests/errors/duplicateFnDecl.milo
'fold' callback parameter 2 is declared
fold validated its callback's RETURN type against the accumulator and never its parameters, so fold(0, (acc: i64, x: &string) => acc + x.len) over a Vec<i64> read each element as a string pointer and folded garbage (-16). It was the one combinator the mechanical fix missed, because its callback is args[1] rather than args[0] — which is exactly the shape this session keeps finding: a rule applied across sites, with the odd one out forgotten. Now gated by tests/callbackSigCoverage.test.ts, which requires every cbHint site to consult checkCallbackSig.
fn main() {
var v: Vec<i64> = Vec.new()
v.push(7)
let r = v.fold(0, (acc: i64, x: &string) => acc + x.len)
print("folded ", r)
}tests/errors/foldCallbackParamType.milo
'get' argument 1: expected HandleB, got HandleA
Two arenas of the same payload type hand out the same Handle<Node>, so a handle from graph A type-checks against graph B and only the runtime id check says no. Branding each arena with its own wrapper pair makes the mixup a compile error. The _ fields keep the brand unstrippable from any other file (private fields).
from "std/arena" import { Arena, Handle }
struct Node {
name: string,
}
struct GraphA { _a: Arena<Node> }
@derive(Eq)
struct HandleA { _h: Handle<Node> }
impl GraphA {
fn new(): GraphA { return GraphA { _a: Arena<Node>.new() } }
fn alloc(self: &mut Self, n: Node): HandleA { return HandleA { _h: self._a.alloc(n) } }
fn get(self: &Self, h: HandleA): Option<Node> { return self._a.get(h._h) }
}
struct GraphB { _b: Arena<Node> }
@derive(Eq)
struct HandleB { _h: Handle<Node> }
impl GraphB {
fn new(): GraphB { return GraphB { _b: Arena<Node>.new() } }
fn alloc(self: &mut Self, n: Node): HandleB { return HandleB { _h: self._b.alloc(n) } }
fn get(self: &Self, h: HandleB): Option<Node> { return self._b.get(h._h) }
}
fn main(): i32 {
var a = GraphA.new()
var b = GraphB.new()
let h = a.alloc(Node { name: "x" })
let _m = b.get(h)
return 0
}tests/errors/arenaBrandMixup.milo
'get' is not available on 'Arena<Res>': 'get' copies its element out, and 'Res' carries Drop
Arena.get is @copyOut: it hands back a structural copy of the element, which for a Drop type is a second owner. The method is absent from this instantiation while alloc, read, modifyMut and free stay (tests/fixtures/arenaDropElements.milo).
from "std/arena" import { Arena, Handle }
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
pub fn main(): i32 {
var a: Arena<Res> = Arena<Res>.new()
let h = a.alloc(Res { id: 1 })
let got = a.get(h)
print(got.isSome().toString())
return 0
}tests/errors/copyOutArenaGet.milo
'get' would copy 'Res' out of the HashMap: it carries Drop
m.get(k) hands back a copy of the value, the map keeps its own, and both run Drop. for k, v in m borrows; remove takes the map's copy out for real.
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
pub fn main(): i32 {
var m: HashMap<i64, Res> = HashMap.new()
m.insert(1, Res { id: 1 })
let got = m.get(1)
print(got.isSome().toString())
return 0
}tests/errors/dropElementHashMapGet.milo
'id' shadows an outer binding
Two levels deep: a match nested inside another match's arm rebinds id, which is the outer function's own parameter. This mirrors the second real-world occurrence of the shadow bug — JSValue.Native(id) nested inside fn evalExprFallback(prog, id: ExprId, ...), thousands of lines into a file, with a diagnostic that carried no location at all.
enum JSValue {
Native(i32),
Number(i32),
}
enum Op {
Wrap(i32),
Direct(i32),
}
fn evalExprFallback(id: i32, op: Op): i32 {
match op {
Op.Wrap(v) => {
var val = JSValue.Native(v)
match val {
JSValue.Native(id) => { return id }
JSValue.Number(id) => { return id }
}
}
Op.Direct(v) => { return v }
}
}
fn main(): i32 {
return evalExprFallback(1, Op.Direct(2))
}tests/errors/shadowFnParamNestedMatchArm.milo
'if' is an expression
? is already the propagate operator, so a ternary leaves its ':' stranded. The hint points at if, which is an expression here and covers the same use.
pub fn main(): i32 {
let x: i64 = 5
let y = x > 3 ? "big" : "small"
print(y)
return 0
}tests/errors/noTernaryOperator.milo
'main' cannot be imported from './lib/frameCore'
An imported module's main is left out of the program (the entry file's main is the only one), so naming it in an import list can never mean anything useful.
from "./lib/frameCore" import { main }
pub fn run(): i32 {
return 0
}tests/errors/importMainByName.milo
'main' must return i32 or void
fn main(): i64 {
return 0
}tests/errors/mainBadReturn.milo
'merge' in 'impl Merge for Box<i64>' takes 'other: &Box<string>'; the trait 'Merge' declares 'other: &Self'
Self on the trait side is the instantiated type: the generic impl over Box<T> is checked once per instantiation, and Box<string> is not Self when Self is Box<i64>.
trait Merge {
fn merge(self: &Self, other: &Self): i64
}
struct Box<T> { v: T }
impl Merge for Box<T> {
fn merge(self: &Self, other: &Box<string>): i64 { return other.v.len }
}
fn main(): i32 {
let a = Box { v: 1 }
let b = Box { v: "hi" }
print(a.merge(b))
return 0
}tests/errors/implGenericTraitSubstSelf.milo
'Meters' is not a generic type
The mirror of the arity error: an ordinary alias stands for one type, so arguments have nowhere to go. This message predates generic aliases and is already the right one, so there is no second rule for it - the fixture is here to keep it that way now that SOME aliases do take arguments.
type Meters = f64
pub fn main(): i32 {
let x: Meters<i64> = 1.0
print(x)
return 0
}tests/errors/aliasTakesNoTypeArgs.milo
'n' shadows an outer binding
A closure parameter shadowing an enclosing local used to be rejected with the same bare, location-less diagnostic as the match-arm case: closure params are declared without a span the same way function params were.
pub fn main(): i32 {
let n = 5
let f = (n: i32): i32 => n + 1
return f(3)
}tests/errors/shadowClosureParam.milo
'nonexistent' is not exported by 'lib/math'
from "lib/math" import {
nonexistent
}
fn main(): i32 {
return 0
}tests/errors/importNamedMissingSymbol.milo
'Ops.read' is a C function pointer and cannot be used as a value
Binding the field to a local would need a thin-to-fat conversion — an environment invented out of nothing — which is the conversion this feature deliberately does not build. Call it, or test it with isNull; there is no third thing.
extern struct Ops {
read: (*u8, i32) => i32,
}
fn bump(_p: *u8, n: i32): i32 {
return n + 1
}
fn main() {
let ops = Ops {
read: bump,
}
let f = ops.read
print(1)
}tests/errors/externFnPtrAsValue.milo
Storage outside the extern struct is the case that would need the thin pointer to become a Milo fn value. A Vec element is storage.
extern struct Ops {
read: (*u8, i32) => i32,
}
fn bump(_p: *u8, n: i32): i32 {
return n + 1
}
fn main() {
let ops = Ops {
read: bump,
}
var fs: Vec<(*u8, i32) => i32> = Vec.new()
fs.push(ops.read)
print(fs.len)
}tests/errors/externFnPtrInVec.milo
Handing the field to a Milo parameter typed (A, B) => R would hand a fat-pointer callee half a value: the parameter's calling convention prepends an environment argument the C function never declared.
extern struct Ops {
read: (*u8, i32) => i32,
}
fn bump(_p: *u8, n: i32): i32 {
return n + 1
}
fn apply(f: (*u8, i32) => i32, n: i32): i32 {
return f(0 as *u8, n)
}
fn main() {
let ops = Ops {
read: bump,
}
print(apply(ops.read, 41))
}tests/errors/externFnPtrPassedOn.milo
Returning it is the same escape as binding it: outside the field there is nowhere for a one-word callee to live that does not promise the two-word calling convention.
extern struct Ops {
read: (*u8, i32) => i32,
}
fn bump(_p: *u8, n: i32): i32 {
return n + 1
}
fn pick(ops: &Ops): (*u8, i32) => i32 {
return ops.read
}
fn main() {
let ops = Ops {
read: bump,
}
print(1)
}tests/errors/externFnPtrReturned.milo
'Option' is a builtin enum and cannot be redeclared
Option is a compiler builtin with dedicated syntax, and prelude signatures name it. Redeclaring it used to be allowed and broke std three files away.
pub enum Option {
Some(i32),
None,
}
pub fn main(): i32 {
let x = Option.Some(42)
if let Option.Some(v) = x {
print(v)
}
return 0
}tests/errors/redeclareBuiltinOption.milo
'orElse': callback's ok type must be i64, got string
orElse forwards the receiver's Ok payload verbatim on the short-circuit path, so there is nowhere to convert it. Only the ERROR type is free to change — that is the whole difference from andThen, which pins E and lets T vary.
fn main() {
let bad: Result<i64, i64> = Result.Err(1)
let _worse = bad.orElse((e) => {
let r: Result<string, i64> = Result.Err(e)
return r
}
)
}tests/errors/resultOrElseOkMismatch.milo
'p' used after its source 'v' was moved (from 'v.ptr()' on line 15)
Moving the source to an owner the checker cannot see ends every holder of a pointer into it: take may free the buffer before p is read. forget(v) is the one move that keeps p alive (tests/fixtures/ptrForgetStillLegal.milo).
from "std/os" import {
strlen
}
fn take(v: Vec<u8>): i64 {
return v.len
}
pub fn main(): i32 {
var v: Vec<u8> = [65, 0]
let p = v.ptr()
print(take(v))
print(strlen(p))
return 0
}tests/errors/ptrUsedAfterSourceMoved.milo
'parallelMap<Conn>' is not allowed: 'parallelMap' is @copyOnly and 'Conn' is not a Copy type (it implements Drop)
@copyOnly on a generic FN is checked at the call, with the type argument the call inferred. A struct with a Drop impl is move-tracked however plain its fields are, so a window over a Vec<Conn> would hand get's caller a second owner of every descriptor.
from "std/shard" import {
Shard, parallelMap
}
struct Conn {
fd: i32,
}
impl Drop for Conn {
fn drop(self: &mut Self) {
print(self.fd)
}
}
fn keep(w: Shard<Conn>): Shard<Conn> {
return w
}
pub fn main(): i32 {
var conns: Vec<Conn> = Vec.new()
conns.push(Conn {
fd: 3
}
)
let back = parallelMap(conns, 1, keep)
print(back.len)
return 0
}tests/errors/copyOnlyFnDropStruct.milo
'parallelMap<string>' is not allowed: 'parallelMap' is @copyOnly and 'string' is not a Copy type (it owns heap memory)
Shard.get reads self.base[i] through a raw pointer: a bitwise copy of the element. For a string that is a second owner of one heap block, and this program (the 2026-09 soundness sweep's hole 1) freed it twice: once when a went out of scope and again when the reassembled Vec was dropped, a malloc abort at HEAD. The element type is decided where parallelMap is called, so that is where it is refused.
from "std/shard" import {
Shard, parallelMap
}
fn extend(w: Shard<string>): Shard<string> {
var a = w.get(0)
a = a + "tail"
return w
}
pub fn main(): i32 {
var data: Vec<string> = Vec.new()
data.push("y".repeat(100))
let v = parallelMap(data, 1, extend)
print(v[0].len)
return 0
}tests/errors/shardStringRejected.milo
'Point' is not generic, so 'Point<...> { … }' has no type arguments to take
Type arguments on a literal of a non-generic struct name nothing; saying so beats silently dropping them.
struct Point {
x: i64,
}
fn main() {
let p = Point<i64> { x: 1 }
print(p.x)
}tests/errors/structLitTypeArgsNotGeneric.milo
'ptr' is not available on Heap<Shape>
A Heap<dyn I> is two words, a box and a vtable, so no single raw pointer stands for it: handing C the box alone would strand the half that does the dispatch. The give leg exists for a Heap<T> whose value IS the allocation, which is every non-interface T.
interface Shape {
fn area(self: &Self): i64
}
struct Square {
side: i64,
}
impl Square {
fn area(self: &Self): i64 {
return self.side * self.side
}
}
pub fn main(): i32 {
let s: Heap<Shape> = Heap(Square { side: 3 })
let _p = s.ptr()
return 0
}tests/errors/heapPtrInterface.milo
'S' is not imported
Types too: a struct named in a signature or literal has to be in the list, even when a function that returns it is.
from "lib/privateField" import { makeS }
fn take(s: S): i32 {
return 0
}
fn main(): i32 {
return take(makeS(1))
}tests/errors/importListRestrictsType.milo
's' may reallocate here while 'p' still points into its buffer (from 's.cstr()' on line 9)
from "std/os" import {
strlen
}
pub fn main(): i32 {
var s = "hello".clone()
var i = 0
let p = s.cstr()
while i < 100000 {
s.push(65 as u8)
i = i + 1
}
print(strlen(p))
return 0
}tests/errors/cstrOutlivesPush.milo
'saturatingSub' expects 1 argument
checker.error() records a diagnostic and returns — it does not throw — so an arity check that doesn't return falls straight through into args[0] and crashes on the argument it just reported as missing. Found by scripts/fuzz-frontend.ts; the same shape guarded the wrapping/checked/rotate intrinsics and Option/Result unwrapOr.
pub fn main(): i32 {
let c: u8 = 20
let diff = c.saturatingSub()
return 0
}tests/errors/saturatingArithNoArgs.milo
'scale' in 'impl Scale for Pt' takes 3 parameter(s); the trait 'Scale' declares 2
trait Scale {
fn scale(self: &Self, k: i64): i64
}
struct Pt { x: i64 }
impl Scale for Pt {
fn scale(self: &Self, k: i64, extra: i64): i64 { return self.x * k + extra }
}
fn main(): i32 {
let p = Pt { x: 2 }
print(p.scale(3, 1))
return 0
}tests/errors/implMethodWrongArity.milo
'schedulerYield' can park this task while 'p' still points into 'g's buffer (from 'g.ptr()' on line 15)
The pointer spelling of the slice-across-park rule: p points into the global's buffer and stays live to the end of its block, so a park after it lets another task push to the global and free that buffer before this task resumes.
from "std/os" import {
strlen
}
from "std/runtime" import {
schedulerYield
}
var g: Vec<u8> = []
pub fn main(): i32 {
g.push(0)
let p = g.ptr()
schedulerYield()
print(strlen(p))
return 0
}tests/errors/ptrGlobalAcrossPark.milo
'schedulerYield' can park this task while 's' is a view into 'g's buffer
A slice binding is a fat pointer into the global's buffer and stays live to the end of its block, so a park anywhere after it in that block is the same hazard as a park inside a for-in. Decided by the binding's type, not its spelling.
from "std/runtime" import {
schedulerYield
}
var g: Vec<i64> = []
pub fn main(): i32 {
g.push(1)
g.push(2)
let s = g[0..2]
schedulerYield()
print(s[0])
return 0
}tests/errors/globalSliceAcrossPark.milo
'schedulerYield' can park this task while the loop variable is a reference into 'g's buffer
A for-in binding is a reference into the global's buffer. schedulerYield parks this task; the writer task then pushes 100000 elements, the buffer reallocs, and the next iteration reads freed memory. This program printed reader sees 4528 before the rule. The fix is to iterate by index, snapshot with .clone(), or take the global with replace(g, []) into a task-owned value (see tests/fixtures/globalIndexLoopAcrossYield.milo).
from "std/runtime" import {
Task, schedulerYield, schedulerRunToCompletion
}
var g: Vec<i64> = []
fn reader(): void {
for x in g {
print("reader sees " + x.toString())
schedulerYield()
}
}
fn writer(): void {
var i: i64 = 0
while i < 100000 {
g.push(777)
i = i + 1
}
var _junk: Vec<i64> = Vec.filled(4, 424242)
print("writer done")
}
pub fn main(): i32 {
g.push(1)
g.push(2)
g.push(3)
Task.spawn(move() => {
reader()
}
)
Task.spawn(move() => {
writer()
}
)
schedulerRunToCompletion()
return 0
}tests/errors/globalForInAcrossYield.milo
'shatter' is private to shard.milo
H2 from docs/plans/soundness-sweep-2026-09.md. This program divided a Vec by hand, handed a window to a worker and returned, dropping the Shards owner while the worker still wrote through the window: a heap-use-after-free under --sanitize that no checker rule could see. The fix is that the manual cycle no longer exists outside std/shard: shatter, windows and weld are module-private, and the only way to divide a buffer is a closed form (parallelMap, parallelMapWith, parallelScanStr) that awaits every worker before the owner can go away.
from "std/shard" import {
Shard, shatter
}
from "std/runtime" import {
Promise
}
fn leak(): Promise<Shard<f64>> {
var data: Vec<f64> = Vec.filled(1000, 1.0)
var owner = shatter(data, 1)
var ws = owner.windows()
let w = ws.pop()!
return Promise<Shard<f64>>.blocking(move(): Shard<f64> => {
var s = w
s.set(0, 2.0)
return s
}
)
}
pub fn main(): i32 {
let p = leak()
let s = p.await()!
print(s.get(0))
return 0
}tests/errors/shardsManualPathPrivate.milo
'sign' returns i64 but can reach the end of its body without a 'return'
A non-void fn whose body can finish without a return. Codegen used to hand an i32 fn an implicit 0 and let every other type reach clang as a block with no terminator.
fn sign(n: i64): i64 {
if n < 0 { return -1 }
}
fn main(): i32 {
print(sign(1))
return 0
}tests/errors/fallOffEndFn.milo
'square' is not imported
The import list is the whole of what a module lends this file: square is exported by lib/math, but this file only asked for add. Before 2026-09-20 the list was validated and then ignored, so any export of any imported module was callable.
from "lib/math" import { add }
fn main(): i32 {
return add(square(2), 1)
}tests/errors/importListRestricts.milo
'step' can park this task while the loop variable is a reference into 'g's buffer
The may-park summary is transitive over the call graph: the loop body calls step, step calls pause, pause yields. The error names the call the loop body made, which is where the fix goes.
from "std/runtime" import {
schedulerYield
}
var g: Vec<i64> = []
fn pause(): void {
schedulerYield()
}
fn step(x: i64): void {
pause()
print(x)
}
pub fn main(): i32 {
g.push(1)
for x in g {
step(x)
}
return 0
}tests/errors/globalForInParkTwoDeep.milo
'strlen' is not imported
The degenerate case of the list-does-not-restrict hole: an empty list imported everything.
from "std/os" import { }
fn main(): i32 {
print(strlen("hi"))
return 0
}tests/errors/importListEmpty.milo
'struct Point' is defined twice in this file
The two copies can drift — whichever field layout survives the merge is the one every use gets, with no diagnostic.
pub struct Point {
x: i64,
y: i64,
}
pub struct Point {
x: i64,
y: i64,
z: i64,
}
fn main() {
let p = Point { x: 1, y: 2, z: 3 }
print("{}", p.x)
}tests/errors/duplicateStructDecl.milo
'total' can park this task while it holds a reference into 'g's buffer
A &[T] parameter is a view into the argument's buffer for as long as the callee runs, even when the argument is the bare global (the whole Vec coerces to a slice). &Vec<T> / &mut Vec<T> name the header, which survives a realloc, and stay legal.
from "std/runtime" import {
schedulerYield
}
var g: Vec<i64> = []
fn total(xs: &[i64]): i64 {
schedulerYield()
var t: i64 = 0
for x in xs {
t = t + x
}
return t
}
pub fn main(): i32 {
g.push(1)
print(total(g))
return 0
}tests/errors/globalSliceArgAcrossPark.milo
'Unit' is defined as a struct in 'std/prelude.milo' and as a struct in
A type's body IS its signature, so redefining a prelude type with different fields is the type-level equivalent of the shadows-stdlib signature mismatch fns already reject: std's own uses of Unit would bind to this definition. (An identical redefinition still merges — see tests/modules.test.ts.)
pub struct Unit {
x: i64,
}
fn main(): i32 {
let u = Unit { x: 1 }
print(u.x)
return 0
}tests/errors/typeShadowsPreludeType.milo
'unwrapOrElse' on a non-Copy Result<string>
Same gate as unwrapOr: the Ok payload is LOADED out of the enum, not moved, so an owned payload would end up with two owners and two frees. match moves it out safely, and ?? collapses the Option form without the restriction.
fn main() {
var s = ""
s.pushStr("hi")
let r: Result<string, i64> = Result.Ok(s)
let _bad = r.unwrapOrElse((e) => "fallback")
}tests/errors/resultUnwrapOrElseNonCopy.milo
'v' goes out of scope before 'p', which would still point into its buffer (from 'v.ptr()' on line 13)
from "std/os" import {
strlen
}
pub fn main(): i32 {
var seed: Vec<u8> = Vec.new()
seed.push(0)
var p: *u8 = seed.ptr()
if true {
var v: Vec<u8> = Vec.new()
v.push(0)
p = v.ptr()
}
print(strlen(p))
return 0
}tests/errors/ptrAssignedFromInnerScope.milo
'v' is borrowed mutably and shared in the same call
An inline v.ptr() argument is a shared borrow of v for the call, so it cannot sit beside a &mut v argument: the callee pushes through the reference and then reads the stale pointer. Same rule as grow(v, v[0]), one more way of spelling the shared side.
from "std/os" import {
strlen
}
fn growRead(p: *u8, v: &mut Vec<u8>): u64 {
v.push(66)
return strlen(p)
}
pub fn main(): i32 {
var v: Vec<u8> = [65, 0]
print(growRead(v.ptr(), &mut v))
return 0
}tests/errors/ptrInlineAliasMutArg.milo
'v' is reassigned here while 'p' still points into its buffer (from 'v.ptr()' on line 9)
from "std/os" import {
strlen
}
pub fn main(): i32 {
var v: Vec<u8> = Vec.new()
v.push(0)
let p = v.ptr()
v = Vec.new()
print(strlen(p))
return 0
}tests/errors/ptrOutlivesReassign.milo
'v' is written here while 'p' still points into its buffer (from 'v.ptr()' on line 9)
from "std/os" import {
strlen
}
pub fn main(): i32 {
var v: Vec<u8> = Vec.new()
v.push(0)
let p = v.ptr()
v[0] = 65
print(strlen(p))
return 0
}tests/errors/ptrOutlivesIndexWrite.milo
'v' may reallocate here while 'base' still points into its buffer (from 'v.ptr()' on line 10)
A cast forwards provenance: as i64 is still an address into v's buffer.
from "std/os" import {
strlen
}
pub fn main(): i32 {
var v: Vec<u8> = Vec.new()
v.push(0)
let base = v.ptr() as i64
v.push(1)
unsafe {
print(strlen(base as *u8))
}
return 0
}tests/errors/ptrCastOutlivesRealloc.milo
'v' may reallocate here while 'p' still points into its buffer (from 'v.ptr()' on line 11)
A bound *T from v.ptr() is an element view: push may realloc and free the buffer it addresses.
from "std/os" import {
strlen
}
pub fn main(): i32 {
var v: Vec<u8> = Vec.new()
v.push(65)
v.push(0)
let p = v.ptr()
var i = 0
while i < 100000 {
v.push(66)
i = i + 1
}
let n = strlen(p)
print("len " + n.toString())
return 0
}tests/errors/vecPtrOutlivesRealloc.milo
'v' may reallocate here while 'ps' still points into its buffer (from 'v.ptr()' on line 11)
A *T pushed into a container makes the container the holder: no binding of *u8 exists in the program text, but ps points into v's buffer for as long as it lives.
from "std/os" import {
strlen
}
pub fn main(): i32 {
var v: Vec<u8> = [65, 0]
var ps: Vec<*u8> = []
ps.push(v.ptr())
v.push(66)
print(strlen(ps[0]))
return 0
}tests/errors/ptrEscapesIntoVec.milo
'v' may reallocate here while 'q' still points into its buffer (from 'v.ptr()' on line 10)
A call whose result carries a pointer inherits the views of its pointer arguments, so a fn that hands its *T parameter back does not launder the provenance.
fn keep(p: *u8): *u8 {
return p
}
fn main(): void {
var v: Vec<u8> = Vec.new()
v.push(1 as u8)
let q = keep(v.ptr())
v.push(2 as u8)
print(q as i64 != 0)
}tests/errors/ptrReturnedParamIsView.milo
'Vec' cannot be instantiated with 'void'
Vec is a builtin tag rather than a monomorphized struct, so it reached codegen with a void element and emitted getelementptr void. An empty Vec<void> was worse: it compiled and ran, because the element type only materializes on the first push.
pub fn nothing(): void {
}
pub fn main(): i32 {
var v: Vec<void> = Vec.new()
v.push(nothing())
print(v.len())
return 0
}tests/errors/vecVoidElement.milo
'w' may reallocate here while 'p' still points into its buffer (from 'v.ptr()' on line 9)
Moving the Vec keeps its buffer in place, so the pointer's obligation moves with it.
from "std/os" import {
strlen
}
pub fn main(): i32 {
var v: Vec<u8> = Vec.new()
let p = v.ptr()
var w = v
w.push(0)
print(strlen(p))
return 0
}tests/errors/ptrOutlivesMovedSource.milo
'writer' writes the global 'g' while 'p' still points into 'g's buffer (from 'g.ptr()' on line 15)
A bound *T into a global is an element view like a slice binding: a callee that pushes to the global frees the buffer it points into. The main pass cannot see a write made inside another function; the global walk has the write summary and the view list.
from "std/os" import {
strlen
}
var g: Vec<u8> = []
fn writer() {
g.push(65)
}
pub fn main(): i32 {
let p = g.ptr()
writer()
print(strlen(p))
return 0
}tests/errors/ptrGlobalCalleePush.milo
'x' shadows an outer binding
if let pattern bindings route through the same declare() path as match arms; a binding that shadows an outer local must point at the if let pattern's own binding.
enum Opt2 { Some2(i32), None2 }
fn main(): i32 {
let x = 10
let o = Opt2.Some2(5)
if let Opt2.Some2(x) = o {
return x
} else {
return 0
}
}tests/errors/shadowIfLetBinding.milo
let ... else bindings escape into the CURRENT scope (unlike if let) and route through the same declare() path — nested one level inside an if so the outer x (the function's own parameter) lives in an enclosing scope, this must hit the shadowing branch, not the same-scope redeclaration branch, and still point at the pattern's own binding site.
enum Opt3 { Some3(i32), None3 }
fn getVal(x: i32, o: Opt3): i32 {
if x > 0 {
let Opt3.Some3(x) = o else {
return 0
}
return x
}
return 0
}
fn main(): i32 {
return getVal(1, Opt3.Some3(4))
}tests/errors/shadowLetElseBinding.milo
while let desugars to while true { if let P = subj { body } else { break } } (see parseWhile in src/parser.ts), so it routes through the same IfLetStmt path and must point at the pattern's own binding, not print bare.
enum Opt4 { Some4(i32), None4 }
fn main(): i32 {
let x = 1
var o = Opt4.Some4(3)
while let Opt4.Some4(x) = o {
o = Opt4.None4
return x
}
return 0
}tests/errors/shadowWhileLetBinding.milo
'yieldNow' can park this task while the loop variable is a reference into 'g's buffer
@parks is a declared marker, not the mechanism. The summary walks the call graph down to the runtime leaves, so a std-style wrapper that forgets the attribute is still seen as parking: this yieldNow has no @parks and calls schedulerYield two levels down through helpers that have none either.
from "std/runtime" import {
schedulerYield
}
var g: Vec<i64> = []
fn yieldInner(): void {
schedulerYield()
}
pub fn yieldNow(): void {
yieldInner()
}
pub fn main(): i32 {
g.push(1)
for x in g {
yieldNow()
print(x)
}
return 0
}tests/errors/globalForInParkUnannotatedWrapper.milo
(via 'work' → 'record')
The global is touched two frames below the closure, so a check that only looked at the closure body would miss it. The diagnostic names the call chain that reached it.
from "std/runtime" import {
Promise
}
var hits: i64 = 0
fn record(): void {
hits = hits + 1
}
fn work(): void {
record()
}
pub fn main(): i32 {
let p = Promise<i64>.blocking(move(): i64 => {
work()
return 0
}
)
p.await()!
return 0
}tests/errors/threadGlobalRaceTransitive.milo
@cName on 'Point.kind': only an 'extern struct' field has a C name
A Milo struct has no C counterpart, so a C name on its field would be read by nothing.
struct Point {
@cName("type") kind: i32,
}
fn main() {
let p = Point { kind: 1 }
print(p.kind)
}tests/errors/cNameNotExtern.milo
a declaration does not match the C header it claims to describe
Naming a field C doesn't have makes offsetof itself ill-formed. Apple Clang and GCC phrase the underlying error differently, so assert Milo's stable wrapper.
@cLayout("struct timespec", "time.h")
extern struct T {
tv_sec: i64,
bogusField: i64,
}
fn main() {
print(sizeOf<T>())
}tests/errors/cLayoutNoSuchField.milo
@cLayout("struct nosuchtype", "time.h")
extern struct T {
a: i64,
}
fn main() {
print(sizeOf<T>())
}tests/errors/cLayoutNoSuchType.milo
a JSON object's keys are strings
A JSON object's keys are strings and nothing else, so only HashMap<string, V> has an encoding. Stringifying an i64 key would round-trip 1 and "1" to the same place.
@derive(Json)
struct Bad {
m: HashMap<i64, string>,
}
fn main() {
let b = Bad { m: HashMap.new() }
print(b.toJson())
}tests/errors/deriveJsonIntKeyedMap.milo
a nested fixed array
MiloType carries ONE isArray flag, so an array of an array has nowhere to record the inner one. The parser built the type from the inner type's NAME alone, which silently dropped it: this annotation was accepted and meant [i64; 2]. A flat literal satisfied it, g[1][0] reported "cannot index type i64" against a program that had written a 2D array, and a nested literal reached clang as a store of [2 x i64] into an i64 slot. An annotation the language cannot represent now says so.
fn main() {
var g: [[i64; 2]; 2] = [1, 2]
print(g[0])
}tests/errors/nestedFixedArray.milo
ambiguous From conversion
enum IoError {
NotFound(string),
}
enum AppError {
Io(IoError),
AlsoIo(IoError),
}
fn readFile(path: string): Result<string, IoError> {
return Result.Ok(path)
}
fn process(path: string): Result<string, AppError> {
let s = readFile(path)?
return Result.Ok(s)
}
fn main(): i32 {
return 0
}tests/errors/resultAmbiguousFrom.milo
an array of references is not expressible
Same dropped-modifier bug as nestedFixedArray, different modifier: the parser kept only the inner type's name, so [&string; 2] silently became [string; 2] — an OWNED array. References are second-class and cannot be stored, so an array of them has no representation at all, and accepting the annotation promised one.
fn main() {
var a: [&string; 2] = ["x", "y"]
print(a[0])
}an interface value has no clone
A Vec<Interface> holds interface values, whose itables carry no clone slot — there is no way to duplicate the erased concrete value.
interface Shape {
fn area(self: &Self): i64
}
struct Square {
side: i64,
}
impl Square {
fn area(self: &Self): i64 {
return self.side * self.side
}
}
fn main() {
let shapes: Vec<Shape> = [Square { side: 2 }]
let copied = shapes.clone()
print($"{copied.len()}")
}tests/errors/vecCloneInterface.milo
and is passed a reference into 'G' here
No loop at all: the argument borrows G's storage and the callee reallocs G while that borrow is live, so the reference dangles before the callee is done reading it.
var G: Vec<i64> = Vec.new()
fn consume(r: &i64): void {
G.push(7)
print(r)
}
pub fn main(): i32 {
G.push(1)
consume(G[0])
return 0
}tests/errors/globalBorrowArgRealloc.milo
argument 'p' is passed to a '&mut' parameter without '&mut'
A non-receiver argument bound to a &mut parameter is written &mut x; the bare form is an error whose hint prints the fixed call and names the fixer.
struct P { n: i64 }
fn bump(p: &mut P, by: i64): void { p.n = p.n + by }
fn main(): i32 {
var p = P { n: 1 }
bump(p, 2)
return p.n as i32
}tests/errors/implicitMutBorrow.milo
argument 1 of 'loadUser': expected Id<UserTag>, got Id<OrderTag>
A phantom brand exists so that this line is rejected, which makes this the error a reader meets exactly when they are doing the right thing. It used to be spelled expected Id_UserTag, got Id_OrderTag: the monomorphized instance name, which is an implementation detail nobody wrote (backlog Tier 1 #33). Diagnostics format a type back to the source spelling; the mangled name stays internal to lookups.
struct UserTag {
}
struct OrderTag {
}
struct Id<Tag> {
n: i64,
}
fn loadUser(id: Id<UserTag>): i64 {
return id.n
}
pub fn main(): i32 {
let orderId: Id<OrderTag> = Id {
n: 7
}
print(loadUser(orderId))
return 0
}tests/errors/brandMismatchNamesWrittenType.milo
array element has type string, but the array is declared [i64; 2]
Both array-literal paths called checkExprWithHint(elem, hint.element) and threw the answer away, so nothing ever compared them. var x: [i64; 2] = ["a", "b"] type-checked, and the mismatch surfaced as an LLVM error — '%t.3' defined with type '%String' but expected 'i64' — which is a type error escaping to clang and reported in a language the user does not write. Vec<i64> = ["a"] had the identical shape. Found by tests/typeAnnotationFidelity.test.ts, which asks whether a value of the WRONG shape can satisfy an annotation.
fn main() {
var x: [i64; 2] = ["a", "b"]
print(x[0])
}tests/errors/arrayLiteralElementType.milo
assert() condition must be bool, got i64
fn main(): i32 {
assert(42)
return 0
}tests/errors/assertBadArg.milo
assert() expects 1-2 arguments, got 3
fn main(): i32 {
assert(true, "msg", "extra")
return 0
}tests/errors/assertTooManyArgs.milo
because 'v' is being iterated
v.push(x) inside the loop was rejected and v[0] = x was not — one rule, two answers depending on how the mutation was spelled. The assignment path exempts any index-qualified target because an in-place element write never reallocates, so a live slice VIEW stays valid and simply observes it. That reasoning does not extend to a loop: the loop is handing out that element, so rewriting it changes what the current binding names. Fixed by recording WHY a variable is frozen (view vs iteration) and applying the exemption only to views. Not a use-after-free — the slot does not move, so the loop reads the new value rather than freed memory. It is an iterator-invalidation semantics bug, and it is here because the inconsistency is what lets the next one through.
fn main() {
var v: Vec<string> = Vec.new()
v.push("a")
v.push("b")
for it in v {
v[0] = "clobbered"
print(it)
}
}tests/errors/forInElementAssign.milo
borrowed mutably and shared in the same call
Passing a variable as both a mutable borrow and the source of a shared borrow in one call is rejected: a mutation through &var could invalidate the & arg (e.g. push reallocates the Vec, leaving &v[0] dangling).
fn grow(a: &mut Vec<i64>, b: &i64) {
a.push(b)
}
fn main(): i32 {
var v: Vec<i64> = [7]
grow(&mut v, v[0])
return 0
}
// @error: borrowed mutably and shared in the same calltests/errors/callSiteExclusivity.milo
borrowed mutably twice
Passing a Vec element and the Vec itself as two &mut args aliases the element into the container; a push inside reallocs and frees the element's storage — a use-after-free. The checker must reject the overlapping mutable borrows.
fn bad(elem: &mut i32, v: &mut Vec<i32>): i32 {
v.push(1)
return elem
}
fn main(): i32 {
var v: Vec<i32> = Vec.new()
v.push(111)
return bad(&mut v[0], &mut v)
}tests/errors/aliasMutContainerElement.milo
C header disagrees
A wrong field width is the silent-corruption case @cLayout exists to catch: tv_sec as i32 still leaves tv_nsec at offset 8 (i64 alignment pads it), so every offset matches and only the field's own size gives the drift away.
@cLayout("struct timespec", "time.h")
extern struct Timespec {
tv_sec: i32,
tv_nsec: i64,
}
fn main() {
print(sizeOf<Timespec>())
}tests/errors/cLayoutMismatch.milo
callback parameter 1 is declared
Every Vec combinator built a cbHint describing what it would pass, handed it to checkExprWithHint, and at best asked whether the answer was a function at all. Nobody compared the PARAMETERS, so a closure could declare any type it liked and be handed something else. v.each((x: &string) => print(x.len)) over a Vec<i64> was accepted and printed a pointer value; v.map((s: &string) => s.len) read the next element's bytes, and with a smaller allocation would read past the end. Both spellings the language does support stay legal: the hint's own type (&i64) and its by-value form (i64), which is how a Copy element is idiomatically taken.
fn main() {
var v: Vec<i64> = Vec.new()
v.push(3)
v.each((x: &string) => print(x.len))
}tests/errors/callbackParamType.milo
calling 'adopt' requires an unsafe block
@unsafe puts the proof obligation on the caller, and here the obligation is the sharpest one in the module: that this pointer came from a Milo allocation of this type and has not been adopted before. Every operation in the body is individually checkable and adopt(p) on a stack address, a C string or a pointer already adopted is still a lie the compiler cannot see.
from "std/foreign" import {
adopt
}
from "std/os" import {
malloc
}
pub fn main(): i32 {
var p = 0 as *i64
unsafe {
p = malloc(8) as *i64
}
let box = adopt(p)
print(box.isSome())
return 0
}tests/errors/adoptNeedsUnsafe.milo
calling 'withRaw' requires an unsafe block
The '@unsafe' attribute puts the proof obligation on the caller. Without it withRaw(v.ptr(), 1000000, f) would be safe-looking Milo: every operation in the body is individually checkable and the length is still a lie.
from "std/foreign" import { withRaw }
from "std/os" import { malloc }
pub fn main(): i32 {
var p = 0 as *i64
unsafe {
p = malloc(8) as *i64
}
let n = withRaw(p, 1, (xs: &[i64]): i64 => xs.len)
print(n!)
return 0
}tests/errors/withRawNeedsUnsafe.milo
calling a C function pointer requires 'unsafe' block
The pointer may be null, may have been written by C after the struct was built, and carries no proof its signature is the declared one. That is what unsafe marks — exactly the situation calling the same thing from C is in.
extern struct Ops {
read: (*u8, i32) => i32,
}
fn bump(_p: *u8, n: i32): i32 {
return n + 1
}
fn main() {
let ops = Ops {
read: bump,
}
print(ops.read(0 as *u8, 41))
}tests/errors/externFnPtrCallNeedsUnsafe.milo
can only be used as a pointer
extern type Opaque
fn main(): i32 {
var x: Opaque = 0
return 0
}
// @error: can only be used as a pointertests/errors/externTypeByValue.milo
can't appear inside an expression
A trailing/separating ';' is a tolerated no-op (Milo is newline-delimited), but a ';' inside an expression is still a parse error. Kept inside parens so the file does not parse — the formatter can't silently strip the ';'.
fn main(): i32 {
let x = (1 ; + 2)
return 0
}tests/errors/semicolonSeparator.milo
cannot assign to 'h.data' because 'h' is borrowed
Assigning a whole field frees its buffer; a live view into it would dangle. The borrow check used to fire only when the assignment target was a bare identifier, so h.data = ... slipped through and w[0] read freed memory.
struct Holder { data: Vec<i64> }
fn main(): i32 {
var h = Holder { data: Vec.new() }
h.data.push(10)
h.data.push(20)
h.data.push(30)
let w = h.data[0..2]
h.data = Vec.new()
print(w[0])
return 0
}
// @error: cannot assign to 'h.data' because 'h' is borrowedtests/errors/fieldAssignUnderView.milo
cannot assign to 'lb.buf' because 'lb' is borrowed
A string slice off a *field* has to freeze the root, the way the array/vec slice path already did. It only froze expr.object when that was an identifier, so slicing lb.buf recorded no borrow at all and the reassignment below freed the bytes w points into. This is ripgrep's LineBuffer shape: a match view held across a fill that rolls the buffer.
struct LineBuf { buf: string }
fn main(): i32 {
var lb = LineBuf { buf: "aaaa.needle.bbbb" }
let w = lb.buf[5..11]
lb.buf = lb.buf + "...more"
print(w)
return 0
}
// @error: cannot assign to 'lb.buf' because 'lb' is borrowedtests/errors/stringFieldSliceEscape.milo
cannot assign to 'n' because it is borrowed
While a payload view from a match arm is alive, the subject it points into cannot be reassigned: n = Node.Empty would drop the string s still views. This held a use-after-free for & views too (ASan: global-buffer-overflow on the print).
enum Node { Pair(string, i64), Empty }
fn clobber(n: &mut Node): void {
match n {
Node.Pair(s, k) => {
n = Node.Empty
print(s)
}
Node.Empty => {}
}
}
fn main(): i32 {
var x = Node.Pair("a", 1)
clobber(&mut x)
return 0
}tests/errors/matchMutSubjectFrozen.milo
cannot assign to 's' because it is borrowed
calling .slice() borrows the source — reassignment is rejected
fn main(): i32 {
var s = "hello world"
let view = s.slice(0, 5)
s = "goodbye"
print(view)
return 0
}
// @error: cannot assign to 's' because it is borrowedtests/errors/borrowSliceMethod.milo
reassigning a var while a slice borrows it is a compile error
fn makeString(): string {
var s: string = ""
for i in 0..50 {
s = s + "A"
}
return s
}
fn main(): i32 {
var s = makeString()
let slice = s[0..5]
s = makeString()
print(slice)
return 0
}
// @error: cannot assign to 's' because it is borrowedtests/errors/borrowSliceReassign.milo
cannot assign to immutable variable 'p.x'
A ?&T unwraps to a shared &T. After the unwrap it is an ordinary second-class reference and every existing rule applies unchanged — there is no new reference kind here to write a new rule for.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn readX(b: ?&Bump): i32 {
let p = b else {
return -1
}
p.x = 3
return p.x
}
fn main() {
print(1)
}tests/errors/nullableRefSharedWrite.milo
cannot assign to immutable variable 'STORE.n'
The EnumLit-to-field fallback on the assignment path must not smuggle a let global past the mutability check — it only fixes *which* target this is, not whether the target is writable.
struct Store {
n: i32,
}
let STORE: Store = Store {
n: 0
}
fn main(): void {
STORE.n = 1
}tests/errors/globalLetStructAssign.milo
cannot assign to immutable variable 'x'
fn main(): i32 {
let x: i32 = 5
x = 10
return 0
}cannot be instantiated with 'void'
void has no runtime representation, so a generic instantiated with it emitted load void and void %param, both rejected by LLVM at the link step, against a temp .ll file with no source location. Fails here instead, where there is a span.
from "std/runtime" import {
Promise
}
pub fn main(): i32 {
let p = Promise<void>.blocking(move(): void => {
print("in thread")
}
)
p.await()!
return 0
}tests/errors/genericVoidArg.milo
The void guard used to live on the annotation path only, so a type argument the checker INFERRED slipped past it: identity(nothing()) monomorphized at void and emitted call void @identity_void(void void), which LLVM rejects at the link step against a temp .ll file with no source location. The guard now sits in monomorphizeFn, where every instantiation passes, spelled or inferred.
pub fn identity<T>(x: T): T {
return x
}
pub fn nothing(): void {
}
pub fn main(): i32 {
identity(nothing())
return 0
}tests/errors/genericVoidArgInferred.milo
cannot be passed in a variadic position
extern struct Pt {
x: i32,
y: i32,
}
extern fn logf(fmt: *u8,...): i32
fn main(): i32 {
let p = Pt {
x: 1, y: 2
}
unsafe {
logf("pt", p)
}
return 0
}tests/errors/externStructVariadicArg.milo
cannot call 'add' on 'b' because it is borrowed
A method-returned view borrows the receiver's storage, so growing the receiver while the view is live must be rejected: push reallocs and frees the buffer the view points into. This compiled once and segfaulted on glibc (silent garbage on macOS).
struct Buf { data: Vec<i64> }
impl Buf {
fn add(self: &mut Self, x: i64): void { self.data.push(x) }
fn view(self: &Self): &[i64] { return self.data[0..self.data.len] }
}
fn main(): i32 {
var b = Buf { data: Vec.new() }
b.add(1)
let s = b.view()
b.add(2)
print(s[0])
return 0
}
// @error: cannot call 'add' on 'b' because it is borrowedtests/errors/viewMutateReceiver.milo
cannot call 'feed' on 'lx' because it is borrowed
holding a returned &string view freezes its receiver: growing the buffer would realloc the bytes the view points into
struct Lexer { src: string }
impl Lexer {
fn word(self: &Self, a: i64, b: i64): &string { return self.src[a..b] }
fn feed(self: &mut Self, more: &string): void { self.src = self.src + more }
}
fn main(): i32 {
var lx = Lexer { src: "alpha beta" }
let w = lx.word(0, 5)
lx.feed("!!")
print(w)
return 0
}
// @error: cannot call 'feed' on 'lx' because it is borrowedtests/errors/stringViewFreezesReceiver.milo
cannot call 'push' on 'b' because it is borrowed
for it in b.items holds a pointer into that Vec's buffer for the loop's life, so pushing to it reallocates the memory the loop is reading. This was accepted until 2026-08-16 and was a heap-use-after-free in safe code (ASan, zero unsafe): the for-in freeze only fired when the iterable was a bare IDENT, so for x in v was caught and for x in b.items was not.
struct Bag { items: Vec<string> }
fn main() {
var b = Bag { items: Vec.new() }
b.items.push("alpha")
for it in b.items {
b.items.push("realloc")
print(it)
}
}tests/errors/forInFieldMutate.milo
cannot call 'push' on 'items' because it is borrowed
fn main(): i32 {
var items: Vec<i64> = Vec.new()
items.push(1)
items.each((x: &i64) => {
items.push(1)
}
)
return 0
}tests/errors/eachMutateReceiver.milo
fn main(): i32 {
var items: Vec<i64> = Vec.new()
items.push(1)
for x in items {
items.push(x + 1)
}
return 0
}tests/errors/vecPushWhileIterating.milo
cannot call 'push' on 's' because it is borrowed
fn main(): i32 {
var s = "hello".clone()
let sl = s[0..3]
s.push(65 as u8)
print(sl)
return 0
}tests/errors/stringPushWhileSliced.milo
cannot call 'push' on 't' because it is borrowed
Appending can reallocate the buffer every piece points into, so the receiver stays frozen for the whole loop — the same rule a s[a..b] slice already carries.
pub fn main(): i32 {
var t: string = "a\nb"
for line in t.lines() {
t.push(67)
print(line)
}
return 0
}
// @error: cannot call 'push' on 't' because it is borrowedtests/errors/stringViewIterMutate.milo
the precise freeze still catches a write to the viewed field: pushing to a can realloc the storage the returned view points into
struct Two { a: Vec<i64>, b: Vec<i64> }
impl Two {
fn items(self: &Self): &[i64] { return self.a[0..self.a.len] }
}
fn main(): i32 {
var t = Two { a: Vec.new(), b: Vec.new() }
t.a.push(1)
let v = t.items()
t.a.push(2)
print(v[0])
return 0
}
// @error: cannot call 'push' on 't' because it is borrowedtests/errors/viewSameFieldFrozen.milo
cannot capture 's' in a closure
a closure stores its captures and can outlive the frame that owns the Vec, so a view captured by one dangles — this returned garbage from freed memory before the check
fn makeGetter(): () => i64 {
var v: Vec<i64> = Vec.new()
v.push(111)
let s = v[0..1]
return move (): i64 => { return s[0] }
}
fn main(): i32 {
let g = makeGetter()
print(g())
return 0
}
// @error: cannot capture 's' in a closuretests/errors/viewCaptureClosure.milo
cannot carry a payload
A repr'd (C-like) enum is integer-valued; its variants can't hold data.
enum Bad: i32 { A, B(i64) }
pub fn main(): i32 { return 0 }tests/errors/enumReprPayload.milo
cannot cast [u8; 4] to i64: only to a pointer
A fixed array casts to a pointer, never straight to an integer: codegen emitted add [16 x i8] for the arithmetic that followed and clang rejected the module.
fn main(): i32 {
var a: [u8; 4] = [0; 4]
let n = (a as i64) + 1
print(n)
return 0
}tests/errors/castArrayToInt.milo
cannot clear an immutable Vec
fn main(): i32 {
let v: Vec<i64> = [1, 2, 3]
v.clear()
return 0
}tests/errors/vecClearImmutable.milo
cannot cross the C ABI
enum Color {
Red,
Green,
Blue,
}
extern fn currentColor(): Color
fn main(): i32 {
return 0
}tests/errors/externEnumRet.milo
cannot derive 'Describe'
A @derive naming nothing — no built-in and no template in scope. The diagnostic lists what IS available, because a user-defined derive has no other reference: it is not in the compiler, so there is nothing to grep.
@derive(Describe)
struct Point {
x: i64,
}
fn main() {
let p = Point { x: 1 }
print(p.x)
}tests/errors/deriveTemplateUnknown.milo
cannot derive Clone for 'E': variant 'F' payload 0
A closure payload cannot be cloned: an owning closure's environment has no duplicate. The explicit derive names the variant and payload that block it instead of synthesizing a clone() whose body would not compile.
@derive(Clone)
enum E {
Plain(i32),
F((i32) => i32),
}
fn main() {
let e = E.Plain(1)
let _c = e.clone()
}tests/errors/cloneDeriveEnumClosurePayload.milo
cannot derive Clone for 'Fd': it implements Drop
A Drop type must never get clone(): the clone and the original both run drop, and the resource is released twice. The explicit derive names the reason instead of synthesizing the hazard.
@derive(Clone)
pub struct Fd { fd: i32 }
impl Drop for Fd {
fn drop(self: &mut Self) {
print("close")
}
}
pub fn main(): i32 {
return 0
}tests/errors/deriveCloneDrop.milo
cannot derive Clone for 'Handle': it holds a raw pointer ('p') and is not @copy
A pointer-holding struct without @copy gets no automatic clone(), and an explicit @derive(Clone) is refused: a clone would be a second owner of what the pointer addresses, the very copy the move tracking exists to prevent.
@derive(Clone)
struct Handle {
p: *u8,
}
fn main() {
let h = Handle { p: 0 as *u8 }
let h2 = h.clone()
print(h2.p as i64)
}tests/errors/deriveClonePointerField.milo
cannot derive Clone for 'Handle': it implements Drop
An enum with a Drop impl must never get clone(): both the clone and the original run drop, so the resource is released twice. Same message the struct derive gives.
@derive(Clone)
enum Handle {
Closed,
Open(i32),
}
impl Drop for Handle {
fn drop(self: &mut Self) {
print("close")
}
}
fn main() {
let h = Handle.Open(3)
let _c = h.clone()
}tests/errors/cloneDeriveEnumDrop.milo
cannot derive Clone for 'Q': field 'f' of type 'Option<move (i32) => i32>' has no clone
Option<closure> is a legal type with no clone(): an owning closure's environment has no duplicate, so the instance gets no conditional Clone. A struct holding one is silently non-clonable (Plain below compiles); asking for the derive names the field.
struct Plain {
f: Option<move (i32) => i32>,
}
@derive(Clone)
struct Q {
f: Option<move (i32) => i32>,
}
fn main() {
let n: i32 = 2
let p = Plain { f: Option.Some(move (x: i32) => x + n) }
let q = Q { f: Option.None }
let _c = q.clone()
match p.f {
Option.Some(_g) => { print("some") }
Option.None => { print("none") }
}
}tests/errors/cloneDeriveOptionClosure.milo
cannot derive Clone for 'Q': field 'h' of type 'H' has no clone
An explicit derive still reports the field that blocks it after the auto fixpoint has run: a @noCopy struct is never auto-clonable.
@noCopy
struct H { id: u32 }
@derive(Clone)
struct Q { h: H }
fn main() {
let q = Q { h: H { id: 1 } }
let _r = q.clone()
}
// @error: cannot derive Clone for 'Q': field 'h' of type 'H' has no clonetests/errors/cloneDeriveExplicitNoCopyField.milo
cannot have type 'void'
A local of type void asks for a storage slot void cannot have: alloca void. void is a function return type; Unit is the value that carries no data.
pub fn nothing(): void {
}
pub fn main(): i32 {
let x: void = nothing()
print("unreachable")
return 0
}tests/errors/voidLocalBinding.milo
cannot infer 'R' for 'Holder.make'
A method type parameter nothing fixes. R appears only in the return type, no argument mentions it, and the call's result is discarded — so there is no concrete type to instantiate with. Rejecting names what would have to change; the alternative is picking a type the program never asked for.
struct Holder {
n: i64,
}
impl Holder {
fn make<R>(self: &Self): R {
return zeroed<R>()
}
}
fn main() {
let h = Holder { n: 1 }
h.make()
print(h.n)
}tests/errors/genericMethodUninferable.milo
cannot infer 'T' for 'Mk.empty'
A static method's own type parameter is inferred from the arguments, then from the expected type (let v: Vec<i64> = Mk.empty() compiles). With neither there is nothing to instantiate, and the error names the parameter rather than claiming the method is missing.
struct Mk {}
impl Mk {
fn empty<T>(): Vec<T> {
let v: Vec<T> = Vec.new()
return v
}
}
pub fn main(): i32 {
let v = Mk.empty()
print(v.len())
return 0
}tests/errors/genericStaticMethodNoInfer.milo
cannot infer the type arguments of 'Arena' for 'Arena.new(...)'
Arena.new() takes no arguments and nothing here says what the arena holds, so there is nothing to infer T from. The method exists; the error says what is missing and the hint gives both fixes (Arena<T>.new(), or an annotated binding). It used to report "type 'Arena' has no static method 'new'", which sent readers looking for a method that is right there in the impl.
from "std/arena" import { Arena }
pub fn main(): i32 {
let a = Arena.new()
print(a.len())
return 0
}tests/errors/genericStaticNoTypeArgs.milo
cannot infer type for parameter 'x'
fn main() {
let f = (x) => x + 1
}tests/errors/closureInferNoHint.milo
cannot infer type parameter(s) 'T' for Option.None
fn main(): i32 {
let value: i32 = null
return value
}tests/errors/nullNonOptional.milo
cannot infer Vec element type
An unannotated Vec.new() that is never pushed to has no element to infer.
fn main() {
var xs = Vec.new() // @error: cannot infer Vec element type
print(xs.len.toString())
}tests/errors/vec_infer_no_push.milo
cannot insert into immutable HashMap
fn main(): i32 {
let m: HashMap<i32, i32> = HashMap.new()
m.insert(1, 2)
return 0
}tests/errors/hashmapImmutableInsert.milo
cannot move '{ … }.name' out of 'R', which implements Drop
Destructuring takes fields out by value, so it follows the move rules: a struct that implements Drop cannot be taken apart, by pattern any more than by hand.
struct R { fd: i32, name: string }
impl Drop for R { fn drop(self: &mut Self): void { print("close") } }
fn f(): R { return R { fd: 1, name: "x" } }
fn main(): i32 {
let { name } = f()
print(name)
return 0
}tests/errors/destructureDropStruct.milo
cannot move 'b' because it is borrowed
moving the receiver away while its view is live drops the storage the view points into
struct Buf { data: Vec<i64> }
impl Buf {
fn view(self: &Self): &[i64] { return self.data[0..self.data.len] }
}
fn consume(x: Buf): void { print(x.data.len) }
fn main(): i32 {
var b = Buf { data: Vec.new() }
b.data.push(1)
let s = b.view()
consume(b)
print(s[0])
return 0
}
// @error: cannot move 'b' because it is borrowedtests/errors/viewMoveReceiver.milo
cannot move 'd.a' out of the borrowed 'd'
?? forks the same way: the result is either the payload — which lives in the borrowed value's storage — or the default.
struct Doc { a: string }
fn pick(d: &Doc, o: Option<string>): string {
return o ?? d.a
}
fn main() {
var d = Doc { a: "aaa" + "" }
print(pick(d, Option.None))
}tests/errors/moveFieldOutOfBorrowDefaultValue.milo
A fork owns nothing itself — it evaluates to one of its tails, so consuming it consumes whichever tail ran. return d.a was always rejected; spelled through an if-expression it used to compile and double-free at runtime.
struct Doc { a: string, b: string }
fn pick(d: &Doc, c: bool): string {
return if c { d.a } else { d.b }
}
fn main() {
var d = Doc { a: "aaa" + "", b: "bbb" + "" }
print(pick(d, true))
}tests/errors/moveFieldOutOfBorrowIfExpr.milo
Same rule as the if-expression form: every arm tail is a candidate result and each one has to answer the move-out-of-borrow question.
struct Doc { a: string, b: string }
fn pick(d: &Doc, n: i64): string {
return match n { 0 => d.a, _ => d.b }
}
fn main() {
var d = Doc { a: "aaa" + "", b: "bbb" + "" }
print(pick(d, 0))
}tests/errors/moveFieldOutOfBorrowMatchExpr.milo
cannot move 'd.text' out of the borrowed 'd'
Returning a non-Copy field out of a &T used to compile, and produced a String that aliased a pointee the caller was about to drop: the value printed as freed bytes and then double-freed. A whole &T binding was already rejected here; the field read is the same hazard one level down.
struct Doc {
text: string,
}
fn describe(d: &Doc): string {
return d.text
}
fn main() {
let doc = Doc {
text: "hi",
}
print(describe(doc))
}tests/errors/moveFieldOutOfBorrow.milo
cannot move 'needle' out of a loop
A move closure built inside a loop takes its captures with it, so the second iteration has nothing left to capture. That was already rejected when the body USED the capture directly (needle.len), and silently accepted when the body passed it to a function instead — one operation, two spellings, two answers. Accepted, it did not merely compile: every iteration after the first captured an empty string and the program printed 4, 0, 0. A wrong answer with no diagnostic is the worst outcome available, which is what makes this an error fixture rather than a lint.
from "std/runtime" import {
Promise
}
fn measure(s: string): i64 {
return s.len
}
pub fn main(): i32 {
let needle: string = "milo"
var ps: Vec<Promise<i64>> = Vec.new()
var i: i64 = 0
while i < 3 {
ps.push(Promise<i64>.blocking(move (): i64 => {
return measure(needle)
}))
i = i + 1
}
let parts = Promise.all(ps).await()!
for p in parts {
print(p.toString())
}
return 0
}tests/errors/closureMoveInLoopViaCall.milo
cannot move 'r.name' out of the borrowed 'r'
A non-Copy field of a borrowed struct cannot be moved out by a pattern either; the rule and the message are the field read's.
struct R { fd: i32, name: string }
fn g(r: &R): void {
let { name } = r
print(name)
}
fn main(): i32 {
let r = R { fd: 1, name: "x" }
g(r)
return 0
}tests/errors/destructureBorrowed.milo
cannot move 's' out of a loop
fn consume(s: string): void {
}
fn main(): i32 {
let s: string = "hello"
var i: i32 = 0
while i < 3 {
consume(s)
i = i + 1
}
return 0
}cannot move 'self.inner.s' out of the borrowed 'self'
The same hazard through a field chain and through self: with only the one-level check, self.inner.s slipped past and the callee zeroed the caller's field, so the caller read an empty string afterwards.
struct Inner {
s: string,
}
struct Outer {
inner: Inner,
}
impl Outer {
fn take(self: &Self): string {
return self.inner.s
}
}
fn main() {
let o = Outer {
inner: Inner {
s: "hi",
},
}
print(o.take())
}tests/errors/nestedFieldMoveOutOfBorrow.milo
cannot move 'toks[...].e' out of 'toks': the element stays in the container, so the move would leave a zeroed 'e' behind
The enum-payload shape of moveFieldOutOfIndexedElement.milo. This one was the worst of the class: the zeroing emptied the payload but the tag survived, so a later match toks[0].e still entered the A arm and read an empty string out of it.
enum E {
A(string),
B,
}
struct Tok {
e: E,
}
fn main(): i32 {
var toks: Vec<Tok> = Vec.new()
toks.push(Tok {
e: E.A("payload"),
})
let taken = toks[0].e
match toks[0].e {
E.A(s) => {
print(s)
}
E.B => {
print("b")
}
}
return 0
}tests/errors/moveEnumFieldOutOfIndexedElement.milo
cannot move 'toks[...].text' out of 'toks': the element stays in the container, so the move would leave a zeroed 'text' behind
Moving a non-Copy field out of a container element used to compile and zero the slot while the container kept the element: print(toks[0].text) afterwards printed "" with no diagnostic. The element never changed hands, so it is an error now; the hint names the three ways out (.clone(), remove/pop, replace).
struct Tok {
text: string,
n: i64,
}
fn main(): i32 {
var toks: Vec<Tok> = Vec.new()
toks.push(Tok {
text: "hi", n: 1,
})
let name = toks[0].text
print(name)
print(toks[0].text)
return 0
}tests/errors/moveFieldOutOfIndexedElement.milo
cannot move 'u.name' out of the borrowed 'u'
v.sortByKey((u: &User) => u.name) compiles; this identical closure handed to a user's own non-retaining combinator does not. That asymmetry looks like a missing generalization, and it is NOT — relaxing it is a use-after-free, proven under ASan (heap-use-after-free in pick, 2026-08-16). The reason is not retention. pick never stores the key: it compares it and lets it go. The problem is that letting it go IS a free — let a = key(v[i]) binds an owned string, so it carries ordinary scope-end drop glue, and the buffer it drops belongs to the User the closure only borrowed. sortByKey is exempt because a builtin is the one kind of callee that can promise never to drop the key it was handed. So the property that would close this is not "does the callee retain the result" but "can the callee hold the result without owning it" — which needs a borrowed return, and second-class references cannot return one. Don't wire a retention analysis to this rule; see backlog Tier 1 #6.
struct User { name: string }
fn pick(v: &Vec<User>, key: (&User) => string): i32 {
var best = 0
var i = 1
while i < v.len() {
let a = key(v[i])
let b = key(v[best])
if a < b { best = i }
i = i + 1
}
return best as i32
}
fn main() {
var v: Vec<User> = Vec.new()
v.push(User { name: "zed" })
v.push(User { name: "amy" })
let i = pick(v, (u: &User) => u.name)
print(i.toString())
}tests/errors/keyExtractorUserCombinator.milo
map RETAINS what its closure returns, so a closure handing back a field of its borrowed parameter built a Vec<string> aliasing the source elements' buffers, and both freed them: a live double-free abort (exit 133), not a hypothetical one. This is why the move-out-of-a-borrow exemption is keyed to sortByKey alone rather than to closures in general — sortByKey reads the key to compare it and never stores or drops it, and that property has to be proved per callee, not assumed. Write u.name.clone() here.
struct User {
name: string,
age: i32,
}
fn main() {
var users: Vec<User> = [User {
name: "Alice", age: 30,
}
,]
let names = users.map((u: &User) => u.name)
print(names[0])
}tests/errors/mapMoveFieldOutOfBorrow.milo
cannot move out of global
Passing a global to a by-value parameter moves it. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
fn take(s: string): i64 {
return s.len
}
fn f(): i64 {
return take(S)
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveArg.milo
An array literal element takes its value by move.
let S: string = "hello"
fn f(): i64 {
let a = [S]
return a[0].len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveArrayLit.milo
Assigning a global into a local moves it (assigning TO a var global stays legal). A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
var S: string = "hello"
fn f(): i64 {
var y = "x"
y = S
return y.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveAssign.milo
Globals are never captured; a move closure's body that moves one moves the global itself. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
fn f(): i64 {
let c = move () => {
let t = S
return t.len
}
return c()
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveClosure.milo
A global enum with an owned payload, moved into a local before matching. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
enum E {
A(string),
B,
}
let G: E = E.A("hello")
fn f(): i64 {
let x = G
match x {
E.A(s) => { return s.len }
E.B => { return 0 }
}
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveEnum.milo
Wrapping a global in Some(...) moves it into the payload. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
fn f(): i64 {
let o = Some(S)
return o!.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveEnumLit.milo
Moving a non-Copy field out of a global struct. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
struct P {
name: string,
age: i64,
}
let G: P = P { name: "bob", age: 3 }
fn f(): i64 {
let n = G.name
return n.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveField.milo
forget(S) consumes its argument; on a global it would leave the slot zeroed for every later reader.
let S: string = "hello"
fn f(): i64 {
forget(S)
return S.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveForget.milo
An if-expression consumes whichever tail ran, so a global tail is a move. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
fn f(): i64 {
let x = if S.len > 0 { S } else { "zz" }
return x.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveIfTail.milo
let x = S binds the global's value by move. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
fn f(): i64 {
let x = S
return x.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveLet.milo
Moving a field two levels into a global. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
struct In {
t: string,
}
struct Out {
i: In,
}
let G: Out = Out { i: In { t: "hello" } }
fn f(): i64 {
let t = G.i.t
return t.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveNestedField.milo
Pushing a global into a Vec moves it into the Vec. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
fn f(): i64 {
var v: Vec<string> = Vec.new()
v.push(S)
return v[0].len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMovePush.milo
return S hands the global's value to the caller. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
fn g(): string {
return S
}
fn f(): i64 {
return g().len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveReturn.milo
A by-value self: Self receiver consumes the global it is called on. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
struct P {
name: string,
}
impl P {
fn consume(self: Self): i64 {
return self.name.len
}
}
let G: P = P { name: "hello" }
fn f(): i64 {
return G.consume()
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveSelfMethod.milo
A global struct holding a string, moved whole. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
struct P {
name: string,
age: i64,
}
let G: P = P { name: "bob", age: 3 }
fn f(): i64 {
let x = G
return x.name.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveStruct.milo
A struct literal field takes its value by move. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: string = "hello"
struct D {
s: string,
}
fn f(): i64 {
let d = D { s: S }
return d.s.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveStructLit.milo
S! unwraps by moving the payload out of the Option. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
let S: Option<string> = Some("hello")
fn f(): i64 {
let x = S!
return x.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveUnwrap.milo
A var global is no more movable than a let one; only assignment to it is legal. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
var S: string = "hello"
fn f(): i64 {
let x = S
return x.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveVar.milo
A Vec global moved into a local. A global has no single owner, so moving out of it left the slot zeroed and the next reader (here, the second call of f) silently saw an empty value.
var V: Vec<i64> = [1, 2, 3]
fn f(): i64 {
let x = V
return x.len
}
pub fn main(): i32 {
print(f())
print(f())
return 0
}tests/errors/globalMoveVec.milo
cannot move out of global 'GE'
Matching an immutable global binds its payloads as borrows (tests/fixtures/ globalReadBorrow.milo). A var global stays a move, which is rejected: an arm may call a function that reassigns the global, and nothing tracks a payload borrow across that call, so borrowing would free the string s still points at. Clone to match.
enum E {
A(string),
B,
}
var GE: E = E.A("hello")
fn reset() {
GE = E.B
}
pub fn main(): i32 {
match GE {
E.A(s) => {
reset()
print(s)
}
E.B => {}
}
return 0
}tests/errors/globalMoveVarMatch.milo
cannot move the borrowed value out of
moving a non-Copy value out of a borrow would alias the owner's heap data (double-free); it must be rejected — clone to take ownership.
fn take(s: string) {
print(s)
}
fn f(s: &string) {
take(s)
}
fn main(): i32 {
return 0
}
// @error: cannot move the borrowed value out oftests/errors/moveBorrowOut.milo
cannot move the borrowed value out of 'line'
A piece is a &string: second-class, so it cannot leave the loop that borrowed it.
fn firstLine(s: &string): string {
for line in s.lines() {
return line
}
return ""
}
pub fn main(): i32 { print(firstLine("a\nb")) return 0 }
// @error: cannot move the borrowed value out of 'line'tests/errors/stringViewIterEscape.milo
cannot open
from "nonexistent.milo" import {
foo
}
fn main(): i32 {
return 0
}tests/errors/importMissing.milo
cannot open 'nonexistent.txt'
fn main(): i32 {
let x = @embedFile("nonexistent.txt")
return 0
}tests/errors/embedFileMissing.milo
cannot pass 'b' because it is borrowed
The same reallocation as forInFieldMutate, reached through a function that takes the struct as &mut instead of by touching the field directly. Freezing the iterable's ROOT is what catches both spellings; freezing only the field would leave this one.
struct Bag { items: Vec<string> }
fn grow(b: &mut Bag) {
b.items.push("realloc")
}
fn main() {
var b = Bag { items: Vec.new() }
b.items.push("alpha")
for it in b.items {
grow(&mut b)
print(it)
}
}tests/errors/forInFieldMutateThroughFn.milo
cannot pass 'items' because it is borrowed
fn grow(v: &mut Vec<i64>): void {
v.push(99)
}
fn main(): i32 {
var items: Vec<i64> = Vec.new()
items.push(1)
for x in items {
grow(&mut items)
}
return 0
}tests/errors/passMutWhileIterating.milo
cannot pass 's', a shared '&' reference, as a '&mut' argument
A '&S' parameter is read-only: forwarding it as '&mut' would let 'bump' write through a borrow the caller handed out as shared, mutating even a 'let' there.
struct S { x: i64 }
fn bump(s: &mut S) { s.x = s.x + 1 }
fn peek(s: &S): i64 {
bump(&mut s)
return s.x
}
fn main(): i32 {
let s = S { x: 1 }
return peek(s) as i32
}tests/errors/sharedRefToMutParam.milo
cannot pass a closure that captures 'n' by reference to 'Promise.blocking', which keeps it
Promise.blocking runs its closure on a real OS thread, so a borrowing closure hands that thread a raw pointer into a frame that is already gone. This parses as EnumLit, not Call/MethodCall, and checkEscapingClosures only had arms for the latter two: it compiled clean and printed Ok(8421921353) off a dead stack. A var capture is what reaches the check, since checkStaticCallArgs auto-promotes a closure whose captures are all immutable to move.
from "std/runtime" import { Promise }
fn mk(): Promise<i64> {
var n = 41
return Promise.blocking(() => {
n = n + 1
return n
})
}
fn main() {
let p = mk()
print(p.await())
}tests/errors/escapingClosurePromiseBlocking.milo
cannot pass a closure that captures 'n' by reference to 'Task.spawn', which keeps it
Same hole as escapingClosurePromiseBlocking, on the green-task side: the spawned task outlives mk's frame, so its capture of n dangles. Task.spawn declares its param move, but the declaration alone never rejected anything — the escape check has to reach the EnumLit call form for the error to fire.
from "std/runtime" import { Task }
fn mk(): Task {
var n = 41
return Task.spawn(() => { n = n + 1 })
}
fn main() {
let t = mk()
t.join()
}tests/errors/escapingClosureTaskSpawn.milo
cannot pass a closure that captures 'n' by reference to 'wrap', which keeps it
The same hole reached through a parameter. retainsParam decides "does the callee keep this argument?" by looking for the parameter as an Ident, and a CAPTURE is invisible to that: f(3) inside the closure body is a Call with a string callee and no Ident node at all. So wrap answered "not retained", the borrowing closure passed the call-site check, and the returned move closure carried a pointer into the dead frame.
fn wrap(f: (i64) => i64): move () => i64 {
return move (): i64 => f(3)
}
fn make(): move () => i64 {
let n = 5
let g = (x: i64) => x + n
return wrap(g)
}
fn main() {
let h = make()
print(h())
}tests/errors/escapingClosureCapturedParam.milo
Handing a borrowing closure to a function that KEEPS it. wrap stores its parameter in a struct it returns, so the closure outlives the frame its capture lives in — a live SIGKILL. Whether the callee keeps the argument is computed, not annotated: a fn-typed parameter that only ever appears in callee position is consumed during the call, and anything else counts as retained. each below only calls its parameter, so passing the same closure there is still accepted (tests/fixtures/closureNonRetainingArg.milo).
struct Box {
f: (i64) => i64,
}
fn wrap(g: (i64) => i64): Box {
return Box { f: g }
}
fn make(): Box {
let n = 5
let f = (x: i64) => x + n
return wrap(f)
}
fn main() {
let b = make()
print(b.f(1))
}tests/errors/escapingClosureRetainedArg.milo
cannot pass immutable
An immutable let Vec cannot be handed to a &mut [T] param — the mutable view would let the callee mutate a value the caller declared immutable.
fn setFirst(xs: &mut [i64]): void { xs[0] = 99 }
fn main() {
let v: Vec<i64> = Vec.new()
setFirst(&mut v) // @error: cannot pass immutable
}tests/errors/mutSliceImmutable.milo
cannot pass immutable 'x' as a '&mut' argument
A 'let' claims immutability + SSA-register storage; passing it to a '&mut' param would mutate it through the call and force a spill. Rejected — same as method receivers (v.push on an immutable Vec). Declare 'var' to allow it.
fn bump(a: &mut i64): void {
a = a + 1
}
fn main(): i32 {
let x: i64 = 5
bump(&mut x)
return x as i32
}tests/errors/letToMutParam.milo
cannot push to immutable Vec
fn main(): i32 {
let v: Vec<i32> = Vec.new()
v.push(10)
return 0
}tests/errors/vecPushImmutable.milo
cannot replace through an immutable place
replace's first argument must be an assignable, mutable place (a var, field, or element).
pub fn main(): i32 {
let x: i64 = 5
let old = replace(x, 9)
return 0
}tests/errors/replaceImmutable.milo
cannot return 'v.ptr()': 'v' is freed when this function returns, so the pointer would dangle
fn data(): *u8 {
var v: Vec<u8> = Vec.new()
v.push(0)
return v.ptr()
}
pub fn main(): i32 {
let p = data()
print(p as i64 != 0)
return 0
}tests/errors/ptrReturnDangles.milo
cannot return a closure that captures 'n' by reference
let g = f aliases the same closure, and the alias used to launder it past the return check: this printed -1. The escape pass follows the binding, so the alias is the same escape one hop removed and gets the same answer.
fn make(): (i64) => i64 {
let n = 5
let f = (x: i64) => x + n
let g = f
return g
}
fn main() {
let h = make()
print(h(1))
}tests/errors/escapingClosureAlias.milo
move on the OUTER closure does not own what an inner borrowing closure points at: moving a {fn, env} pair copies the pointer, and that env is still the frame make is about to leave. This printed -1 for 8 — a live use-after-free that move, the escape hatch every other diagnostic in this pass names, was laundering.
fn make(): move () => i64 {
let n = 5
let f = (x: i64) => x + n
let outer = move (): i64 => f(3)
return outer
}
fn main() {
let h = make()
print(h())
}tests/errors/escapingClosureMoveLaunder.milo
cannot return a closure that captures 's' by reference
Returning a by-reference-capturing closure is rejected, not promoted. The promotion it replaces set move at the return — after the move checker had already walked the body — so this exact program compiled and printed an EMPTY LINE for print(s) with no diagnostic: the capture had been moved into the closure's env back at the literal. Writing move yourself gets the intended behaviour and a real 'use of moved variable' on the print (tests/fixtures/escapingClosureMove.milo).
fn make(): () => i64 {
let s = "hello world"
let f = () => s.len()
print(s)
return f
}
fn main() {
let g = make()
print(g())
}tests/errors/escapingClosureReturn.milo
cannot return a reference
fn getSlice(s: &string): &string {
return s
}
fn main(): i32 {
return 0
}only a method may return a view; a free function has no receiver to freeze
fn viewOf(v: &Vec<i64>): &[i64] { return v[0..v.len] }
fn main(): i32 { return 0 }
// @error: cannot return a referencetests/errors/viewReturnFreeFn.milo
cannot return a reference stored inside 'Option'
A view handed back inside an enum payload outlives the freeze the call site took for it: Vec<&T> was already rejected for this, enum payloads were not, and the view below survived five reallocating pushes and still read the old buffer.
struct Hay { data: Vec<i64> }
impl Hay {
fn find(self: &Self): Option<&[i64]> { return Option.Some(self.data[0..1]) }
}
fn main(): i32 { return 0 }
// @error: cannot return a reference stored inside 'Option'tests/errors/optionOfViewReturn.milo
The view may not outlive the closure. Smuggling it out through R (the closure's own return type) is the escape nestedRef already rejects for a freeze, and it rejects it here for the same reason: Option<&[T]> is storage holding a reference.
from "std/foreign" import { withRaw }
from "std/os" import { malloc }
pub fn main(): i32 {
unsafe {
let p = malloc(8) as *i64
let escaped = withRaw(p, 1, (xs: &[i64]): Option<&[i64]> => Option.Some(xs))
print(1)
}
return 0
}tests/errors/withRawEscape.milo
cannot return a view of 'other'
a returned &string view may only point into the receiver's storage: the call site freezes the receiver and nothing else, so any other source could be freed under it
struct Lexer { src: string }
impl Lexer {
fn bad(self: &Self, other: &string): &string { return other[0..1] }
}
fn main(): i32 { return 0 }
// @error: cannot return a view of 'other'tests/errors/stringViewOfOtherParam.milo
the call site freezes the receiver, not the other argument
struct Buf { data: Vec<i64> }
impl Buf {
fn bad(self: &Self, other: &Vec<i64>): &[i64] { return other[0..1] }
}
fn main(): i32 { return 0 }
// @error: cannot return a view of 'other'tests/errors/viewOfOtherParam.milo
cannot return a view of 'tmp'
a returned view must point into the receiver's storage — a method-local's buffer is freed at the return, and the caller's freeze on the receiver would not cover it
struct Buf { data: Vec<i64> }
impl Buf {
fn bad(self: &Self): &[i64] {
var tmp: Vec<i64> = Vec.new()
tmp.push(1)
return tmp[0..1]
}
}
fn main(): i32 { return 0 }
// @error: cannot return a view of 'tmp'cannot send 'p' of type '*u8' across threads
from "std/runtime" import {
Promise
}
fn main(): i32 {
var x: i32 = 42
unsafe {
let p = (x.addrOf()) as *u8
let job = Promise<i64>.blocking(move(): i64 => {
return p as i64
}
)
let v = job.await()!
print(v)
}
return 0
}tests/errors/promiseBlockingNotSend.milo
The same body errors on Promise.blocking. spawnOsThreadDetached was added after the two hardcoded Send checks and never got one, so this compiled clean and handed a pointer into a dead frame to a real OS thread.
from "std/runtime" import {
spawnOsThreadDetached
}
pub fn main(): i32 {
var x: i32 = 42
unsafe {
let p = (x.addrOf()) as *u8
spawnOsThreadDetached(move () => {
print("ptr: ", p as i64)
}
)
}
return 0
}tests/errors/spawnDetachedNotSend.milo
from "std/runtime" import {
Promise
}
fn main(): i32 {
var x: i32 = 42
unsafe {
let p = (x.addrOf()) as *u8
let _t = Promise<i64>.blocking(move(): i64 => {
print("ptr value: ", p as i64)
return 0
}
)
}
return 0
}tests/errors/spawnNotSend.milo
cannot send 'u' of type 'Unsafe' across threads
from "std/runtime" import {
Promise
}
struct Unsafe {
p: *u8,
}
fn main(): i32 {
unsafe {
let u = Unsafe {
p: 0 as *u8
}
let _t = Promise<i64>.blocking(move(): i64 => {
print("bad: ", u.p as i64)
return 0
}
)
}
return 0
}tests/errors/spawnNotSendStruct.milo
cannot send 'wrapped'
from "std/runtime" import { Promise }
struct Wrapper<T> {
ptr: *u8,
value: T,
}
// Safety: the pointer is only an inert sentinel in this fixture.
unsafe impl Send for Wrapper<T> {}
fn main(): i32 {
let wrapped: Wrapper<*u8> = Wrapper {
ptr: 0 as *u8,
value: 0 as *u8,
}
let promise = Promise<i64>.blocking(move (): i64 => {
let _ = wrapped
return 0
})
let _ = promise
return 0
}tests/errors/sendGenericArgNotSend.milo
cannot store a closure that captures 'n' by reference
A closure without move captures by reference, so it is only valid while the frame owning the capture is alive. Returning one directly is fine (the checker promotes it to move), but hiding it inside a struct escaped by a side door the return check never saw: the struct is what leaves, and the closure rode along pointing at the dead frame. It read garbage at -O0 and hung at -O2, with ASAN silent because the capture lives on the stack. The store is rejected instead — move is the way to say you want the closure to own n (tests/fixtures/closureStoredMove.milo).
struct Holder {
f: (i64) => i64,
}
pub fn make(): Holder {
let n = 5
return Holder {
f: (x: i64) => x + n
}
}
pub fn main(): i32 {
let h = make()
let g = h.f
print(g(1))
return 0
}tests/errors/closureStoredInStruct.milo
The collection form of the same escape: pushing a by-reference closure into a Vec that then leaves the function. Rejected at the push — whether the Vec escapes is exactly what the checker cannot know without escape analysis, so it assumes it does.
pub fn make(): Vec<(i64) => i64> {
let n = 5
var v: Vec<(i64) => i64> = Vec.new()
v.push((x: i64) => x + n)
return v
}
pub fn main(): i32 {
let v = make()
let g = v[0]
print(g(1))
return 0
}tests/errors/closureStoredInVec.milo
A store into a struct LITERAL was already rejected; assigning into the field afterwards reached the same place by a route the pass did not walk, and was a live SIGKILL. Assigning a closure to a plain local stays legal — that local dies with the same frame the captures live in. A field, an element, or a global outlives it.
struct Box {
f: (i64) => i64,
}
fn make(): Box {
let n = 5
var b = Box { f: move (x: i64) => x }
b.f = (x: i64) => x + n
return b
}
fn main() {
let b = make()
print(b.f(1))
}tests/errors/escapingClosureAssign.milo
The store spelling of escapingClosureMoveLaunder — a different arm of the escape walk (StructLit, not Return), so it is locked separately. Ran off the end of the dead frame and printed unprintable bytes.
struct Box { f: move () => i64 }
var G: Box = Box { f: move (): i64 => 0 }
fn make() {
let n = 5
let f = (x: i64) => x + n
G = Box { f: move (): i64 => f(3) }
}
fn main() {
make()
print(G.f())
}tests/errors/escapingClosureMoveLaunderStore.milo
cannot store a reference in a Vec
struct Source { data: Vec<u8> }
impl Source {
fn view(self: &Self): &[u8] { return self.data[0..self.data.len] }
}
fn main() {
var src = Source { data: Vec.new() }
src.data.push(1 as u8)
var toks = Vec.new()
toks.push(src.view())
print($"{toks.len}")
}cannot take 'Fd' out of a container by index: it carries Drop
Indexing copies an element memberwise and consults neither Drop nor @noCopy, so let a = v[0] on a Vec<Fd> made a second owner of the same descriptor and closed it twice — printed close(3) twice, with only a warning. std/io's File, std/net's TcpStream and std/http's Socket are all this shape.
struct Fd {
fd: i32,
}
impl Drop for Fd {
fn drop(self: &mut Self): void {
if self.fd > 0 {
print("close(" + self.fd.toString() + ")")
}
}
}
pub fn main(): i32 {
var v: Vec<Fd> = Vec.new()
v.push(Fd { fd: 3 })
let a = v[0]
print("got " + a.fd.toString())
return 0
}tests/errors/indexDropElement.milo
cannot take 'Handle' out of a container by index: it carries a raw pointer field ('p')
Taking a pointer-holding struct out of a container by index copies it memberwise, so the copy and the container's own element would both own the pointee. The same error Drop and @noCopy elements get.
struct Handle {
p: *u8,
n: i64,
}
fn main() {
var v: Vec<Handle> = Vec.new()
v.push(Handle { p: 0 as *u8, n: 1 })
let h = v[0]
print(h.n)
}tests/errors/indexPointerFieldElement.milo
cannot take 'Res' out of a container by index: it carries Drop
A by-value parameter consumes its argument. Fed v[0] it received a memberwise copy of a Drop element, and both the parameter and the Vec slot ran Drop.
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
fn peek(r: Res): i64 { return r.id }
pub fn main(): i32 {
var v: Vec<Res> = Vec.new()
v.push(Res { id: 1 })
let n = peek(v[0])
print(n.toString())
return 0
}tests/errors/dropElementAsCallArg.milo
An assignment's right-hand side is consumed by value: s.r = v[0] copied the Drop element into the field and left the original in the Vec.
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
struct Holder { r: Res }
pub fn main(): i32 {
var v: Vec<Res> = Vec.new()
v.push(Res { id: 1 })
var h = Holder { r: Res { id: 0 } }
h.r = v[0]
print(h.r.id.toString())
return 0
}tests/errors/dropElementAssigned.milo
A struct-literal field takes its initializer by value: Holder { r: v[0] } made a second owner of the Drop element.
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
struct Holder { r: Res }
pub fn main(): i32 {
var v: Vec<Res> = Vec.new()
v.push(Res { id: 1 })
let h = Holder { r: v[0] }
print(h.r.id.toString())
return 0
}tests/errors/dropElementInStructLit.milo
The by-index rule used to fire only at a let initializer. Wrapping the element in an enum payload copied it memberwise and ran Drop once for the payload and once for the element still in the Vec (made 1, gone 3 with the call form below it).
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
pub fn main(): i32 {
var v: Vec<Res> = Vec.new()
v.push(Res { id: 1 })
let o = Option.Some(v[0])
print(o.isSome().toString())
return 0
}tests/errors/dropElementIntoEnumPayload.milo
return v[0] hands the caller a memberwise copy of a Drop element while the Vec keeps the original; std/arena's Arena.get was exactly this shape.
var gone: i64 = 0
struct Res { id: i64 }
impl Drop for Res { fn drop(self: &mut Self): void { gone = gone + 1 } }
fn first(v: &Vec<Res>): Res {
return v[0]
}
pub fn main(): i32 {
var v: Vec<Res> = Vec.new()
v.push(Res { id: 1 })
let r = first(v)
print(r.id.toString())
return 0
}tests/errors/dropElementReturned.milo
cannot take 'Texture' out of a container by index: it carries @noCopy
docs/language-reference.md says of @noCopy handles that "releasing twice, or using after release, is a compile error". Through the index spelling it was not: this program released texture 7 twice and only warned.
@noCopy
struct Texture {
id: u32,
}
fn release(t: Texture): void {
print("glDeleteTextures " + t.id.toString())
}
pub fn main(): i32 {
var v: Vec<Texture> = Vec.new()
v.push(Texture { id: 7 })
let a = v[0]
let b = v[0]
release(a)
release(b)
return 0
}tests/errors/indexNoCopyElement.milo
cannot take a view of a temporary
A view of a temporary has nothing to freeze: mk()[0..2] points into a Vec that no binding owns. freezeViewSource already rejected the METHOD spelling of this (mk().view()), and its own comment is the reason the slice spelling had to be rejected too: that storage survives only because temporaries leak, and it becomes a use-after-free the moment they get drop glue. Three drop-glue paths landed on 2026-08-16 alone, so the gap between the two spellings was closing from the wrong side. One hazard, three spellings, and only one of them errored. See tests/errors/ stringSliceOfTemporary.milo for the string half.
fn mk(): Vec<string> {
var v: Vec<string> = Vec.new()
v.push("alpha")
v.push("beta")
return v
}
fn main() {
let s = mk()[0..2]
print(s.len)
}tests/errors/sliceOfTemporary.milo
The string half of sliceOfTemporary: mk()[0..5] on a returned string views a buffer no binding owns. Same hazard, same reasoning, separate code path — which is why it needed its own check rather than inheriting one.
fn mk(): string {
return "hello world"
}
fn main() {
let v = mk()[0..5]
print(v)
}tests/errors/stringSliceOfTemporary.milo
a view of a temporary receiver has no binding the call site can freeze; the temporary only outlives the view today because temporaries are never dropped (they leak)
struct Ring { data: Vec<i64> }
impl Ring {
fn items(self: &Self): &[i64] { return self.data[0..self.data.len] }
}
fn makeRing(): Ring {
var r = Ring { data: Vec.new() }
r.data.push(7)
return r
}
fn main(): i32 {
let s = makeRing().items()
print(s[0])
return 0
}
// @error: cannot take a view of a temporarytests/errors/viewOfTemporary.milo
cannot take type parameters
An interface dispatches through a vtable: one slot per method, holding one address. A method with its own type parameter has one address PER INSTANTIATION, so there is no single function to put in the slot. Traits are static dispatch and can carry these; interfaces cannot, and saying so at the declaration beats failing later at the call.
interface Bad {
fn apply<R>(self: &Self, f: (&i64) => R): R
}
fn main() {
print(1)
}tests/errors/interfaceGenericMethod.milo
carries a payload in 'Num'
Only payload-free enums have a JSON form (the variant name as a string). A tagged union would need an encoding choice the derive is not entitled to make.
enum Value {
Nothing,
Num(i64),
}
@derive(Json)
struct Holder {
v: Value,
}
fn main() {
print(Holder {
v: Value.Nothing
}
.toJson())
}tests/errors/deriveJsonPayloadEnum.milo
casts only to an integer type
A repr'd enum casts to its integer discriminant, not to a float.
enum Kind: i32 { A, B }
pub fn main(): i32 {
let x = Kind.B as f64
return 0
}tests/errors/enumCastFloat.milo
closure returns string but can reach the end of its body without a 'return'
A closure with a declared non-void return that can fall off: codegen returned a zero of the return type, which for string is a null pointer.
fn main(): i32 {
let name = (n: i64): string => {
if n == 0 { return "zero" }
}
print(name(0))
return 0
}tests/errors/fallOffEndClosure.milo
collides with the built-in @derive(Eq)
A template may not take a built-in derive's name. Letting it lose silently to Eq would make a package's derive a no-op that still type-checks — the worst failure an extension point can have, because nothing anywhere reports it.
derive Eq {
fn eq(self: &Self, other: &Self): bool { return true }
}
fn main() {
print(1)
}tests/errors/deriveTemplateBuiltinName.milo
constant expression '2147483647 + 1' overflows i32
With an i32 hint the const subexpr is checked against i32's range (context-free it would now default to i64 and fit; the explicit type is what makes this an overflow).
fn main(): i32 {
let x: i32 = 2147483647 + 1
return 0
}tests/errors/constExprOverflow.milo
crosses the C ABI by value but is not declared 'extern struct'
a regular (non-extern) struct has no defined C layout — can't pass it by value
struct Vec2 {
x: f32,
y: f32,
}
extern fn takeVec(v: Vec2): void
fn main(): i32 {
return 0
}tests/errors/externStructMiloByValue.milo
declared as *u8 but got **u8
A **u8 value must not satisfy a *u8 binding — the depths differ. Before the fix both collapsed to *u8 and this wrongly type-checked.
extern fn get2(): **u8
fn main(): void {
unsafe {
// @error: declared as *u8 but got **u8
let bad: *u8 = get2()
print(bad[0] as i64)
}
}tests/errors/ptrDepthMismatch.milo
declared as string but got Result
The point of making the decoders fallible: you cannot use the decoded bytes without first dealing with the failure. Binding Base64.decode's Result straight into a string has to be rejected at compile time.
from "std/base64" import {
Base64
}
pub fn main(): i32 {
let bytes: string = Base64.decode("!!!!")
print(bytes.len.toString())
return 0
}tests/errors/decodeResultIgnored.milo
decreases clause must be an integer measure
fn f(n: i64): i64
decreases n > 0
{
if n == 0 {
return 0
}
return f(n - 1)
}
fn main() {
print(f(2))
}tests/errors/decreasesNotInteger.milo
depends on itself through another global
pub let A: string = "x" + B
pub let B: string = "y" + A
pub fn main(): i32 {
print(A)
return 0
}
// @error: depends on itself through another globaltests/errors/globalInitCycle.milo
did you mean 'clampF64' or 'clampI64'?
A truncated name is the common mistake on a namespace whose members carry a type suffix. Edit distance alone cannot find clampF64 from clamp (three letters short), and for min it used to answer 'sin'. A name that starts a longer member is suggested ahead of any edit-distance match, all of them when several do.
from "std/math" import { Math }
pub fn main(): i32 {
let x = Math.clamp(1.5, 0.0, 1.0)
print(x)
return 0
}tests/errors/staticMethodPrefixSuggestion.milo
did you mean 'toUpper'?
toUpperCase is not a typo — it is the JavaScript spelling — so edit distance alone would never find it. The alias table carries it.
pub fn main(): i32 {
let s = "hello"
print(s.toUpperCase())
return 0
}tests/errors/methodNameSuggestion.milo
discriminant 2147483648 is out of range for i32
Reject before codegen can truncate the tag and silently construct another value.
enum Bad: i32 { TooLarge = 2147483648 }
pub fn main(): i32 { return 0 }tests/errors/enumReprOutOfRange.milo
does not implement trait 'HasValue'
trait HasValue {
fn value(self: &Self): i32
}
struct Empty {
x: i32
}
fn printValue<T: HasValue>(thing: &T): i32 {
return thing.value()
}
fn main(): i32 {
let e = Empty {
x: 0
}
return printValue(e)
}tests/errors/traitBoundUnsatisfied.milo
does not implement trait 'HasValue', required by 'Wrap<T: HasValue>'
A trait bound on a generic STRUCT's type param is enforced, and the diagnostic names the struct that required it instead of leaking "no method 'get'" errors out of the wrapper's own body.
trait HasValue {
fn value(self: &Self): i32
}
struct Empty {
x: i32
}
struct Wrap<T: HasValue> {
inner: T,
}
impl Wrap<T> {
fn make(inner: T): Wrap<T> {
var w: Wrap<T> = Wrap {
inner: inner
}
return w
}
fn get(self: &Self): i32 {
return self.inner.value()
}
}
fn main(): i32 {
let w = Wrap<Empty>.make(Empty {
x: 0
}
)
return w.get()
}tests/errors/structBoundUnsatisfied.milo
does not satisfy interface
interface Drawable {
fn draw(self: &Self): string
}
struct Empty {
}
fn render(d: &Drawable) {
print(d.draw())
}
fn main(): i32 {
let e = Empty {
}
render(e)
return 0
}tests/errors/interfaceNotSatisfied.milo
ensures clause must be bool
fn bar(x: i64): i64
ensures result + 1
{
return x
}
fn main(): i32 {
print(bar(5))
return 0
}tests/errors/ensuresTypeMismatch.milo
enum 'E' is recursive by value and has infinite size
An enum whose payload reaches back to it by value through a struct has infinite size: E.X(A) stores an A inline and A stores an E inline. This used to compile with the enum's payload sized before the struct's size was known, and the program then spilled 32 bytes past every E it built (ASan: dynamic-stack-buffer-overflow). The clonable version of this program lives in tests/fixtures/cloneDeriveEnumInStruct.milo, with the cycle broken by a Heap.
struct A {
name: string,
e: E,
}
enum E {
X(A),
Y,
Z(string),
}
fn describe(a: &A): string {
match a.e {
E.X(inner) => {
return $"{a.name}>" + describe(inner)
}
E.Y => {
return $"{a.name}>Y"
}
E.Z(s) => {
return $"{a.name}>Z({s})"
}
}
}
fn main() {
let leaf = A {
name: "leaf", e: E.Z("z")
}
let root = A {
name: "root", e: E.X(leaf)
}
let copy = root.clone()
print(describe(root))
print(describe(copy))
let e = E.X(A {
name: "solo", e: E.Y
}
)
let e2 = e.clone()
match e2 {
E.X(a) => {
print(describe(a))
}
E.Y => {
print("Y")
}
E.Z(s) => {
print(s)
}
}
}
// @expect: root>leaf>Z(z)
// @expect: root>leaf>Z(z)
// @expect: solo>Ytests/errors/enumRecursiveThroughStruct.milo
enum 'List' has infinite size due to recursive field
enum List {
Cons(i32, List),
Nil
}
fn main(): i32 {
return 0
}tests/errors/recursiveEnumNoHeap.milo
expected '='
Use-before-init probe from docs/memory-safety-vs-rust.md: a declaration without an initializer does not parse, so there is no uninitialized binding for a later read to observe. Rust rejects the read; Milo rejects the declaration.
pub fn main(): i32 {
let x: i32
x = 1
return x
}tests/errors/useBeforeInit.milo
expected 'IDENT'
Malformed parameter list — parser should report what it expected.
fn main(: i32 {
return 0
}tests/errors/parseExpectedToken.milo
expected *HandleA, got *HandleB
extern type HandleA
extern type HandleB
fn takeA(h: *HandleA): void {
}
fn main(): i32 {
let b = 0 as *HandleB
takeA(b)
return 0
}
// @error: expected *HandleA, got *HandleBtests/errors/externTypeMismatch.milo
expected a type, but the file ended here
A file that stops mid-declaration used to walk the parser's token index past the EOF sentinel, and the next lookahead read undefined.kind — a raw TypeError with a JS stack trace instead of a diagnostic. Found by scripts/fuzz-frontend.ts. The file must stay truncated: the last token is the colon, with nothing after it.
struct P{x:tests/errors/truncatedStructFieldType.milo
expected an integer length
fn main(): i32 {
var v: Vec<i64> = [1, 2, 3]
v.truncate("two")
return 0
}tests/errors/vecTruncateBadArg.milo
expected NodeId, got EdgeId
Newtypes are distinct types: passing one where another is expected must fail, even though both wrap i64. This is the cross-pool/mixed-ID guard.
struct NodeId { idx: i64 }
struct EdgeId { idx: i64 }
fn takesNode(n: NodeId): i64 { return n.idx }
fn main() {
let e = EdgeId { idx: 5 }
print(takesNode(e)) // @error: expected NodeId, got EdgeId
}tests/errors/newtypeCrossType.milo
expected two string arguments
Bare idents parse (that's @derive's arg form) but say nothing about which C type or header is meant, so @cLayout must reject them rather than skip the check.
@cLayout(timespec)
extern struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
fn main() {
print(sizeOf<Timespec>())
}tests/errors/cLayoutBadArgs.milo
expects 1 args, got 2
extern fn puts(s: *u8): i32
fn main(): i32 {
puts("a", "b")
return 0
}tests/errors/wrongArgCount.milo
extern 'fcntl' declares 3 fixed parameters but C fixes only 2
The exact declaration that cost node-milo hours: fcntl(fd, F_SETFL, flags) with fixed arity compiles clean, but on AArch64 the variadic callee reads its third arg off the stack while a fixed-arity call passes it in a register — so O_NONBLOCK never landed and every socket in the runtime stayed blocking. It presented as a throughput mystery, not as a bad declaration, because x86_64's conventions coincide for integer args.
extern fn fcntl(fd: i32, cmd: i32, arg: i32): i32
fn main() {
print(fcntl(0, 3, 0))
}tests/errors/variadicExternFixedArity.milo
field '_data' of 'Sealed' is private to 'std/seal.milo'
The seal forge path: swapping _data under a live Span would leave the span resolving against different bytes with the brand still matching. _data is file-private to std/seal.milo, so user code cannot reach it.
from "std/seal" import { Sealed, seal }
fn main(): i32 {
var s = seal("hello")
s._data = "world"
return 0
}tests/errors/sealedDataForged.milo
field '_x' of 'S' is private to
Naming a _ field in a struct literal from another file. A struct with any _ field can only be built by literal in its own file; other files go through a constructor.
from "lib/privateField" import { S }
fn main(): i32 {
let s = S { _x: 1, y: 2 }
return s.y
}tests/errors/privateFieldLiteral.milo
Reading a _ field of a struct declared in another file. The declaring file exports a constructor and an accessor; this file has to use them.
from "lib/privateField" import { S, makeS }
fn main(): i32 {
let s = makeS(3)
return s._x
}tests/errors/privateFieldRead.milo
Assigning a _ field from outside the declaring file: the write is what would break whatever invariant the declaring file keeps on it.
from "lib/privateField" import { S, makeS }
fn main(): i32 {
var s = makeS(3)
s._x = 4
return s.y
}tests/errors/privateFieldWrite.milo
field 'a' of 'Pair': expected i64, got string
A struct literal that spells its type arguments is checked against them: the fields do not get to pick a different instance.
struct Pair<A, B> {
a: A,
b: B,
}
fn main() {
let p = Pair<i64, string> { a: "no", b: "x" }
print(p.b)
}tests/errors/structLitTypeArgsMismatch.milo
for range start must be an integer
fn main(): i32 {
for i in "hello".."world" {
print(i)
}
return 0
}tests/errors/forRangeType.milo
from "std/json" import { Json }
A type used without importing it is indistinguishable from a typo at the use site, but the compiler can read std and write out the exact import line.
pub fn main(): i32 {
let doc = Json.obj()
print(doc.build())
return 0
}tests/errors/unknownTypeMissingImport.milo
has more than one @iter field
Two delegates would make for x in pair ambiguous with no way to say which.
struct Pair {
@iter a: Vec<i64>,
@iter b: Vec<i64>,
}
pub fn main(): i32 {
let p = Pair {
a: [1], b: [2]
}
print(p.a.len())
return 0
}tests/errors/iterDelegateTwoFields.milo
has no static method 'knew'
A failed static call on a type that exists names the type and the method. This used to report "unknown enum 'P'" — a word that does not apply to a struct.
struct P { a: i64 }
impl P {
fn new(): P {
return P { a: 1 }
}
}
pub fn main(): i32 {
let p = P.knew()
print(p.a)
return 0
}tests/errors/staticMethodTypo.milo
has signature extern (*u8, i64) => i32, expected extern (*u8, i32) => i32
A C function pointer is the one place where a near-miss signature is not caught by the callee: the call site is the declaration. So the match has to be exact here.
extern struct Ops {
read: (*u8, i32) => i32,
}
fn wide(_p: *u8, n: i64): i32 {
return (n + 1) as i32
}
fn main() {
let ops = Ops {
read: wide,
}
print(1)
}tests/errors/externFnPtrWrongSig.milo
if condition must be bool, got Result<JwtClaims, JwtError>
from "std/jwt" import {
Jwt
}
// `if Jwt.verifyHS256(token, secret)` was the whole problem: it checks the signature
// and silently accepts a token that expired years ago, or was minted for another
// audience. Verification returns the claims, so the old spelling has to stop
// compiling — a caller who wants a yes/no answer writes `.isOk()` and has at least
// looked at what they are throwing away.
pub fn main(): i32 {
let token = Jwt.signHS256("{\"sub\":\"a\"}", "secret")
if Jwt.verifyHS256(token, "secret") {
print("authenticated")
}
return 0
}tests/errors/jwtVerifyIsNotBool.milo
infinite size
A struct that contains itself by value has no finite layout. It used to compile into a broken type; it must be rejected. (Indirection via Heap/Vec is the fix.)
struct Node {
value: i32
next: Node
}
fn main(): i32 {
return 0
}tests/errors/recursiveStructByValue.milo
instantiation exceeded depth
An unbounded recursive generic instantiates itself on an ever-growing type. The monomorphizer must cap the depth and fail cleanly, not blow the JS stack.
struct Wrap<T> { v: T }
fn grow<T>(x: T, n: i32): i32 {
if n <= 0 { return 0 }
let w: Wrap<T> = Wrap { v: x }
return grow<Wrap<T>>(w, n - 1)
}
fn main(): i32 {
return grow<i32>(0, 100000)
}tests/errors/recursiveGenericDepth.milo
integer literal 200 overflows i8
fn main(): i32 {
let x: i8 = 200
return 0
}tests/errors/integerLiteralOverflow.milo
integer-repr enum 'Bad' cannot be generic
Monomorphization must not silently discard a repr or its discriminants.
enum Bad<T>: i32 { A, B }
pub fn main(): i32 { return 0 }tests/errors/enumReprGeneric.milo
is @pure but calls 'helper', which is not
fn helper(x: i64): i64 {
return x + 1
}
@pure
fn wrap(x: i64): i64 {
return helper(x)
}
pub fn main() {
print(wrap(1))
}tests/errors/pureCallsImpure.milo
is @pure but calls extern fn 'cbrt'
extern fn cbrt(x: f64): f64
@pure
fn root(x: f64): f64 {
return cbrt(x)
}
pub fn main() {
print(root(8.0))
}tests/errors/pureCallsExtern.milo
is @pure but calls the function value 'f'
@pure
fn apply(f: (i64) => i64, x: i64): i64 {
return f(x)
}
pub fn main() {
print(apply((n: i64) => n + 1, 1))
}tests/errors/pureCallsFnValue.milo
is @pure but contains an 'unsafe' block
@pure
fn peek(x: i64): i64 {
var y = x
unsafe {
let p = y.addrOf()
}
return y
}
pub fn main() {
print(peek(1))
}tests/errors/pureUnsafeBlock.milo
is @pure but touches the mutable global 'counter'
var counter: i64 = 0
@pure
fn bump(): i64 {
counter = counter + 1
return counter
}
pub fn main() {
print(bump())
}tests/errors/pureTouchesGlobal.milo
is a copy of the matched payload — the write would be discarded
A &mut self method on a copy-bound pattern binding used to compile and silently throw the write away: inside bump v==2, after the match v==1. The identical operation through a &mut fn arg was always rejected ("cannot pass immutable 'n' as a '&mut' argument"), so the two paths disagreed and the accepting one was wrong. Three things have to line up for this to be a real loss, and they all do here: - Ctr is all-i64, so it is Copy and the binding is a snapshot (a non-Copy payload is MOVED into the binding, which then owns it — that write is real); - the binding is by value, not a & view into the enum; - the subject b is a VARIABLE that outlives the arm, so the discard is observable. Matching a temporary (match Child.spawn(...)) is legal for exactly the opposite reason — see tests/fixtures/matchTempBindMutate.milo.
struct Ctr {
v: i64,
}
impl Ctr {
fn bump(self: &mut Self): void {
self.v = self.v + 1
}
}
enum Box {
Full(Ctr),
Empty,
}
fn main() {
var b = Box.Full(Ctr {
v: 1
}
)
match b {
Box.Full(c) => {
c.bump()
}
Box.Empty => {
}
}
}tests/errors/matchCopyBindMutate.milo
is a directory
A directory used to surface as a raw EISDIR stack trace from the compiler.
fn main(): i32 {
let x = @embedFile("../fixtures/lib")
return 0
}tests/errors/embedFileDirectory.milo
is a mutable global, and this code runs on a real OS thread
A mutable global is not a capture, so the Send/Sync check, which is keyed to closure capture types, never saw it. Two OS threads incrementing it compiled clean, and at -O2 LLVM hoists the whole loop into one load/add/store pair, so the race loses every update but the last rather than a few.
from "std/runtime" import {
Promise
}
var counter: i64 = 0
pub fn main(): i32 {
let p = Promise<i64>.blocking(move(): i64 => {
counter = counter + 1
return 0
}
)
counter = counter + 1
p.await()!
return 0
}tests/errors/threadGlobalRace.milo
is already unwrapped here — a second '&mut Bump' to the same object would alias the first
Two live &mut to one object is the aliasing the one rule exists to prevent, and the unwrap is the only place a nullable extern reference could produce a second one. The borrow is scoped, so unwrapping once per arm of an if is still fine.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn bumpX(b: ?&mut Bump): i32 {
let first = b else {
return -1
}
let second = b else {
return -1
}
first.x = 1
return second.x
}
fn main() {
print(1)
}tests/errors/nullableRefDoubleUnwrap.milo
is borrowed
The map is borrowed for the whole of a modify callback: an insert inside it could grow the table and move the very entry the callback's &mut view points into.
fn main(): i32 {
var m: HashMap<string, i64> = HashMap.new()
m.insert("a", 1)
m.modify("a", (v: &mut i64): void => {
m.insert("b", 2)
v = v + 1
})
return 0
}tests/errors/hashMapModifyReentrant.milo
is borrowed mutably and shared in the same call
A container and a view into it cannot be arguments to the same call: the callee's push reallocates and frees the storage the view points at. The exclusivity check only saw auto-borrowed args, and a slice expression is already a &[T], so this pair used to compile and read freed memory.
fn grow(v: &mut Vec<i64>, s: &[i64]): void {
var i = 0
while i < 4096 {
v.push(999)
i = i + 1
}
print(s[0])
}
fn main() {
var v: Vec<i64> = [11, 22, 33]
grow(&mut v, v[0..2]) // @error: is borrowed mutably and shared in the same call
}tests/errors/viewAliasesContainerArg.milo
is borrowed mutably twice in the same call
Same hazard with both sides mutable: &mut Vec plus a &mut [T] window into it. The write through the stale view landed in freed memory and was silently lost.
fn grow(v: &mut Vec<i64>, s: &mut [i64]): void {
var i = 0
while i < 4096 {
v.push(9)
i = i + 1
}
s[0] = 7
}
fn main() {
var v: Vec<i64> = [1, 2, 3]
grow(&mut v, &mut v[0..2]) // @error: is borrowed mutably twice in the same call
}tests/errors/mutViewAliasesContainerArg.milo
is bound by a pattern of an immutable subject
Assigning to a match binding of a &Node subject is rejected. The hint names the two ways that work: match through a &mut subject (the payload then binds as a &mut view, tests/fixtures/matchMutPayload.milo) or rebuild the variant. It must not offer the generic "declare with 'var'" advice: v is a pattern binding and there is no let to change.
enum Node {
Leaf(i64),
Branch(string),
}
fn bump(n: &Node): void {
match n {
Node.Leaf(v) => { v = v + 1 }
Node.Branch(s) => { print("branch ", s) }
}
}
fn main() {
var n = Node.Leaf(1)
bump(n)
}tests/errors/matchBindingAssign.milo
is incomplete
A struct with a field moved out is not the whole value any more, so using it AS a whole shows the emptied field as if it were data — 'Pair { a: "", b: "two" }'. Reading a different field stays fine; this is only the whole-value use. Rust: E0382, borrow of partially moved value.
struct Pair {
a: string,
b: string,
}
fn main() {
let p = Pair {
a: "one", b: "two"
}
let x = p.a
print(x)
print(p.b)
print(p)
}tests/errors/readPartiallyMovedStruct.milo
is not a C function signature
The signature is pasted into a generated TU, so it's held to a charset that can't close the assert and inject statements.
@cSig("unistd.h", "int close(int)); void evil(void")
extern fn close(fd: i32): i32
fn main() {
print(close(-1))
}is not a C header path
The header string is pasted into a generated '#include <...>', so it's constrained to a charset that can't break out of the include and inject C into the guard TU.
@cLayout("struct timespec", "time.h> \n#include <stdio.h")
extern struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
fn main() {
print(sizeOf<Timespec>())
}tests/errors/cLayoutBadHeader.milo
is not a C identifier
The C name is pasted into a generated TU, so it is held to a charset that cannot close the assert and inject statements — the same rule @cSig applies to its signature.
@cValue("SEEK_END), 1); int evil(void", "stdio.h")
let SEEK_END: i64 = 2
fn main() {
print(SEEK_END)
}tests/errors/cValueBadName.milo
is not a constant
A var can be reassigned at runtime, so there is no fixed value for the guard to compare against the header — the assert would describe only the initializer.
@cValue("SEEK_END", "stdio.h")
var SEEK_END: i64 = 2
fn main() {
print(SEEK_END)
}is not a library name
'framework:' is the only prefix the link step knows how to spell. Anything else in that position would reach the linker verbatim, so it is rejected here instead.
@link("bundle:OpenGL")
extern fn floor(x: f64): f64
fn main() {
print(1)
}tests/errors/linkBadPrefix.milo
The @link name is pasted into the link command the compiler shells out to. milo add fetches third-party source, so a package that could smuggle a shell command through this would run it on every machine that built the package.
@link("m; touch /tmp/milo-link-pwned")
extern fn floor(x: f64): f64
fn main() {
print(1)
}tests/errors/linkInjection.milo
is not a nullable extern reference, so 'let q = … else { … }' has nothing to unwrap
let NAME = value else { … } with no pattern is the nullable-extern-reference unwrap and nothing else. Unwrapping an enum names its variant, so the two forms cannot be confused for one another.
fn main() {
let n = 5
let q = n else {
return
}
print(q)
}tests/errors/letElseNotNullableRef.milo
is not C-representable
A move closure type owns a heap environment, which is the one thing a C field cannot hold — so it is NOT thinned into a code pointer, it is refused. The plain (A, B) => R spelling is the one that means "C function pointer" here.
extern struct Ops {
read: move (*u8, i32) => i32,
}
fn main() {
print(1)
}tests/errors/externFnPtrMoveField.milo
The field is a C spelling, so its own parameters and return have to cross the ABI too. A Milo string is a three-word owned buffer and has no C counterpart, in a callback signature exactly as in a plain field.
extern struct Ops {
name: (string) => i32,
}
fn main() {
print(1)
}tests/errors/externFnPtrNonCRepr.milo
string carries drop glue + non-C layout — illegal in an extern struct
extern struct BadRec {
id: i32,
name: string,
}
fn main(): i32 {
return 0
}tests/errors/externStructBadField.milo
is not iterable
@iter only makes sense on a field for-in already knows how to walk.
struct Bad {
@iter n: i64,
}
pub fn main(): i32 {
let b = Bad {
n: 1
}
print(b.n)
return 0
}tests/errors/iterDelegateNotIterable.milo
is not supported on a struct field
The attribute set is closed. A silently-ignored one is the failure @cLayout exists to close — a typo'd @cOpaque would leave the field checked against a C struct that has no such member, which is a confusing error at best.
extern struct Timeval {
tv_sec: i64,
@cBogus tv_usec: i32,
}
fn main() {
print(sizeOf<Timeval>())
}tests/errors/cOpaqueUnknownFieldAttr.milo
is not supported on enums
Enums parse attributes but nothing consumes them — processDerives walks only structs. Rejecting is honest; silently ignoring left @derive(Eq) enum looking like it worked.
@derive(Eq)
enum Color {
Red,
Green,
}
fn main() {
print(1)
}tests/errors/attributeOnEnum.milo
is not what unistd.h declares
asserts on a diagnostic that quotes unistd.h; the MSVC CRT has no such header, so the compile fails for a different reason and proves nothing. The stated C signature is wrong: sysconf returns long, not int. Checked exactly against the header via __builtin_types_compatible_p.
@cSig("unistd.h", "int sysconf(int)")
extern fn sysconf(name: i32): i64
fn main() {
print(sysconf(29))
}it is not inside a '@fields { … }' block
@name names a field, so it only means something inside the repetition that supplies one. Rejecting is the point: the alternative — substituting an empty string — produces code the user never wrote, failing somewhere it cannot be read.
trait Describe { fn describe(self: &Self): string }
derive Describe {
fn describe(self: &Self): string { return "@name" }
}
@derive(Describe)
struct Point {
x: i64,
}
fn main() {
let p = Point { x: 1 }
print(p.describe())
}tests/errors/deriveTemplateFieldHole.milo
jsonStringify: field 'tags' has unsupported type
struct Bad {
name: string,
tags: Vec<string>,
}
fn main(): i32 {
var tags: Vec<string> = Vec.new()
tags.push("x")
let b = Bad {
name: "a", tags: tags
}
print(jsonStringify(b))
return 0
}tests/errors/jsonStringifyUnsupportedField.milo
let binding 'x' has no value after '='
The error must anchor at the let (line below), not the return a line later.
fn main(): i32 {
let x =
return x
}tests/errors/let-missing-value.milo
let-else block must diverge
The else block runs only when the pattern doesn't match, so it must diverge (return/break/continue) — otherwise the binding wouldn't be guaranteed live.
fn find(x: i32): Option<i32> {
return Option.Some(x)
}
fn main() {
let Option.Some(v) = find(3) else {
print("this does not diverge")
}
print(v)
}tests/errors/letElseNoDiverge.milo
manual 'Send' implementation for 'Handle' must be unsafe
struct Handle {
ptr: *u8,
}
impl Send for Handle {}
fn main(): i32 {
return 0
}tests/errors/sendImplRequiresUnsafe.milo
mark 'Conn' @copy if it does not own what the pointer points at
A struct with a raw pointer field is move-tracked unless it says @copy: the pointer is a scalar, but what it addresses may be owned, and two copies of an owning handle would each release it. The hint names the opt-out, so a non-owning view knows what to write.
struct Conn {
handle: *u8,
id: i32,
}
impl Conn {
fn close(self: Self) {
print(self.id)
}
}
fn main() {
let c = Conn { handle: 0 as *u8, id: 3 }
c.close()
c.close()
}tests/errors/pointerFieldStructNotCopy.milo
MathException.kind (C 'type'): Milo says 8 bytes, C header disagrees
The @cLayout guard checks a @cName field under its C name: this assertion can only fire if the generated C reads sizeof(((struct exception *)0)->type). C's type is an int; declaring it i64 is the wrong-width drift the guard exists to catch.
@cLayout("struct exception", "math.h")
pub extern struct MathException {
@cName("type") kind: i64,
name: *u8,
arg1: f64,
arg2: f64,
retval: f64,
}
pub fn main() {
print(sizeOf<MathException>())
}tests/errors/cNameFieldMismatch.milo
Milo declares a 4-byte return
asserts on a diagnostic that quotes unistd.h; the MSVC CRT has no such header, so the compile fails for a different reason and proves nothing. The C signature is right, but the Milo declaration disagrees with it: long is 8 bytes, i32 is 4. This is the Milo<->C mapping check — the half that a header-only comparison can't catch.
@cSig("unistd.h", "long sysconf(int)")
extern fn sysconf(name: i32): i32
fn main() {
print(sysconf(29))
}tests/errors/cSigWrongMilo.milo
Milo declares i64 (8 bytes)
asserts on a diagnostic that quotes stdlib.h's abs spelling. A scalar parameter declared too wide is an ABI mismatch the linker cannot see.
@cSig("stdlib.h", "int abs(int)")
extern fn abs(x: i64): i32
fn main() {
print(1)
}tests/errors/cSigParamWidth.milo
Milo has no '++' — write 'i += 1'
++ is a C-family reflex with an exact Milo spelling, so the parse error names it rather than reporting a stray '+'.
pub fn main(): i32 {
var i: i64 = 0
i++
print(i)
return 0
}tests/errors/noIncrementOperator.milo
Milo says offset 0
Transposed fields: every read of tv_sec would silently return nanoseconds. The widths match, so only the offsets catch this one.
@cLayout("struct timespec", "time.h")
extern struct Timespec {
tv_nsec: i64,
tv_sec: i64,
}
fn main() {
print(sizeOf<Timespec>())
}tests/errors/cLayoutTransposed.milo
Milo writes through a *u32 (4-byte pointee)
asserts on a diagnostic that quotes string.h's strlen spelling; the MSVC CRT declares it differently, so the compile fails for a different reason. An out-param's pointee width IS the contract — it is how many bytes the callee writes into the caller's frame. Nothing else in the pipeline can see this: the ABI passes one word whatever the pointee, so a wrong pointee links, runs, and scribbles.
@cSig("string.h", "size_t strlen(const char *)")
extern fn strlen(s: *u32): u64
fn main() {
print(1)
}tests/errors/cSigParamPointee.milo
mismatched types
fn main(): i32 {
let x = if true {
10
} else {
"hello"
}
return 0
}tests/errors/if_expr_mismatch.milo
missing required method 'eq'
trait Eq {
fn eq(self: &Self, other: &Self): bool
}
struct Point {
x: i32
}
impl Eq for Point {
}
fn main(): i32 {
return 0
}tests/errors/traitMissingMethod.milo
moves a captured value out of itself when it runs
A move closure whose body moves a capture out can only run once. Captures live in the environment's own slots, so handing one to a callee by value zeroes the slot it came from. A second call therefore reads an emptied capture. That used to be a double free; once captures aliased their slots it became a silent wrong answer (this program printed 52 then 0), which is worse to debug than either.
fn consume(s: string): i64 {
return s.len()
}
fn main(): void {
let s = "hello world, long enough to force a heap allocation" + "!"
let f = move () => consume(s)
print(f())
print(f())
}tests/errors/moveClosureCalledTwice.milo
must be an integer literal
Folding this in the compiler and asserting the result would compare Milo's arithmetic against itself — it proves nothing about what the header says. The point of @cValue is to check a hand-transcribed constant, so there has to be a transcription to check.
@cValue("SEEK_END", "stdio.h")
let SEEK_END: i64 = 1 + 1
fn main() {
print(SEEK_END)
}tests/errors/cValueNotLiteral.milo
must diverge (return/break/continue) — it runs when C passed null
The else block runs when C passed null, so falling out of it would reach code where the binding does not exist. Same rule the enum let-else already enforces.
extern struct Bump {
x: i32,
}
@externalLinkage
pub fn bumpX(b: ?&mut Bump): i32 {
let p = b else {
print("null")
}
return p.x
}
fn main() {
print(1)
}tests/errors/nullableRefElseFallsThrough.milo
no definition here to give linkage to
@externalLinkage
extern fn getpid(): i32
fn main() {
let _p = getpid()
}tests/errors/externalLinkageOnExtern.milo
no entry point
A module with no main() is a library: it type-checks fine (milo check says ok), but it cannot be built into an executable. The linker used to answer this with Undefined symbols: "_main", which names no Milo file and reads like a missing extern. The errors lane runs milo build, so this fixture pins the driver's diagnostic rather than the checker's.
pub let GREETING: string = "hello"
pub fn greet(): string {
return GREETING.clone()
}tests/errors/noEntryPoint.milo
no whitespace allowed between '@' and attribute name
An attribute name must hug its '@' — @derive, never @ derive.
@ derive(Eq)
struct Point {
x: i32,
y: i32,
}
fn main(): i32 {
return 0
}tests/errors/attributeSpace.milo
non-exhaustive match
Every arm returns, but the arms do not cover the subject: the missing variant is the path that falls off. Only the exhaustiveness error is reported, since the fall-off is a consequence of it and not a second mistake.
enum Color { Red, Green, Blue }
fn code(c: Color): i64 {
match c {
Color.Red => { return 1 }
Color.Green => { return 2 }
}
}
fn main(): i32 {
print(code(Color.Blue))
return 0
}tests/errors/fallOffEndMatchNonExhaustive.milo
non-exhaustive match: missing variant
enum Color {
Red,
Green,
Blue,
}
fn main(): i32 {
let c = Color.Red
match c {
Color.Red => {
return 0
}
Color.Green => {
return 1
}
}
return 0
}tests/errors/matchNonexhaustive.milo
non-exhaustive match: missing variant 'Plain'
The other direction of the same rule: only a variant that CANNOT hold a value is excused. A live variant sitting next to a dead one is still a case the reader has to handle, and dropping the whole check would be the silent-success version of this feature.
enum Never {}
enum Wrapper {
Boxed(Never),
Plain(i64),
}
pub fn main(): i32 {
let w = Wrapper.Plain(5)
match w {
Wrapper.Boxed(n) => {
match n {
}
}
}
return 0
}tests/errors/uninhabitedStillRequiresLive.milo
not hashable
A struct is hashable only if every field is. A Vec field is not, so a struct containing one cannot be a key. (A struct of scalar fields, like Point, IS a valid key — see fixtures/hashmapStructKey.milo.)
struct Bag {
items: Vec<i32>
}
fn main(): i32 {
var m: HashMap<Bag, i32> = HashMap.new()
return 0
}tests/errors/hashmapWrongKeyType.milo
old() may only appear in an 'ensures' clause
fn f(n: i64): i64
requires old(n) >= 0
{
return n
}
fn main() {
print(f(1))
}tests/errors/oldOutsideEnsures.milo
old() takes a scalar
fn f(v: &mut Vec<i64>): i64
ensures v.len == old(v).len
{
return v.len
}
fn main() {
var xs = vecNew<i64>()
print(f(&mut xs))
}tests/errors/oldNonScalar.milo
only 'extern struct' has a C layout to verify
A plain Milo struct has no C counterpart, so there's nothing to check it against.
@cLayout("struct timespec", "time.h")
struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
fn main() {
print(sizeOf<Timespec>())
}tests/errors/cLayoutNotExtern.milo
only a top-level 'fn' can be stored in a C function-pointer field
A closure value is a { code, environment } pair and the field holds only the code half. Storing one would drop the environment on the floor, and the C caller would then invoke the code with whatever happened to be in the argument register.
extern struct Ops {
read: (*u8, i32) => i32,
}
fn main() {
let bias = 7
let ops = Ops {
read: (_p: *u8, n: i32): i32 => n + bias,
}
print(1)
}tests/errors/externFnPtrClosure.milo
only an 'extern fn'
@link("SDL2")
fn notExtern(): i32 {
return 0
}
fn main(): i32 {
return notExtern()
}tests/errors/link-attr-non-extern.milo
only an 'extern fn' has a C signature to verify
A Milo fn is compiled from this source — there's no foreign declaration to check.
@cSig("unistd.h", "int f(int)")
fn f(x: i32): i32 {
return x
}
fn main() {
print(f(1))
}tests/errors/cSigOnMiloFn.milo
only an 'extern struct' field can be C-invisible
Field attributes parse on any struct so the checker can explain why this one doesn't belong; rejecting it in the grammar would only say expected IDENT, got '@'.
struct Point {
@cOpaque x: i32,
y: i32,
}
fn main() {
print(sizeOf<Point>())
}tests/errors/cOpaqueOnMiloStruct.milo
only an integer constant can be checked
The guard compares the two sides with '==' in C. A string has no such comparison, so the attribute cannot mean anything here.
@cValue("SDL_HINT_RENDER_DRIVER", "stdio.h")
let RENDER_DRIVER: string = "metal"
fn main() {
print(RENDER_DRIVER)
}tests/errors/cValueNonInt.milo
only valid as the iterable of a 'for ... in' loop
The pieces are views into t, so there is nowhere to keep them: the loop that freezes the receiver is the only place they are valid.
pub fn main(): i32 {
let t = "a,b"
let _parts = t.splitView(",")
return 0
}
// @error: only valid as the iterable of a 'for ... in' looptests/errors/stringViewIterStored.milo
Option
pop() returns Option<T>, not T — using it as a bare i32 must fail. Force the value out with !, ?, or ??.
fn main(): i32 {
var v: Vec<i32> = Vec.new()
v.push(1)
let x: i32 = v.pop()
print(x)
return 0
}tests/errors/popReturnsOption.milo
Option<Option<T>> has no distinct JSON encoding
Some(None) and an absent field both encode as null, so the outer layer cannot survive a round trip. Refused rather than silently collapsed.
@derive(Json)
struct Row {
v: Option<Option<i64>>,
}
fn main() {
print(Row {
v: Option.None
}
.toJson())
}tests/errors/deriveJsonNestedOption.milo
out of range
A ranged-int parameter enforces its bound on the argument (was unchecked).
fn setPercent(p: i32(0..100)): void { print(p) }
fn main() {
setPercent(500) // @error: out of range
}Reassigning an out-of-range literal to a ranged var is rejected (was unchecked).
fn main() {
var p: i32(0..100) = 50
p = 500 // @error: out of range
}tests/errors/rangeReassign.milo
A ranged-int return type enforces its bound on the returned value (was unchecked).
fn get(): i32(0..100) {
return 300 // @error: out of range
}
fn main() { print(get()) }overflows u8
A coerced if-arm still range-checks each literal against the target width.
fn main(): void {
let h: u8 = if true {
300
} else {
0
} // @error: overflows u8
print(h.toString())
}tests/errors/ifExprArmOverflow.milo
passes struct 'Pt' by value
extern struct Pt {
x: i32,
y: i32,
}
// callback taking a by-value struct — unsupported at the C boundary
extern fn onEach(cb: (Pt) => void): void
fn main(): i32 {
return 0
}tests/errors/externFnPtrStructParam.milo
pointer field access requires 'unsafe' block
extern struct Point {
x: i32,
y: i32,
}
fn main(): i32 {
var p = Point {
x: 1, y: 2
}
let ptr = 0 as *Point
let v = ptr.x
return 0
}
// @error: pointer field access requires 'unsafe' blocktests/errors/ptrFieldNoUnsafe.milo
pointer parameter 'c' is stored in the global 'GS', which outlives the buffer it points into
The pointer travels inside a struct passed by value; the type carries it, so the store is caught the same way as a bare *u8 parameter.
struct Cfg { buf: *u8 }
var GS: Cfg = Cfg { buf: 0 as *u8 }
fn keepS(c: Cfg): void {
GS = c
}
fn main(): void {
var v: Vec<u8> = Vec.new()
v.push(1 as u8)
keepS(Cfg { buf: v.ptr() })
v.push(2 as u8)
}tests/errors/ptrParamStoredInGlobalStruct.milo
pointer parameter 'p' is stored in the global 'G', which outlives the buffer it points into
Backlog #43: let p = v.ptr() freezes v while p is live, and a call holds p for its duration, but a callee that copies p into a global keeps it past the call. The next v.push reallocates and G dangles: heap-use-after-free under --sanitize with zero unsafe. The store is the error.
var G: *u8 = 0 as *u8
fn keep(p: *u8): void {
G = p
}
fn main(): void {
var v: Vec<u8> = Vec.new()
v.push(104 as u8)
keep(v.ptr())
var i = 0
while i < 1000 {
v.push(0 as u8)
i = i + 1
}
print(G as i64 != 0)
}tests/errors/ptrParamStoredInGlobal.milo
pointer parameter 'p' is stored in the global 'GV' by 'push', which outlives the buffer it points into
var GV: Vec<*u8> = Vec.new()
fn keepV(p: *u8): void {
GV.push(p)
}
fn main(): void {
var v: Vec<u8> = Vec.new()
v.push(1 as u8)
keepV(v.ptr())
v.push(2 as u8)
}tests/errors/ptrParamPushedIntoGlobal.milo
push: expected i32, got bool
fn main(): i32 {
var v: Vec<i32> = Vec.new()
v.push(true)
return 0
}tests/errors/vecPushWrongType.milo
reading 'T' by value through a raw pointer copies it bitwise; 'T' may own memory
The shape that Shard<string> freed a block twice through, kept from recurring by omission: a generic body is only ever checked as an instance, and the unsound instance is the one nobody wrote a fixture for. So the TEMPLATE is scanned: an rvalue read of self.base[i] where base: *T and T is a type parameter of a generic that is not @copyOnly is refused here, whatever T the program goes on to use. Writes, borrows (self.base[i].len) and memcpy-style moves are not this rule's business.
struct Window<T> {
base: *T,
len: i64,
}
impl Window<T> {
fn get(self: &Self, i: i64): T {
unsafe {
return self.base[i]
}
}
}
pub fn main(): i32 {
var v: Vec<i64> = Vec.new()
v.push(4)
let w: Window<i64> = Window {
base: v.ptr(), len: v.len
}
print(w.get(0))
return 0
}tests/errors/rawTypeParamReadNotCopyOnly.milo
references cannot be stored in a collection
struct Bag { items: Vec<&[u8]> }
fn main() {
print("unreachable")
}tests/errors/structFieldVecOfRefs.milo
A borrow inside a Vec outlives the value it borrows: the Vec survives the scope that owns it, so reading the element later is a use-after-free.
fn main() {
var toks: Vec<&[u8]> = Vec.new()
print($"{toks.len}")
}tests/errors/vecOfRefsAnnotated.milo
references cannot be stored in structs
struct MyView {
data: &string
}
fn main(): i32 {
return 0
}requires an unsafe block
extern fn malloc(size: i64): *u8
fn main(): i32 {
let p = malloc(64)
return 0
}
// @error: requires an unsafe blocktests/errors/unsafeExternPtrReturn.milo
requires clause 'lo <= hi' violated
from "std/math" import { Math }
fn main(): i32 {
let x = Math.clampI64(5, 10, 3)
return 0
}tests/errors/contractViolationClamp.milo
requires clause 'x >= 0.0' violated
from "std/math" import { Math }
fn main(): i32 {
let r = Math.sqrt(-1.0)
return 0
}tests/errors/contractViolation.milo
requires clause must be bool
fn foo(x: i64): i64
requires x + 1
{
return x
}
fn main(): i32 {
print(foo(5))
return 0
}tests/errors/contractTypeMismatch.milo
requires function to return
fn bad(opt: Option<i32>): i32 {
let val = opt?
return val
}
fn main(): i32 {
return bad(Option.Some(1))
}tests/errors/propagateWrongRet.milo
requires impl 'Eq' for 'Score'
trait Eq {
fn eq(self: &Self, other: &Self): bool
}
trait Ord: Eq {
fn less(self: &Self, other: &Self): bool
}
struct Score {
val: i32
}
impl Ord for Score {
fn less(self: &Self, other: &Self): bool {
return self.val < other.val
}
}
fn main(): i32 {
return 0
}tests/errors/traitMissingSupertrait.milo
requires numeric type
fn main(): i32 {
let a = "hello"
let b = "world"
let c = a - b
return 0
}shadows a standard-library function
asciiIsDigit is a prelude/std function taking (ch). Redefining it with a different signature would rebind std/string's own internal calls to this body and break them — the resolver rejects it. (A same-signature override is fine.)
fn asciiIsDigit(ch: u8, extra: i64): bool {
return extra > 0
}
fn main(): i32 {
if asciiIsDigit(48, 1) {
return 1
}
return 0
}tests/errors/shadowsStdlib.milo
shadows an outer binding
A loop binding that shadows an outer name is rejected. It used to be accepted and then LEAK past the loop: codegen's locals map is flat by name, so every later mention of row resolved to the loop's slot and read the last element — silently, and to a let. Rejecting the shadow removes the class rather than patching each loop form's scope handling.
fn main(): i32 {
let row = 5
var nums: Vec<i64> = [7, 8, 9]
var total = 0
for row in nums { // @error: shadows an outer binding
total = total + row
}
print($"{total + row}")
return 0
}tests/errors/loopVarShadow.milo
string.push: expected u8, got i64
Hinting string.push's arg with u8 lets an int LITERAL coerce (s.push(65)), but must not start accepting an actual i64 value — that would be a silent truncation, which is the opposite of what the papercut fix was for.
fn main() {
var s: string = ""
let n: i64 = 65
s.push(n)
print(s)
}tests/errors/stringPushI64.milo
struct 'Direct<i64>' is recursive by value and has infinite size
A generic declaration says nothing about layout: Direct<T> holding a Direct<T> is only infinite once T is chosen, so the rejection has to happen per instantiation, not per declaration. It used to fall through both: the declaration walk skips generics, and the instantiation walk was a stack overflow. Codegen then received %Direct_i64 = type { i64, %Direct_i64 }, which the backend refuses.
pub struct Direct<T> {
v: T,
me: Direct<T>,
}
fn use(d: Direct<i64>): i64 {
return d.v
}
pub fn main(): i32 {
print("unreachable")
return 0
}tests/errors/genericStructInfiniteSize.milo
struct 'P' has no field 'nope'
struct P { a: i64 }
fn main(): i32 {
let { nope } = P { a: 1 }
print(nope)
return 0
}tests/errors/destructureUnknownField.milo
struct 'Ping<i64>' is recursive by value and has infinite size
The cycle does not have to be one hop. Ping<T> reaches itself through Pong<T>, and both instantiations are infinite, so both are rejected. The legitimate near-miss is a cycle through a METHOD signature rather than a field, which is finite and must keep compiling: see tests/fixtures/genericStructCycle.milo.
pub struct Ping<T> {
v: T,
other: Pong<T>,
}
pub struct Pong<T> {
back: Ping<T>,
}
fn use(p: Ping<i64>): i64 {
return p.v
}
pub fn main(): i32 {
print("unreachable")
return 0
}tests/errors/genericStructInfiniteSizeIndirect.milo
struct 'Plain' has no JSON codec
A nested struct needs its own codec. Without this check the generated call to Plain.fromJsonNode fails inside code the user never wrote.
struct Plain {
x: i64,
}
@derive(Json)
struct Wrapper {
inner: Plain,
}
fn main() {
print(Wrapper {
inner: Plain {
x: 1
}
}
.toJson())
}tests/errors/deriveJsonUnsupportedField.milo
swap: operands have different types
swap exchanges two places of the SAME type.
pub fn main(): i32 {
var a: i64 = 1
var b: i32 = 2
swap(a, b)
return 0
}tests/errors/swapTypeMismatch.milo
takes 2 parameters, the Milo declaration takes 3
Arity is checkable with no header in the picture, and it has to be: the guard TU compares C parameter i against Milo parameter i, so a signature listing a different number of parameters would silently shift every width comparison by one and report a mismatch against the wrong parameter — or, worse, agree by luck.
@cSig("unistd.h", "ssize_t read(int, void *)")
extern fn read(fd: i32, buf: *u8, n: i64): i64
fn main() {
print(1)
}tests/errors/cSigArityMismatch.milo
takes string by value, but each passes &string
A combinator lends its callback a pointer INTO the container it is iterating. Declaring the parameter by value is fine for a Copy element (codegen loads it), but a string owns heap: taking it by value would move it out of a container the caller still owns. This used to type-check. Codegen passed the pointer anyway and the callback read it as a string, so each printed whatever bytes followed that address in the process — a memory disclosure out of ordinary safe Milo, no unsafe anywhere.
pub fn main(): i32 {
let v: Vec<string> = ["a", "bb"]
v.each((s: string) => {
print(s)
}
)
return 0
}tests/errors/vecCallbackMovesOwned.milo
targetOs() takes no arguments
@targetOs() is a nullary compile-time builtin — the OS is fixed by the build target, not chosen by an argument. Passing one is a mistake worth naming.
fn main(): i32 {
let os = @targetOs("windows")
print(os)
return 0
}tests/errors/targetOsArgs.milo
the ranges 0..2 and 1..3 overlap
two &mut windows into one buffer with literal bounds: disjointness is decidable, so overlap is a rejectable aliasing violation rather than the "may be distinct elements" case that sibling index args fall under
fn f(a: &mut [i32], b: &mut [i32]): void {
a[0] = 1
b[0] = 2
}
fn main(): i32 {
var v: Vec<i32> = [1, 2, 3, 4]
f(&mut v[0..2], &mut v[1..3])
return 0
}
// @error: the ranges 0..2 and 1..3 overlaptests/errors/mutSliceOverlapArgs.milo
the struct does not derive Json
@json only means something to the derived codec. Accepted silently, a rename on a hand-written serializer would read as applied when nothing consumes it.
struct User {
@json("user_id") userId: i64,
}
fn main() {
print(User {
userId: 1
}
.userId.toString())
}tests/errors/jsonFieldAttrWithoutDerive.milo
two fields map to the JSON name 'id'
A rename that collides with another field would make the encoder emit a duplicate key and the decoder read whichever the parser kept.
@derive(Json)
struct Row {
id: i64,
@json("id") rowId: i64,
}
fn main() {
print(Row {
id: 1, rowId: 2
}
.toJson())
}tests/errors/deriveJsonDuplicateKey.milo
type '[i64]' has no method 'push'
A slice is a non-owning VIEW. The read-only combinators are available on it because it shares the Vec representation, but the mutating half of that same set must not be: a slice does not own the storage it points at, and growing it would reallocate a buffer belonging to someone else. Whitelisting the read-only names rather than widening the whole arm is what keeps this rejected.
fn grow(s: &[i64]) {
s.push(1)
}
fn main() {
var v: Vec<i64> = Vec.new()
v.push(1)
grow(v)
print(v.len)
}type 'Handle' has no method 'clone'
@noCopy exists to stop copies of an integer-shaped resource handle, so the auto-derive skips it: handing the same type a clone() would be the same hazard with an explicit spelling.
@noCopy
pub struct Handle { id: i32 }
pub fn main(): i32 {
let h = Handle { id: 7 }
let h2 = h.clone()
print(h2.id)
return 0
}tests/errors/deriveCloneNoCopyAbsent.milo
type 'Math' has no static method 'minimum'
The binding cosT is initialized from a call that failed, so its type is unknown. Its later uses used to report "use of moved variable 'cosT'" twice on top of this error, because unknown is not Copy. tests/checkerRecovery.test.ts asserts that follow-on error is gone; this fixture pins the one that remains.
from "std/math" import { Math }
struct V {
x: f64,
y: f64,
}
fn dot(a: V, b: V): f64 {
return a.x * b.x + a.y * b.y
}
fn scale(v: V, s: f64): V {
return V { x: v.x * s, y: v.y * s }
}
fn refract(a: V, n: V): V {
let cosT = Math.minimum(dot(a, n), 1.0)
let perp = scale(n, cosT)
let par = scale(a, cosT * 2.0)
return V { x: perp.x + par.x + cosT, y: perp.y + par.y }
}
pub fn main(): i32 {
let r = refract(V { x: 1.0, y: 0.0 }, V { x: 0.0, y: 1.0 })
print(r.x)
return 0
}tests/errors/poisonedBindingNoCascade.milo
type alias 'A' is cyclic
An alias is expanded, so one that names itself has no type to expand into and the expansion does not terminate. Before this it ran until the host stack died and printed a JavaScript trace instead of a diagnostic - it did exit nonzero, so the build failed either way, but the message named a line of the compiler rather than a line of the program. A struct or enum may refer to itself through an indirection; an alias cannot.
type A = B
type B = A
pub fn main(): i32 {
var x: A = 1
print(x)
return 0
}tests/errors/cyclicTypeAlias.milo
type alias 'Loop' is cyclic
Same rule through the generic path, where the substitution rebuilds the same application on every step.
type Loop<T> = Loop<T>
pub fn main(): i32 {
var x: Loop<i64> = 1
print(x)
return 0
}tests/errors/cyclicGenericAlias.milo
type alias 'Pair' takes 1 type argument(s), got 0
A generic alias names a shape, not a type: Pair on its own is missing the very thing that would make it one.
type Pair<T> = Vec<T>
pub fn main(): i32 {
var x: Pair = Vec.new()
x.push(1)
return 0
}tests/errors/genericAliasBare.milo
type alias 'Pair' takes 1 type argument(s), got 2
An alias is expanded by substituting one argument per parameter, so the wrong count has nothing to expand into. Reported against the alias rather than as an "unknown type", which is what the use site would otherwise become.
type Pair<T> = Vec<T>
pub fn main(): i32 {
var x: Pair<i64, bool> = Vec.new()
x.push(1)
return 0
}tests/errors/genericAliasArity.milo
type does not implement Send
from "std/runtime" import {
Promise
}
struct Dangerous {
_ptr: *u8,
}
fn main(): i32 {
unsafe {
let d = Dangerous {
_ptr: 0 as *u8
}
let _t = Promise<i64>.blocking(move(): i64 => {
let _x = d._ptr
return 0
}
)
}
return 0
}type mismatch
extern fn puts(s: *u8): i32
fn main(): i32 {
let x: i32 = "hello"
return 0
}tests/errors/typeMismatch.milo
type mismatch in '-': u32 vs i32
fn main(): i32 {
let s: i32 = 5
let u: u32 = 7
let r = u - s
return 0
}tests/errors/mixedSignArith.milo
type mismatch in '+': i64 vs i32
fn main(): i32 {
let a: i64 = 10
let b: i32 = 5
let c = a + b
return 0
}tests/errors/mixedWidthArith.milo
type mismatch: 'a' declared as i32 but got i64
An unannotated int binding defaults to i64 and never silently narrows: let m = 5 is i64, so assigning it into an i32 var is rejected (i64 -> i32 is lossy).
fn main(): i32 {
let m = 5
let a: i32 = m
return 0
}tests/errors/flexIntLocked.milo
type parameter 'T' cannot carry a bound
A bound constrains what a body may do with a parameter. An alias has no body of its own - it is replaced at each use - so a bound here would be a promise nothing checks. Put it on the function or struct that uses the alias instead.
trait Show {
fn show(self: &Self): string
}
type Shown<T: Show> = Vec<T>
pub fn main(): i32 {
var x: Shown<i64> = Vec.new()
x.push(1)
return 0
}tests/errors/genericAliasBound.milo
undefined function 'adoptHeap'
Minting ownership of an arbitrary address is not a general escape hatch: outside std/foreign.milo the name means nothing. adopt adds the null test and the Option wrapping in readable Milo, and it is @unsafe; let b = adoptHeap(p) here would hand back the same owned box with neither.
from "std/os" import {
malloc
}
pub fn main(): i32 {
unsafe {
let p = malloc(8) as *i64
let b = adoptHeap(p)
print(*b)
}
return 0
}tests/errors/adoptHeapOutsideForeign.milo
undefined function 'adoptVec'
The slice half of the same seam. Both intrinsic names are gated by one file test, so this and adoptHeapOutsideForeign.milo fall together, which is the point of checking both: a gate written per name is a gate that loses a name.
from "std/os" import {
malloc
}
pub fn main(): i32 {
unsafe {
let p = malloc(8) as *i64
let v = adoptVec(p, 1)
print(v.len)
}
return 0
}tests/errors/adoptVecOutsideForeign.milo
undefined function 'foo'
fn main(): i32 {
foo()
return 0
}undefined function 'rawSlice'
The view constructor is not a general escape hatch: outside std/foreign.milo the name means nothing. withRaw's closure parameter is what bounds the view's life, and let s = rawSlice(p, n) here would hand back the same view with no such bound.
from "std/os" import { malloc }
pub fn main(): i32 {
unsafe {
let p = malloc(8) as *i64
let s = rawSlice(p, 1)
print(s.len)
}
return 0
}tests/errors/rawSliceOutsideForeign.milo
undefined variable 'y'
extern fn printf(fmt: *u8, val: i32): i32
fn main(): i32 {
printf("%d\n", y)
return 0
}tests/errors/undefinedVar.milo
unknown attribute '@drive'
Attribute names are a closed set. An unrecognized one used to be dropped in silence, so a typo compiled clean while doing nothing — exactly the failure @cLayout exists to close.
@drive(Eq)
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 }
print(p.x)
}tests/errors/unknownAttribute.milo
unknown attribute '@mustUse' on 'S'
@mustUse
struct S {
n: i32
}
fn main(): i32 {
let s = S { n: 1 }
return s.n
}tests/errors/mustUseOnStructRejected.milo
unknown escape sequence '\q'
An escape the lexer does not know used to drop its backslash silently, so a typo'd \u{1b} shipped as visible garbage instead of failing to compile.
fn main() {
print("\q")
}tests/errors/unknownEscape.milo
unknown module directive '@!foo'
@!foo
pub fn main() {}tests/errors/unknownModuleDirective.milo
unreachable code
pub fn main(): i32 {
var i = 0
while i < 5 {
if i == 2 {
break
print(i)
}
i = i + 1
}
return 0
}tests/errors/unreachableAfterBreak.milo
fn f(x: i64) {
if x > 0 {
print(x)
return
} else {
print(0 - x)
return
}
print(x + 1)
}
pub fn main(): i32 {
f(3)
return 0
}tests/errors/unreachableAfterIfElseReturn.milo
A bare return in a void fn used to swallow the next line as its value, so the call still ran (and the block was left with instructions past its terminator).
fn f(x: i64) {
print(x)
return
print(x + 1)
}
pub fn main(): i32 {
f(3)
return 0
}tests/errors/unreachableAfterVoidReturn.milo
unsafe impl is only supported for the Send and Sync marker traits
trait Named {
fn name(self: &Self): string
}
struct Item {}
unsafe impl Named for Item {
fn name(self: &Self): string {
return "item"
}
}
fn main(): i32 {
return 0
}tests/errors/unsafeImplNonMarker.milo
unsupported representation 'u32'
Enum tags currently have an i32 ABI; other repr spellings must fail in the checker.
enum Bad: u32 { A, B }
pub fn main(): i32 { return 0 }tests/errors/enumReprUnsupported.milo
unwrap it with 'match'
parseInt is total now: it answers Option<i64>, never a silent 0. Using it as a bare i64 has to say so, and say what to do about it.
pub fn main(): i32 {
let n: i64 = "42".parseInt()
print(n)
return 0
}tests/errors/parseIntIsOption.milo
use of moved value 'p.a'
A field is a place: moving out of it a second time has to be caught the same way moving a whole binding twice is. It used to compile — the first move zeroed the field so nothing could double-free, and the second read handed back the zeroed slot as an empty string. Memory-safe and silently wrong. Found by scripts/fuzz-ownership.ts.
struct Pair {
a: string,
b: string,
}
fn main() {
let p = Pair {
a: "delta", b: "papa"
}
let x = p.a
let y = p.a
print(x)
print(y)
}tests/errors/moveFieldTwice.milo
use of moved variable
fn check(m: HashMap<i32, i32>): i32 {
return 0
}
fn main(): i32 {
var m: HashMap<i32, i32> = HashMap.new()
check(m)
check(m)
return 0
}tests/errors/hashmapUseAfterMove.milo
The other half of what @noCopy buys: releasing the same handle twice is the same use-after-move the compiler already rejects for a String or a Vec.
@noCopy
struct Fd {
n: i32,
}
impl Fd {
fn close(self: Self) {
print(self.n)
}
}
fn main() {
let f = Fd {
n: 3
}
f.close()
f.close()
}tests/errors/noCopyDoubleFree.milo
A resource handle is an integer, so the all-fields-Copy rule would make this type Copy and free would consume nothing — the release-then-use below is the bug @noCopy exists to catch. See isAllCopyStruct.
@noCopy
struct Texture {
id: u32,
w: i64,
}
impl Texture {
fn make(): Texture {
return Texture {
id: 7, w: 640
}
}
fn bind(self: &Self) {
print(self.id)
}
// Consumes: releasing the GL name ends this handle's life.
fn free(self: Self) {
print(self.id)
}
}
fn main() {
let t = Texture.make()
t.free()
t.bind()
}tests/errors/noCopyUseAfterFree.milo
The parallel-sum footgun — the thing people get wrong. In C this compiles and silently races: long total = 0; // in each worker thread: total += partial; // torn reads, lost updates, passes tests "usually" In Milo you physically cannot hand the same mutable counter to two threads: the first move closure takes ownership of total, so the second capture is a use-after-move. The compiler names the fix — clone the handle (AtomicI64.clone shares one synchronized cell) or don't share at all and return partials. Either way the data race is unrepresentable, not merely discouraged. See tests/fixtures/battleConcurrency.milo for the guided-correct version.
from "std/sync" import {
AtomicI64
}
from "std/runtime" import {
Promise
}
pub fn main(): i32 {
let total = AtomicI64.new(0)
// first thread takes ownership of `total`...
let a = Promise<i64>.blocking(move(): i64 => {
total.add(1)
return 0
}
)
// ...so handing it to a second thread is a compile error, not a silent race.
let b = Promise<i64>.blocking(move(): i64 => {
total.add(1)
return 0
}
)
a.await()!
b.await()!
return 0
}tests/errors/sharedMutAcrossThreads.milo
fn take(s: string): i32 {
return 0
}
fn main(): i32 {
let s = "hello"
take(s)
take(s)
return 0
}tests/errors/stringUseAfterMove.milo
use of moved variable 'a'
freeze() consumes the arena. Reaching for the old binding afterwards is the compile-time half of the guarantee: no path survives through which a slot could be freed after the freeze, so a frozen handle cannot go stale.
from "std/arena" import {
Arena, FrozenArena
}
pub fn main(): i32 {
var a: Arena<i64> = Arena<i64>.new()
let h = a.alloc(1)
let frozen = a.freeze()!
let more = a.alloc(2)
print(frozen.get(h).toString())
return 0
}tests/errors/arenaFreezeUseAfterMove.milo
A binding consumed on either arm of a fork is unusable afterwards — which arm ran is a runtime fact. This used to compile and double-free: neither arm was marked moved, so both slots were still dropped at scope exit.
fn main() {
var a = "aaa" + ""
var b = "bbb" + ""
let c = true
let s = if c { a } else { b }
print(s)
print(a)
}tests/errors/useAfterMoveIfExpr.milo
fn main(): i32 {
let a = "owned string"
let b = a
print(a)
print(b)
return 0
}tests/errors/useAfterMoveLetBinding.milo
fn main() {
var a = "aaa" + ""
var b = "bbb" + ""
let n = 0
let s = match n { 0 => a, _ => b }
print(s)
print(a)
}tests/errors/useAfterMoveMatchExpr.milo
use of moved variable 'box'
The adopted box is owned in the ordinary way, which means the ordinary rules apply to it. Nothing about arriving through a raw pointer makes it exempt: moving it out and then reading it is the same error the move checker gives for Heap(value), and that equivalence is the whole claim adopt makes.
from "std/foreign" import {
adopt
}
from "std/os" import {
malloc
}
fn consume(b: Heap<i64>): i64 {
return *b
}
pub fn main(): i32 {
unsafe {
let p = malloc(8) as *i64
p[0] = 5
match adopt(p) {
Option.Some(box) => {
let a = consume(box)
let b = consume(box)
print(a, " ", b)
}
Option.None => {
print("none")
}
}
}
return 0
}tests/errors/adoptedUseAfterMove.milo
use of moved variable 'd'
?? moves its default operand whether or not its result is bound. Recording that only when the result landed in a move position meant (o ?? d).len moved d with no diagnostic, and the later read of d printed an empty string.
fn maybe(i: i64): Option<string> {
if i > 0 {
return Option.Some("s")
}
return Option.None
}
fn main() {
var d = "default-"
d.pushStr("x")
let n = (maybe(0) ?? d).len
print(n.toString())
print(d)
}tests/errors/coalesceMovesDefault.milo
use of moved variable 'data'
parallelMap CONSUMES the Vec. That is the whole aliasing argument: while the buffer is divided there is no binding through which it can be reached except the disjoint windows, so nothing the workers touch can alias anything the caller still holds.
from "std/shard" import {
Shard, parallelMap
}
fn keep(w: Shard<f64>): Shard<f64> {
return w
}
pub fn main(): i32 {
var data: Vec<f64> = Vec.new()
data.push(1.0)
let out = parallelMap(data, 2, keep)
data.push(2.0)
print(out.len.toString())
return 0
}tests/errors/shardUseAfterMap.milo
use of moved variable 'f'
An owning closure holds a heap environment, so there is exactly one of it. Duplicating the value would give two owners of that environment — the reason a destructor could not exist for a closure at all before move became part of the type.
fn call(f: move () => i64): i64 {
return f()
}
fn main() {
var s = "heap"
s.pushStr("-payload")
let f = move(): i64 => {
return s.len
}
print(call(f))
print(call(f))
}tests/errors/moveClosureNotCopy.milo
Spawning transfers the closure: an owning closure is not Copy, so Task.spawn(f) moves it, spawnWithStack forgets it after handing the environment to the task, and the task releases it when it is reaped. Calling f afterwards would run against that released environment — this exact program printed a captured string's length as 34 inside the task and 5 after the join, back when the free was added without the ownership.
from "std/runtime" import {
Task
}
fn main() {
var s = "heap"
s.pushStr("-payload-long-enough-to-matter")
let f = move(): void => {
print("in-task len=" + s.len.toString())
}
let t = Task.spawn(f)
t.join()
f()
}tests/errors/spawnMovesTheClosure.milo
use of moved variable 'o'
The other half: ?? also moves the payload OUT of its Option operand. The enum's tag survives, so o still matched Option.Some afterwards and handed out an emptied payload — a use-after-move that read as a legitimate value.
fn main() {
var d = "dflt"
d.pushStr("-x")
var o: Option<string> = Option.Some("payload-long-enough")
let n = (o ?? d).len
print(n.toString())
if let Option.Some(p) = o {
print(p)
}
}tests/errors/coalesceMovesOperand.milo
The move tracking a pointer field brings reaches every struct that embeds it by value: Outer has no pointer of its own, yet moving it ends its life.
struct Inner {
p: *u8,
}
struct Outer {
inner: Inner,
n: i64,
}
fn take(o: Outer): i64 {
return o.n
}
fn main() {
let o = Outer { inner: Inner { p: 0 as *u8 }, n: 7 }
take(o)
take(o)
}tests/errors/nestedPointerFieldUseAfterMove.milo
orElse forwards the receiver's Some payload into its result, so for a non-Copy T the receiver and the result would both own one heap buffer and both free it. The combinator consumes the receiver instead — the same rule Result.map/andThen use for their forwarded Err payload. Clone at the call site to keep the original.
fn main() {
var s = ""
s.pushStr("hi")
let o: Option<string> = Option.Some(s)
let _first = o.orElse(() => Option.None)
let _second = o.orElse(() => Option.None)
}tests/errors/optionOrElseUseAfterMove.milo
use of moved variable 'r'
.context consumes its receiver: the Err payload moves into the box and the Ok payload is forwarded, so the original Result is gone afterwards.
fn f(): Result<string, string> {
return Result.Err("boom")
}
fn main(): i32 {
let r = f()
let c = r.context("calling f")
let _ = c
print(r.isOk())
return 0
}tests/errors/contextMovesReceiver.milo
expr? consumes the operand (Err returns it, Ok extracts + zeros the slot); using it afterward must be a compile error, not a silent read of the zeroed enum.
fn getIt(): Result<string> {
return Result.Ok("owned heap string")
}
fn use(): Result<i32> {
var r = getIt()
let s = r?
match r {
Result.Ok(v) => { print("ok: ", v) }
Result.Err(e) => { print("err: ", e) }
}
print(s)
return Result.Ok(0)
}
fn main() {
let _ = use()
}tests/errors/propagateUseAfterMove.milo
map forwards the Err payload into its result untouched. With a non-Copy E that would leave the receiver and the result both owning one heap buffer, and both run drop glue — an ASAN double-free. So map consumes the receiver when E is non-Copy, and touching it afterwards must be rejected here rather than crashing at runtime. The error is built with pushStr, not a literal: a literal string has cap == 0, marking a static buffer that drop glue skips, so a literal payload would not expose the bug this rule exists to prevent.
fn makeErr(): Result<i64, string> {
var s = ""
s.pushStr("boom-")
s.pushStr("heap-allocated-tail")
return Result.Err(s)
}
fn main() {
let r = makeErr()
let m = r.map((n) => n * 2)
print($"r isErr={r.isErr()} m isErr={m.isErr()}")
}tests/errors/resultMapConsumesNonCopyErr.milo
expr! moves the payload out of a non-Copy Result and codegen zeros the source slot; using the operand afterward must be a compile error, not a silent read of the zeroed enum.
fn getIt(): Result<string> {
return Result.Ok("owned heap string")
}
fn main() {
var r = getIt()
let s = r!
match r {
Result.Ok(v) => { print("ok: ", v) }
Result.Err(e) => { print("err: ", e) }
}
print(s)
}tests/errors/unwrapUseAfterMove.milo
use of moved variable 's'
An enum whose variant carries a raw pointer is move-tracked, as a struct with a pointer field is: an owning handle wrapped in an enum was Copy by the all-variants-Copy rule, so take(s); take(s) duplicated it.
enum Slot { Some(*u8), Empty }
fn take(s: Slot): void {
match s {
Slot.Some(p) => { print("some") }
Slot.Empty => { print("empty") }
}
}
fn main(): i32 {
let s = Slot.Some(0 as *u8)
take(s)
take(s)
return 0
}tests/errors/enumPointerPayloadCopy.milo
forget consumes its argument like any other transfer — the value is gone, it is only the drop that does not run. Reading it afterwards is the same use-after-move as any.
fn main() {
var s = "heap"
s.pushStr("-buffer")
forget(s)
print(s)
}tests/errors/forgetEndsOwnership.milo
from "std/runtime" import {
Task
}
fn main(): i32 {
let s = "hello"
Task.spawn(move(): void => {
print(s)
}
)
print(s)
return 0
}tests/errors/moveAfterClosureCapture.milo
fn consume(s: string): void {
}
fn main(): i32 {
let s: string = "hello"
consume(s)
consume(s)
return 0
}tests/errors/useAfterMove.milo
use of moved variable 'value'
fn consume(value: Heap<i32>): void {
}
fn main(): i32 {
let value: Heap<i32> = Heap(42)
consume(value)
print(*value)
return 0
}tests/errors/heapUseAfterMove.milo
use of moved variable 'x'
The element rule is what makes "arrays are Copy" safe: [u8; N] is Copy, but an array of NON-Copy elements must still move, or the two takes calls below would each own the same heap strings and free them twice.
fn takes(a: [string; 2]): i64 {
return a[0].len
}
fn main() {
let x: [string; 2] = ["ab", "cd"]
print(takes(x))
print(takes(x))
}tests/errors/arrayNonCopyMove.milo
Same arm-entry consumption as match (see matchSubjectReadInArm.milo), via the if-let path: the then-branch destructures a non-Copy payload, so the subject is zeroed before the branch body runs. The else-branch never destructures and must stay free to read the subject.
enum Val { Str(string), Num(i64) }
fn describe(v: &Val): i64 {
match v {
Val.Str(s) => { return s.len }
Val.Num(n) => { return 0 }
}
}
fn main() {
var x = Val.Str("hello world")
if let Val.Str(s) = x {
print($"{describe(x)}")
} else {
print($"{describe(x)}")
}
}tests/errors/ifLetSubjectReadInThen.milo
Reading the match subject inside an arm that destructures a non-Copy payload used to compile and silently read zeroed memory: codegen zeroes the payload slot at ARM ENTRY (extractBindings), but the checker deferred the subject's move to the END of the whole match, so the arm body saw it as still-live. The failure mode was worse than "always false" — the enum TAG survives the zeroing, so a discriminant-only predicate still answered correctly while a payload-reading one silently didn't. The _ arm is deliberately non-destructuring: it must stay legal to read the subject there, since nothing is zeroed on that path.
enum Val { Str(string), Num(i64) }
fn startsHello(v: &Val): bool {
match v {
Val.Str(s) => { return s.startsWith("hello") }
Val.Num(n) => { return false }
}
}
fn main() {
var x = Val.Str("hello world")
match x {
Val.Str(s) => {
let bad = startsHello(x)
print($"{bad}")
}
Val.Num(n) => { print("num") }
}
}tests/errors/matchSubjectReadInArm.milo
value -1 is out of range
type Altitude = i32(0..50000)
fn main() {
let alt: Altitude = -1
}tests/errors/rangeUnderflowLiteral.milo
value 60000 is out of range
type Altitude = i32(0..50000)
fn main() {
let alt: Altitude = 60000
}tests/errors/rangeOverflowLiteral.milo
variable 'a' already declared in this scope
Two bindings in the same pattern with the same name hit the same-scope redeclaration branch of declare(), not the shadowing branch — must still point at the offending (second) binding site, not print bare.
enum Pair { Two(i32, i32) }
fn main(): i32 {
let p = Pair.Two(1, 2)
match p {
Pair.Two(a, a) => { return a }
}
}tests/errors/matchArmDuplicateBinding.milo
variant 'Some' has 1 fields, but pattern has 2 bindings
enum Maybe {
Some(i32),
None,
}
fn main(): i32 {
let x = Maybe.Some(42)
if let Maybe.Some(a, b) = x {
return 0
}
return 0
}tests/errors/ifLetWrongBindings.milo
was already moved out of it
Once a field has left, the struct is no longer the whole value. Passing it on anyway hands the next owner a zeroed field dressed up as real data — the same silent-empty-string result as re-moving the field, one level up.
struct Pair {
a: string,
b: string,
}
fn take(p: Pair): i64 {
return p.b.len()
}
fn main() {
let p = Pair {
a: "delta", b: "papa"
}
let stolen = p.a
print(stolen)
print(take(p))
}tests/errors/movePartiallyMovedStruct.milo
which implements Drop
Rust's E0509. A Drop impl takes '&mut self' and is written against every field being present, so a type with one cannot be taken apart field-wise. What made this urgent is what used to happen instead: codegen saw a partially moved local, skipped its drop glue entirely, and the destructor never ran at all — a silent resource leak with no diagnostic. Found by scripts/fuzz-ownership.ts (the 'drop-partial' spelling).
struct Res {
name: string,
}
impl Drop for Res {
fn drop(self: &mut Self) {
print("dropping " + self.name)
}
}
fn main() {
let r = Res {
name: "alpha"
}
let stolen = r.name
print(stolen)
}tests/errors/moveFieldOutOfDropType.milo
write 'self: &mut Self'
&mut self is the Rust receiver spelling. Milo's only receiver form is an ordinary typed parameter, so the parse error names the exact replacement instead of a stray '&'.
struct Point { x: i64 }
impl Point {
fn get(&mut self): i64 { return self.x }
}
pub fn main(): i32 {
return 0
}tests/errors/rustReceiverRefMut.milo
write 'self: &Self'
&self is the Rust receiver spelling. Milo's only receiver form is an ordinary typed parameter, so the parse error names the exact replacement instead of a stray '&'.
struct Point { x: i64 }
impl Point {
fn get(&self): i64 { return self.x }
}
pub fn main(): i32 {
return 0
}tests/errors/rustReceiverRef.milo
write 'self: Self'
self is the Rust receiver spelling. Milo's only receiver form is an ordinary typed parameter, so the parse error names the exact replacement instead of a stray ')'.
struct Point { x: i64 }
impl Point {
fn get(self): i64 { return self.x }
}
pub fn main(): i32 {
return 0
}tests/errors/rustReceiverBare.milo
writes the global 'G', which is being iterated here
The aliasing model is keyed to locals, params and self, so a global mutated inside a callee was invisible to it. The local form of this is already rejected (vecPushWhileIterating); the global form was a heap-use-after-free.
var G: Vec<i64> = Vec.new()
fn grow(): void {
G.push(99)
}
pub fn main(): i32 {
G.push(1)
for x in G {
grow()
print(x)
}
return 0
}tests/errors/globalIterCalleeGrows.milo
Same hole, reached by reassignment rather than realloc: dropping the old Vec frees the buffer the loop variable points into.
var G: Vec<i64> = Vec.new()
fn blow(): void {
G = Vec.new()
}
pub fn main(): i32 {
G.push(1)
for x in G {
blow()
print(x)
}
return 0
}