Skip to content

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))

? 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.

milo
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

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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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

milo
@!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.

milo
@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.

milo
@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.

milo
@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.

milo
@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

milo
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

milo
@mustUse(x)
fn f(): bool {
    return true
}

fn main(): i32 {
    let _ = f()
    return 0
}

tests/errors/mustUseArgsRejected.milo

'@pure' takes no arguments

milo
@pure(always)
fn square(x: i64): i64 {
    return x * x
}

pub fn main() {
    print(square(2))
}

tests/errors/pureWithArgs.milo

'@wrapping' on extern fn

milo
@wrapping
extern fn sqrt(x: f64): f64
pub fn main() {}

tests/errors/wrappingOnExtern.milo

'@wrapping' takes no arguments

milo
@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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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'

milo
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.

milo
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'").

milo
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.

milo
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.

milo
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.

milo
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.

milo
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)

milo
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

milo
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'

milo
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.

milo
@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.

milo
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.

milo
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

milo
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.

milo
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.

milo
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".

milo
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.

milo
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.

milo
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).

milo
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).

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.

milo
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.

milo
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.

milo
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.

milo
from "./lib/frameCore" import { main }

pub fn run(): i32 {
    return 0
}

tests/errors/importMainByName.milo

'main' must return i32 or void

milo
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>.

milo
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.

milo
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.

milo
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'

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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).

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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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)

milo
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.

milo
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

milo
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.

milo
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.

milo
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).

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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.)

milo
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.

milo
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)

milo
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.

milo
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)

milo
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)

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
@cLayout("struct timespec", "time.h")
extern struct T {
    tv_sec: i64,
    bogusField: i64,
}

fn main() {
    print(sizeOf<T>())
}

tests/errors/cLayoutNoSuchField.milo

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.

milo
@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.

milo
fn main() {
    var g: [[i64; 2]; 2] = [1, 2]
    print(g[0])
}

tests/errors/nestedFixedArray.milo

ambiguous From conversion

milo
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.

milo
fn main() {
    var a: [&string; 2] = ["x", "y"]
    print(a[0])
}

tests/errors/arrayOfRefs.milo

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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
fn main() {
    var x: [i64; 2] = ["a", "b"]
    print(x[0])
}

tests/errors/arrayLiteralElementType.milo

assert() condition must be bool, got i64

milo
fn main(): i32 {
    assert(42)
    return 0
}

tests/errors/assertBadArg.milo

assert() expects 1-2 arguments, got 3

milo
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.

milo
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).

milo
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 call

tests/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.

milo
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.

milo
@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.

milo
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.

milo
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.

milo
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.

milo
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

milo
extern type Opaque

fn main(): i32 {
    var x: Opaque = 0
    return 0
}

// @error: can only be used as a pointer

tests/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 ';'.

milo
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.

milo
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 borrowed

tests/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.

milo
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 borrowed

tests/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).

milo
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

milo
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 borrowed

tests/errors/borrowSliceMethod.milo

reassigning a var while a slice borrows it is a compile error

milo
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 borrowed

tests/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.

milo
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.

milo
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'

milo
fn main(): i32 {
    let x: i32 = 5
    x = 10
    return 0
}

tests/errors/assignToLet.milo

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.

milo
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.

milo
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

milo
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).

milo
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 borrowed

tests/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

milo
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 borrowed

tests/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.

milo
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

milo
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

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

milo
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.

milo
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 borrowed

tests/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

milo
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 borrowed

tests/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

milo
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 closure

tests/errors/viewCaptureClosure.milo

cannot carry a payload

A repr'd (C-like) enum is integer-valued; its variants can't hold data.

milo
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.

milo
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

milo
fn main(): i32 {
    let v: Vec<i64> = [1, 2, 3]
    v.clear()
    return 0
}

tests/errors/vecClearImmutable.milo

cannot cross the C ABI

milo
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.

milo
@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.

milo
@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.

milo
@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.

milo
@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.

milo
@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.

milo
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.

milo
@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 clone

tests/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.

milo
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.

milo
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.

milo
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.

milo
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'

milo
fn main() {
    let f = (x) => x + 1
}

tests/errors/closureInferNoHint.milo

cannot infer type parameter(s) 'T' for Option.None

milo
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.

milo
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

milo
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.

milo
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

milo
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 borrowed

tests/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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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

milo
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
}

tests/errors/moveInLoop.milo

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.

milo
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.

milo
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).

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
fn take(s: string) {
    print(s)
}

fn f(s: &string) {
    take(s)
}

fn main(): i32 {
    return 0
}
// @error: cannot move the borrowed value out of

tests/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.

milo
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

milo
from "nonexistent.milo" import {
    foo
}

fn main(): i32 {
    return 0
}

tests/errors/importMissing.milo

cannot open 'nonexistent.txt'

milo
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.

milo
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

milo
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.

milo
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.

milo
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.

milo
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.

milo
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).

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.

milo
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.

milo
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

milo
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).

milo
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

milo
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.

milo
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.

milo
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).

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

milo
fn getSlice(s: &string): &string {
    return s
}

fn main(): i32 {
    return 0
}

tests/errors/refReturn.milo

only a method may return a view; a free function has no receiver to freeze

milo
fn viewOf(v: &Vec<i64>): &[i64] { return v[0..v.len] }
fn main(): i32 { return 0 }
// @error: cannot return a reference

tests/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.

milo
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.

milo
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

milo
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

milo
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

milo
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'

tests/errors/viewOfLocal.milo

cannot send 'p' of type '*u8' across threads

milo
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.

milo
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

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

milo
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'

milo
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).

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.

milo
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.

milo
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.

milo
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

milo
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}")
}

tests/errors/vecPushRef.milo

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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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).

milo
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.

milo
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.

milo
@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.

milo
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.

milo
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)

milo
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 temporary

tests/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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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).

milo
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

milo
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.

milo
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.

milo
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

milo
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

milo
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 global

tests/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.

milo
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.

milo
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.

milo
enum Bad: i32 { TooLarge = 2147483648 }
pub fn main(): i32 { return 0 }

tests/errors/enumReprOutOfRange.milo

does not implement trait 'HasValue'

milo
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.

milo
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

milo
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

milo
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.

milo
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>Y

tests/errors/enumRecursiveThroughStruct.milo

enum 'List' has infinite size due to recursive field

milo
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.

milo
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.

milo
fn main(: i32 {
    return 0
}

tests/errors/parseExpectedToken.milo

expected *HandleA, got *HandleB

milo
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 *HandleB

tests/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.

milo
struct P{x:

tests/errors/truncatedStructFieldType.milo

expected an integer length

milo
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.

milo
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.

milo
@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

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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

milo
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.

milo
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.

milo
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.

milo
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.

milo
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>

milo
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.)

milo
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.

milo
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

milo
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.

milo
enum Bad<T>: i32 { A, B }
pub fn main(): i32 { return 0 }

tests/errors/enumReprGeneric.milo

is @pure but calls 'helper', which is not

milo
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'

milo
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'

milo
@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

milo
@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'

milo
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.

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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
@cSig("unistd.h", "int close(int)); void evil(void")
extern fn close(fd: i32): i32

fn main() {
    print(close(-1))
}

tests/errors/cSigBadSig.milo

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.

milo
@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.

milo
@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.

milo
@cValue("SEEK_END", "stdio.h")
var SEEK_END: i64 = 2

fn main() {
    print(SEEK_END)
}

tests/errors/cValueOnVar.milo

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.

milo
@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.

milo
@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.

milo
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.

milo
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.

milo
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

milo
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.

milo
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.

milo
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.

milo
@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.

milo
@cSig("unistd.h", "int sysconf(int)")
extern fn sysconf(name: i32): i64

fn main() {
    print(sysconf(29))
}

tests/errors/cSigWrongC.milo

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.

milo
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

milo
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.

milo
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.

milo
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

milo
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.

milo
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.

milo
@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.

milo
@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.

milo
@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 '+'.

milo
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.

milo
@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.

milo
@cSig("string.h", "size_t strlen(const char *)")
extern fn strlen(s: *u32): u64

fn main() {
    print(1)
}

tests/errors/cSigParamPointee.milo

mismatched types

milo
fn main(): i32 {
    let x = if true {
        10
    } else {
        "hello"
    }
    return 0
}

tests/errors/if_expr_mismatch.milo

missing required method 'eq'

milo
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.

milo
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.

milo
@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.

milo
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

milo
@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.

milo
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.

milo
@ 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.

milo
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

milo
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.

milo
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.)

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

milo
fn f(n: i64): i64
requires old(n) >= 0
{
    return n
}

fn main() {
    print(f(1))
}

tests/errors/oldOutsideEnsures.milo

old() takes a scalar

milo
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.

milo
@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.

milo
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'

milo
@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.

milo
@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 '@'.

milo
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.

milo
@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.

milo
pub fn main(): i32 {
    let t = "a,b"
    let _parts = t.splitView(",")
    return 0
}
// @error: only valid as the iterable of a 'for ... in' loop

tests/errors/stringViewIterStored.milo

Option

pop() returns Option<T>, not T — using it as a bare i32 must fail. Force the value out with !, ?, or ??.

milo
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.

milo
@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).

milo
fn setPercent(p: i32(0..100)): void { print(p) }
fn main() {
  setPercent(500)  // @error: out of range
}

tests/errors/rangeArg.milo

Reassigning an out-of-range literal to a ranged var is rejected (was unchecked).

milo
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).

milo
fn get(): i32(0..100) {
  return 300  // @error: out of range
}
fn main() { print(get()) }

tests/errors/rangeReturn.milo

overflows u8

A coerced if-arm still range-checks each literal against the target width.

milo
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

milo
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

milo
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' block

tests/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.

milo
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.

milo
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

milo
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

milo
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.

milo
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

milo
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.

milo
fn main() {
    var toks: Vec<&[u8]> = Vec.new()
    print($"{toks.len}")
}

tests/errors/vecOfRefsAnnotated.milo

references cannot be stored in structs

milo
struct MyView {
    data: &string
}

fn main(): i32 {
    return 0
}

tests/errors/refInStruct.milo

requires an unsafe block

milo
extern fn malloc(size: i64): *u8

fn main(): i32 {
    let p = malloc(64)
    return 0
}

// @error: requires an unsafe block

tests/errors/unsafeExternPtrReturn.milo

requires clause 'lo <= hi' violated

milo
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

milo
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

milo
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

milo
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'

milo
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

milo
fn main(): i32 {
    let a = "hello"
    let b = "world"
    let c = a - b
    return 0
}

tests/errors/stringArith.milo

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.)

milo
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.

milo
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.

milo
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.

milo
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'

milo
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.

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.

milo
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.

milo
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.

milo
@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.

milo
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.

milo
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

milo
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 overlap

tests/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.

milo
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.

milo
@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.

milo
fn grow(s: &[i64]) {
    s.push(1)
}

fn main() {
    var v: Vec<i64> = Vec.new()
    v.push(1)
    grow(v)
    print(v.len)
}

tests/errors/slicePush.milo

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.

milo
@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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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

milo
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
}

tests/errors/sendNotSend.milo

type mismatch

milo
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

milo
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

milo
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).

milo
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.

milo
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.

milo
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.

milo
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'

milo
fn main(): i32 {
    foo()
    return 0
}

tests/errors/undefinedFn.milo

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.

milo
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'

milo
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.

milo
@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'

milo
@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.

milo
fn main() {
    print("\q")
}

tests/errors/unknownEscape.milo

unknown module directive '@!foo'

milo
@!foo
pub fn main() {}

tests/errors/unknownModuleDirective.milo

unreachable code

milo
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

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).

milo
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

milo
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.

milo
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.

milo
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.

milo
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

milo
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.

milo
@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.

milo
@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.

milo
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

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.

milo
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.

milo
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

milo
fn main(): i32 {
    let a = "owned string"
    let b = a
    print(a)
    print(b)
    return 0
}

tests/errors/useAfterMoveLetBinding.milo

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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
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.

milo
fn main() {
    var s = "heap"
    s.pushStr("-buffer")
    forget(s)
    print(s)
}

tests/errors/forgetEndsOwnership.milo

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

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'

milo
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.

milo
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.

milo
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.

milo
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

milo
type Altitude = i32(0..50000)

fn main() {
    let alt: Altitude = -1
}

tests/errors/rangeUnderflowLiteral.milo

value 60000 is out of range

milo
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.

milo
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

milo
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.

milo
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).

milo
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 '&'.

milo
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 '&'.

milo
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 ')'.

milo
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.

milo
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.

milo
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
}

tests/errors/globalIterCalleeReplaces.milo