Skip to main content
v0.13.0

Metel Error Code Reference

All Metel errors carry a code. Codes are prefixed by phase:

PrefixPhase
PParse — invalid source text
TType — type-checker rejection
RRuntime — error during execution
IInternal — bug in the interpreter (please report)

Parse errors (P)

P0001 — Syntax error

The source text does not match the Metel grammar.

Fix: correct the syntax at the indicated position.

Tested by (18)

P0002 — Invalid integer literal

An integer literal is out of range for i64 (−9,223,372,036,854,775,808 to 9,223,372,036,854,775,807).

Fix: use a value that fits in i64, or split the computation.

P0003 — Invalid float literal

A float literal cannot be represented as an f64.

[P0003] parse error in main.mtl at 4..12: invalid float literal '1e9999'

Fix: use a value within the f64 range (~±1.8 × 10³⁰⁸).

Exempt from fixture coverage — untestable: Neither documented route is reachable: the grammar has no exponent notation, so a literal like 1e9999 is actually P0001, not P0003; and a literal long enough to overflow f64 in plain decimal notation silently saturates to infinity instead of erroring.

Type errors (T)

T0001 — Type mismatch, or an impl that is not allowed

Two types that must be equal are not.

Fix: ensure the expression produces the expected type. Add an explicit cast if widening (e.g. x as f64).

The same code also covers an extend block the language does not permit, which is a distinct situation sharing one code:

  • a target that cannot carry the impl at all — extend { … }: Drop on an anonymous record;
  • a target with nowhere to register, so its methods could never be found — a tuple, an anonymous record, a fun type, or an array whose element is not one of the impl's own type parameters. Only extend<T> T[]: Aspect — the array's element spelled exactly as one of the impl's own generics — is implemented today;
  • a drop body, while destructor invocation is not yet implemented.

Fix: each message names the way forward — usually a named struct, or the generic form where one exists.

Tested by (50)

T0002 — Annotation required

The type checker cannot infer a type without an explicit annotation.

Fix: annotate the binding: let x: i64 = ....

The same code also covers dereferencing (*expr) an operand that isn't a reference type at all — not an inference gap, but sharing the code with the annotation case above since both are "the checker has nothing to work with here":

Fix: remove the *, or check that the operand actually has reference type (&T / &var T).

T0003 — Undefined name

A name is used but not defined in the current scope.

Fix: define the variable or function before use, or correct the spelling.

Tested by (29)

T0004 — Arity mismatch

A function is called with the wrong number of arguments.

Fix: pass the exact number of arguments the function declares.

T0005 — Invalid operand types

An operator is applied to operands it does not support. Three forms share this code:

  • Mismatched operands. The two sides of a binary operator disagree, e.g. 1 == "x". The message names the operator and both types.
  • Binary arithmetic/ordering on unsupported types.
  • Equality (==, !=) on anything other than a numeric type, boolean, String or char. == does not yet dispatch through the Eq aspect, so structs, enums, arrays, tuples and references are rejected; use .eq(..) on a type that implements Eq.

Since v0.12.0: address-of (&, &var) applied to a non-addressable expression — a literal, a call result, a struct/enum construction — is no longer one of this code's cases. Both forms now get temporary lifetime extension instead of being rejected; see Expressions — References.

Fix: use compatible types, cast one operand, or bind the value to a name so it has an address.

T0006 — Assignment to immutable binding

A write operation targets a let binding. This covers three forms:

  • Direct reassignment: x = newValue
  • Field assignment through an immutable binding: point.x = 1
  • Taking a mutable reference to an immutable binding: &var x

Fix: change the binding declaration to var.

Tested by (4)

T0007 — Invalid cast

A as cast between incompatible types.

Fix: only cast between numeric types (i64 as f64). Use an explicit conversion function for other types.

T0008 — Non-exhaustive match

A match expression does not cover all possible values of the scrutinee type.

Fix: add the missing arms, or add a wildcard arm _ => ....

Tested by (2)

T0012 — Aspect bound not satisfied

A generic type parameter's bound is not satisfied by the concrete type at the call site or construction site. Covers both directions: a positive bound (T: Aspect) requires an implementation that isn't reachable, or a negative bound (T: !Aspect, RFC-0072) is violated because the concrete type does implement the aspect. Also covers a conditional extend block's own where-clause bounds (RFC-0036) failing at a use site — the same check as an ordinary function bound, just reached through an implementation block's condition instead of a function's generic parameter.

A type satisfying T: Copy automatically satisfies T: !Drop (RFC-0072 §2.3) even though it implements Drop — this is a narrow, Copy/Drop-specific exception, not a general rule.

Fix: implement the required aspect for the type, or (for a negative bound) remove the conflicting positive implementation.

Tested by (52)

T0013 — Ambiguous aspect method/associated-type resolution

Two different aspects define the same method name on the same receiver type, so a call like value.method() does not have a unique static target — or (RFC-0082 §3a) two different aspects bound on the same generic type parameter both declare an associated type of the same name, so a bare projection like T::AssocName doesn't have a unique target either.

Fix (method case): rename one of the methods, remove one of the conflicting impls, or change the design so the receiver type does not expose two indistinguishable aspect methods.

Fix (associated-type case): bind the associated type to a fresh type parameter via an equality-constrained bound instead of projecting it directly — e.g. fun f<T: Deref<Target = U> + Convert, U>(x: &T) -> U — which resolves unambiguously since U is an ordinary type parameter, not a projection.

Tested by (2)

T0014 — Orphan implementation

An extend Type: Aspect block where neither Aspect nor Type's outermost type constructor is declared in the current module (or std::core, for built-ins).

Fix: move the extend block into the module that declares the aspect or the type, or (for two foreign types) into std::core if this is genuinely a standard-library concern.

Tested by (5)

T0015 — Conflicting implementation

Two implementations of the same aspect cover the same concrete type — either two identical extend blocks, or a positive and a negative impl (see Negative Impls in the declarations reference) for the same concrete type.

Fix: remove the duplicate extend block, or narrow one block's type arguments so the two no longer overlap.

Tested by (4)

T0016 — Non-diverging -> ! function

A function declared -> ! (RFC-0078) contains a reachable path that doesn't diverge — most commonly an ordinary return <expr> where <expr> isn't itself !-typed. A -> ! function promises never to return; the compiler verifies every control-flow path ends in a diverging expression (a panic, a loop with no reachable break, or a return/tail expression whose own value is already !-typed).

Fix: make every path genuinely diverge (panic(msg), loop { }, or a recursive/other !-returning call), or drop the -> ! annotation if the function is meant to return normally.

T0017 — Missing associated type definition

An extend Type: Aspect block omits a type Name = ConcreteType; definition for an associated type the aspect declares (RFC-0082 §2). Every implementation of an aspect with associated types must define all of them.

Fix: add the missing type Item = ConcreteType; definition to the extend block.

T0018 — Naming the concrete type of an opaque return value

A function returning extends Aspect (RFC-0037) hides its concrete return type. Using the result in a position that pins it to a specific type — annotating it, or unifying it with a concrete type — defeats that, and is rejected.

Fix: keep the value opaque — annotate it as extends Aspect too, or accept it through a generic parameter with the same bound.

T0019 — Use of moved value

Since v0.12.0, under --move-check only. Move checking is off by default in this release.

An ownership rule from RFC-0071 §1/§7 was violated. Seven distinct situations share this code, each with its own message:

  • a value used after it was moved;
  • a partially moved value used as a whole;
  • a partial move out of a type that implements Drop, which is never allowed;
  • a move out of an array element, which is banned outright;
  • a move of a non-Copy element out of a borrowed T[] view;
  • a &var binding moved by a use that is not a reborrow;
  • a value moved out of a reference — by calling a by-value self method through it, in general assignment or by-value argument position, or by reading a field through it with no explicit * at all. A reference only grants access, never ownership, so its pointee cannot be moved out this way, unless the pointee's own type is Copy (in which case the read is a copy, exactly as T: Copy already permits at read-copy positions per §3a).

Each message names the binding and the location of the move. When the move happened on an earlier iteration of an enclosing loop, the message says so — a loop-carried move is usually the same expression as the use, one iteration later, so naming only its location would point back at the line you are already reading.

Fix: depending on the rule — borrow instead of moving (&x), clone the value, move the whole value rather than a field of a Drop type, or index-and-copy rather than moving an element out of an array.

Tested by (54)

T0021 — break/continue with no enclosing loop

break or continue appeared with no enclosing loop of any kind (loop, while, for, or for-in) to bind to. This includes a break/continue written inside a closure body — a closure is never considered to be "inside" whatever loop happens to lexically surround its definition, since the closure may be called long after that loop has exited, or from somewhere the loop never ran at all.

Fix: remove the keyword, or move it inside the loop it is meant to control. If it is meant to control a loop that encloses the call site of a closure rather than the closure's own definition, restructure the code — a closure cannot break or continue a loop it does not itself contain.

T0022 — extends Aspect outside parameter or return position

extends Aspect was written somewhere other than a function parameter's type or a function's return type — for example, a let/var annotation, a struct or enum variant field, a cast target (x as extends P), or a generic bound. Parameter position is lowered to a fresh bounded type parameter, and return position is RFC-0037's opaque return type; every other position is not part of this language version.

Fix: name a concrete type instead, or restructure the code so the aspect bound is expressed through a parameter or return type.

Tested by (2)

T0023 — Assignment through a non-owning view

An index assignment targets a T[] value. Since RFC-0126, T[] is an unconditionally Copy, non-owning view — it never grants write access through its indices, independent of whether the binding holding it is let or var. This is a different failure shape than T0006 (all three of T0006's forms are about a let binding that declaring it var would fix); no annotation or binding-mutability change can fix this one.

Fix: use [T; N] (a fixed-size array) or List<T> (a growable, owned collection) instead of T[] for storage that needs index-write access.

T0024 — Read-copy of a non-Copy value out of a reference

Since v0.12.1.

RFC-0067a §3a's "read-copy": a let/mut binding, return/break value, tail expression, or explicit ascription (expr: T) whose own declared type differs from its initializer's reference type (&U/&var U) implicitly copies the referent out — but only when U is Copy. A reference only grants access, never ownership, so reading a non-Copy value out this way would silently duplicate it with no move and no explicit clone.

Checked once against the fully-dereferenced type at the end of a reference chain, not each intermediate layer — let x: i64 = rr; where rr: &&i64 is unaffected, since i64 is Copy regardless of how many reference layers it's read through.

Fix: call .clone() if the type implements Clone, or restructure the code to take ownership of the value directly instead of reading it through a reference.

The closure cluster reserves the contiguous T0026–T0030 block, split below so each code's own coverage is visible rather than folded into one shared entry (an implementation gap in only one of the five would otherwise hide behind the other four).

T0026 — Capture list required, incomplete, or incompatible

Since: v0.13.0.

A capture list is required, incomplete, or uses an incompatible capture form.

Fix: use the capture list the closure body's captures actually require.

Tested by (2)

T0027 — Consuming capture without once

Since: v0.13.0.

A closure body consumes a capture but the literal/type is not once.

Fix: mark the closure once.

T0028 — Mutating capture without var

Since: v0.13.0.

A closure body mutates a capture, or uses [&var x], but is not var.

Fix: mark the closure var.

Tested by (2)

T0029 — var closure called through a shared reference

Since: v0.13.0.

A var closure is called through a shared reference.

Fix: call through an owned binding or an exclusive (&var) reference instead.

T0030 — Inner closure borrows an outer by-value capture

An inner closure borrows an enclosing closure's by-value capture.

Fix: restructure the code until RFC-0122 supplies the necessary borrow analysis.

Exempt from fixture coverage — blocked on RFC-0122: requires RFC-0122's borrow analysis, not yet implemented

Runtime errors (R)

R0001 — No main function defined

Execution requires a main function but none was found.

Fix: add fun main() { ... } to your program.

R0002 — main is not a valid entry point

main exists but is generic or is not a function.

Fix: main must be a concrete, non-generic function with no parameters.

Note: also raised for a generic closure invoked with no call-site type context, with a different message — this entry covers the main case only.

R0003 — Undefined variable at runtime

A variable name is not found in the current environment. This can occur when a variable is used before it is defined in a branch that the type-checker did not flag.

[R0003] runtime error in main.mtl at 10..15: undefined variable `x`

Exempt from fixture coverage — blocked on metel-core#986: Confirmed live raise sites (lvalue.rs, mod.rs). #986's follow-up round tried #712's exact precedent (nested-fun forward reference) across 5 more statement positions -- var initializer, if-branch, call-argument, match-arm, struct-literal-field, while-condition -- all resolved correctly, meaning #712's original let-initializer fix was thorough, not narrow. Still no repro found across either investigation round, and still not confirmed unreachable either.

R0004 — Index out of bounds

An array index is negative or ≥ the array length.

Fix: check that the index is within 0..array.len() before access.

R0005 — Tuple index out of bounds

A tuple element is accessed by an index that does not exist.

[R0005] runtime error in main.mtl at 5..10: tuple index 3 out of bounds

Fix: tuple indices are fixed at compile time; verify the index against the tuple's declared length.

Note: not confirmed reachable from ordinary source. A tuple index is always a literal token, never a computed expression, so an out-of-range index was caught as T0003 statically in every construction tried. Unlike P0003 above, the raise site is real code — just unconfirmed.

Exempt from fixture coverage — untestable: Checked and found unreachable via ordinary source -- tuple indices are fixed at compile time and out-of-range access is caught statically, not deferred to runtime.

R0006 — Non-exhaustive match at runtime

A match expression reached its end without any arm matching. This indicates a pattern that the type checker approved as exhaustive but that is not, which is a known limitation.

[R0006] runtime error in main.mtl at 2..30: match: no arm matched scrutinee

Exempt from fixture coverage — blocked on metel-core#986: A known limitation (the type checker approving a match as exhaustive when it is not). #986's follow-up round read check_match_exhaustiveness end to end (typechecker/construction/patterns.rs) -- the Boolean/Named-enum/Never/SizedArray cases, is_variant_uninhabited's RFC-0078 uninhabited-payload check, and pattern_covers_variant's enum+variant name matching all look sound on inspection. No construction attempted this round (unlike R0003/R0009) since no plausible gap surfaced worth testing against.

R0007 — Arithmetic error

Integer division or remainder by zero, or integer overflow on +, -, *, or / (RFC-0007 D3, amended 2026-08-26 — panics unconditionally in every build; there is no debug/release distinction). Floating-point arithmetic never raises this code — float overflow and division by zero follow IEEE 754 (inf/-inf/NaN), never a panic.

Fix: guard with a zero check before dividing, or ensure operands stay in range before an operation that could overflow.

R0008 — Field not found

A struct or enum value does not have the accessed field.

[R0008] runtime error in main.mtl at 5..12: no field `colour` on value

Fix: check the field name against the type definition.

Exempt from fixture coverage — blocked on metel-core#986: Confirmed live raise site, but every attempted repro (a generic function reading an unconstrained field) was caught statically as T0002 instead. #986's follow-up round: field access resolves directly against the accessed value's own concrete fields (lvalue.rs's TypedPlace::Field), not through a bare-name-keyed lookup table the way aspect methods do -- so R0009's newly-found collision bug (metel-core#989) doesn't obviously carry over here. No new construction attempted this round.

R0009 — Method not found

A method call cannot be resolved for the receiver type.

[R0009] runtime error in main.mtl at 5..20: no method `draw` on `Circle`

Fix: define the method in an extend block for the type.

Exempt from fixture coverage — blocked on metel-core#989: Confirmed live raise site. #986's follow-up round found a real root-cause bug in this exact dispatch machinery: metel-core#989, two same-named aspects in different modules corrupt each other's dispatch resolution (TypeRegistry::aspect_decl_modules is keyed by bare aspect name, not a qualified path). In the variant tried (colliding aspects with differently-named methods) this surfaced as a false static T0003 rejection, not a runtime R0009 -- construction-time method lookup apparently consults the same corrupted map before dispatch is ever elaborated. A same-method-name variant (also tried) resolved correctly in both import orderings tried, so this mechanism isn't yet confirmed to reach R0009 specifically -- revisit once #989 is fixed.

R0010 — Call on non-callable value

A call expression (f(...)) is applied to a value that is not a function or closure.

[R0010] runtime error in main.mtl at 3..8: call: expected a closure or builtin

Exempt from fixture coverage — blocked on metel-core#986: Confirmed live raise site, but calling a plain i64 variable was caught statically as T0001. #986's follow-up round: calling a value generically/dynamically has no route to try at all in v0.13.0 -- RFC-0161's dyn Callable / Callable aspect (the mechanism that would make a call target's callability depend on a runtime value rather than a static function type) is deferred in full to a later milestone, so there is currently no dynamic-dispatch angle to test against this code.

R0011 — Invalid for-in iterator

A for x in expr loop where expr does not evaluate to an Array, a Range, or a type implementing Iterable.

[R0011] runtime error in main.mtl at 1..20: for-in: expected Array or Range

Fix: ensure the iterable is an array literal, a range (a..b), a value of those types, or a type with its own Iterable implementation (see expressions.md, "for-in").

Exempt from fixture coverage — blocked on metel-core#986: Confirmed live raise sites (evaluator/mod.rs), but a plain non-iterable typed value (e.g. for (x in n) where n: i64) is caught statically as T0001 before reaching this runtime path. #986's follow-up round: the user-defined-Iterable dispatch this code guards resolves through the receiver value's own runtime type id (resolve_value_type_id + get_regular_method), not a bare-name-keyed table -- unlike R0009's aspect-method path (metel-core#989), this one isn't obviously vulnerable to the same class of collision bug. No construction attempted this round on that basis.

R0012 — Assertion failed

assert(cond) or assert(cond, msg) is called with cond evaluating to false. The panic message is the fixed string "assertion failed" for the one-argument form, or the caller-supplied msg for the two-argument form.

Fix: this is not a bug in the interpreter — it means the asserted condition was actually false at runtime. Fix the condition, or the code that led to it.

R0013 — Unwrap on None/Err

.yolo() is called on a Perhaps<T> that is None, or a Result<T, E> that is Err. For Result, the panic message includes the Err value's debug representation.

Fix: this is not a bug in the interpreter — .yolo() is meant only for cases where None/Err represents a logic error that should never occur in correct code. Use match, .unwrap_or, .unwrap_or_else, or (for Result) ? to handle the expected case instead.

R0014 — Explicit panic

panic(msg) (RFC-0078) is called. Always panics unconditionally with msg.

Fix: this is not a bug in the interpreter — panic is meant for logic errors that should never occur in correct code. Handle the expected case with ordinary control flow instead of reaching the panic call.

R0015 — Re-entrant mutating closure call

Since: v0.13.0.

A var closure tried to call the same closure value again before its current invocation finished. This is an uncatchable assertion-class runtime error.

Fix: restructure the callback/control flow so a mutating closure is not re-entered.

Internal errors (I)

I0001 — Internal interpreter error

The interpreter reached an impossible state. This is a bug in the interpreter — the typechecker should have caught it before execution.

[I0001] internal error: binop: unsupported operand types (typechecker should have caught this)

What to do: please file a bug report at the Metel issue tracker with the source program that triggered this error.

Exempt from fixture coverage — untestable: Forcing an internal-error state deliberately isn't meaningfully the same kind of check as an ordinary trigger -- a real repro would mean finding an actual interpreter bug, not demonstrating a language rule (not attempted).

I0002 — Not implemented

The program uses a language feature that is not yet supported in this version of the interpreter.

[I0002] internal error: generic functions are not supported in v0.1

What to do: check the changelog for the current supported feature set and the release plan for the planned implementation milestone.

Note: I0002 and its not_implemented() constructor are kept as scaffolding — the intended way to report a recognized but not-yet-built construct while a feature is under development. There is no live raise site today. metel-core#992 tracks removing the variant and constructor if they stay unused.

Exempt from fixture coverage — untestable: Kept as scaffolding for reporting a recognized-but-unimplemented construct during feature development; there is no live raise site today, so nothing to trigger. metel-core#992 tracks removal if it stays unused.