Changelog
v0.12.0
Released 2026-08-03. The spec's Since v0.12.0 / Changed in v0.12.0 markers
refer to this entry.
Anonymous records:
- Closed, anonymous, exact-shape, structurally typed product types:
{ x: f64, y: f64 }as a type,{ x = 1.0, y = 2.0 }as a value, andHandle.{ fd }to project a nominal type's row. - Structural identity is order-insensitive —
{ x: i64, y: i64 }and{ y: i64, x: i64 }are the same type. - Records are exact: unification requires the same label set, and there is no width subtyping. A record with an extra field is a different type, not a subtype.
- Duplicate labels are a parse error.
- A bare
{ x }or{ x = e }where a block may appear is still a block. Write a record there in parentheses —({ x = e }); the diagnostic says so at the point of use. - Records satisfy
Send/Syncby field composition, but carry no impl-based aspect. Inherent methods, non-local aspect impls, and a customDropon a record are rejected. - Not yet available: chained and pattern projection, narrowing, record conversions, named records, and open rows.
Row bounds:
-
A bound may be a bare row, constraining a type parameter by the fields it carries rather than by an aspect:
fun magnitude<record T: { x: f64, y: f64, .. }>(p: T) -> f64fun labels<record T>(x: T) -> Symbol[]fun f<record T: { x, y: f64, .. }>(p: T)fun h<T>(p: T) where record T: { x: f64, .. }fun send<record T: !{ token }>(t: T) -> i64 -
Only records satisfy a row bound. A struct never does, and the diagnostic says why.
-
A closed row (no
..) requires exactly that label set; an open row requires at least it. -
A field may omit its type to constrain the label alone. There is no
_wildcard. -
Negation is per listed field, not the complement of the whole row.
!{ x: f64 }is satisfied by a record whosexis ani64;!{ x }rejects the label outright. -
A row bound on a parameter that is
record-kinded in neither the parameter list nor thewhereclause is an error that names the fix.
Ownership — partially available, and off by default:
-
The
CopyandDropaspects are declared in the standard library.Copyis implemented for the twelve numeric primitives plusbooleanandChar. -
Structural rules: a tuple is
Copyiff every element is, a fixed array iff its element type is,&TisCopy, and&var Tis not. -
A struct may implement
Copyonly if every field is; an enum only if every payload in every variant is. The diagnostic names the offending field or payload. -
CopyandDropare mutually exclusive, rejected in either declaration order and also when two overlapping conditional impls would give one instantiation both. -
Move checking is available but off by default. Pass
--move-checkto enable it. Nothing in the language moves without that flag; the default remains copy-on-assign.With it enabled:
- Loop bodies are analysed to a fixed point, so a move inside a loop is visible to the
next iteration. The diagnostic says which iteration it means:
`s` was moved here on an earlier iteration. - A move on a path leaving through
break,continue, orreturnno longer reaches the code that follows it — removing a class of false positives, in and out of loops. - Writing to a moved place makes it valid again rather than counting as a use of it, so
let moved = s; s = "again";is accepted. Assigning a field works the same way; a write whose base is gone is still an error. - A dereference is a place:
*pand(*p).fcan be named, and moving the same value out of a reference twice is caught rather than ignored. - Generic function bodies, generic impl methods, and named let-polymorphic closures are analysed rather than skipped. Bodies that still cannot be analysed produce a warning naming the reason instead of failing silently.
- A by-value
selfmethod is rejected when called through a reference, and a&var selfmethod is rejected through a shared reference, in every receiver form. A reference grants access, never ownership.&selfmethods and owned receivers are unaffected, as is aCopypointee. - Consuming a non-
Copyelement out of a borrowedT[]is rejected.
- Loop bodies are analysed to a fixed point, so a move inside a loop is visible to the
next iteration. The diagnostic says which iteration it means:
-
Known limitation: closures are not tracked as owners. Every function type is treated as
Copy, so a closure that captures a non-Copyvalue can be reused freely, and calling one never consumes what it captured:fun call(f: () -> String) -> String { f() }let s = "hello";let f = () -> String { s }; // captures a non-Copy valuelet a = call(f);let b = call(f); // accepted, though `f` is once-callableTreat
--move-checkin this release as checking ownership of values, not of closures. This is a checker gap, not memory unsafety — the runtime deep-clones a closure's environment at creation, so both calls produce a value. It is the reason move checking is not yet the default. -
The
Dropaspect is declared, but destructors do not run in this release, and writing adropbody is therefore rejected rather than compiling into a destructor that never fires:extend Handle: Drop {fun drop(self) { close(self.fd); } // rejected: this cleanup would never happen}extend Handle: Drop {fun drop(self) {} // fine — declares Drop, promises nothing}Declaring a type
Dropstill works and everything it means at the type level is available: theCopy/Dropexclusion, the eligibility rules,T: DropandT: !Dropbounds, the ban onDropfor anonymous records, and the refusal to partially move aDropvalue. Only invocation is missing. If you have cleanup to run today, put it in an ordinary method and call it. This applies tostd::core::Dropspecifically — a module's own unrelatedDropaspect is unaffected. -
extendon a concrete structural target is now a diagnostic rather than an internal error.extend i64[]: Area { … },extend (i64, i64): Area { … }andextend { w: i64 }: Area { … }report the target kind and the form that works:cannot `extend` a tuple type without type parameters: only the generic form isimplemented, so this block's methods could never be found. Write it as`extend<A, B> (A, B): Aspect { … }`, or use a named structA tuple or record target in the generic form is rejected too — it typechecked and then was invisible to both dispatch and bound satisfaction. Of the structural targets, the generic array form is the one that works:
extend<T> T[]: Display { … }registers and dispatches as before. -
Auto-deref now reaches through a reference in two places it previously missed: an array intrinsic resolves through
&T[]and&[T; N](arr.len()wherearr: &i64[]needed(*arr).len()before), and an aspect method resolves through&Tunder aT: Aspectbound, so a read-only generic can borrow its parameter and still call methods on it. -
impl Aspectis now lowered wherever it appears in a parameter annotation, not only as the annotation's outermost type.impl Printable[]previously bound the array type rather than its element, making the bound vacuous. Nested occurrences in generic arguments, tuples, records, references, function types, and associated-type projections are lowered too.
Arrays:
T[]is now a non-owning, immutable, unconditionally-Copyborrowed view. Produced only by borrowing aList<T>, a[T; N], or another slice; array literals with no expected type now default to[T; N]. The existing[T; N]→T[]coercion means most code needs no change — it only differs at a genuinely unannotated literal.List<T>gains.set(i, value) -> Perhaps<T>, overwriting in place and returning the replaced value, orNoneif out of bounds.
References:
&and&var <rvalue>no longer require binding the value to a name first (foo(&Vec::new()),foo(&var Vec::new())). A literal, call result, or construction is materialized into a fresh, independent cell and referenced directly. Nothing outside the expression can alias that cell, so a mutable reference to it is always sound.
Breaking changes:
-
Field initializers use
=, not:—Point { x = 1.0, y = 2.0 }. This completes the rule that:classifies and=defines. Field declarations (message: String), enum variant declarations, and patterns are unchanged and still use:.Migration is mechanical but must not be done with a regex. Declarations, patterns and literals share brace syntax and co-occur on one line —
Perhaps::Some { value } => Perhaps::Some { value = f(value) }has a pattern and a literal in one expression, and only the literal changes. Rewrite over parsed field-initializer spans. -
a[0] = 9through aT[]no longer compiles. Mutate viaList<T>or[T; N]. -
recordis now a keyword and can no longer be used as an identifier. -
StringandList<T>read-only methods now take&selfinstead ofself—String's entire method surface, andList<T>'sget/len/as_slice/map/filter/fold/find/concat. Only observable under--move-check, where calling two such methods on one binding previously moved it on the first call.
Diagnostics:
- New error code T0019, reported
only under
--move-check, with distinct wording per rule rather than one generic message: use after move, a partially moved value used as a whole, a partial move of aDroptype, a banned array-element move, a&varmoved by a use that is not a reborrow, and a move out of a reference. Each names the binding and the location of the move. - Taking a
&varreference through a shared reference reportsT0006in every lvalue form —&var r.fieldas well as&var *r— where one form previously reported a non-exhaustive-match code. - A malformed record projection is diagnosed directly, instead of against a synthesised type name that appears nowhere in the program.
- A generic field type in a
Copyeligibility error is rendered as written —Inner<T>— rather than leaking the inference variable behind it (Inner<?t16>).
Fixes:
- Undeclared type and aspect names in annotations and bounds are rejected at their
declaration, including unused function signatures, unused struct/enum fields, and
extendclauses. Diagnostics name the unresolved type or aspect instead of blaming a later unification failure. - A record no longer satisfies an aspect bound vacuously; it must actually meet it.
..on a negative row bound is rejected, since "at least these fields, negated" has no coherent reading.Copyeligibility now sees conditional impls on generic field types, soextend<T: Copy> Outer<T>: Copyis accepted whenOuter's field is anInner<T>that is itself conditionallyCopy.return,break, andcontinuenested inside an ordinary expression position no longer crash the interpreter.1 + (return 7),f(return 7),[return 7, 1], a struct-literal field, amatchscrutinee, anifcondition and aletinitializer each aborted the process; the signal now propagates to the construct that owns it.- Taking a reference to a field reached through a reference now works. Both
&r.aand&(*r).a, and the tuple, array, nested, and&varequivalents, previously raised an internal error. Reading such a field always worked — only taking a reference to one did not. Cloneexists as an aspect but has essentially no standard-library impls, soT: Cloneis not yet usable as a bound for a primitive orString.
v0.11.0
Released 2026-07-24. The spec's Since v0.11.0 / Changed in v0.11.0 markers
refer to this entry.
Enum variants:
- A match arm may name a variant without its
Enum::prefix when the scrutinee's enum determines it:match c { Red => .., Green => .. }. Resolution is type-directed against the scrutinee's own type — not a lexical import — so two enums may both declareRedwith no ambiguity. - The same applies in expression position, against the expected type:
let c: Colour = Red;,paint(Blue),fun favourite() -> Colour { Green }. None,Some,OkandErrare ordinary variants, not literals. They have no special status in the grammar or the type system and resolve exactly as a user-declared variant does. Qualified forms remain valid everywhere.- A bare variant is a last resort: an in-scope binding wins, and so does a same-named unit struct. Where no expected type exists the bare form does not resolve — there is deliberately no search for "some enum, somewhere".
References:
- Explicit
*exprreturns, for reading through a reference and, as an assignment target, for writing through a&var T. This reverses v0.10.0's removal of explicit dereference syntax. - Auto-deref is now confined to selectors — field access, field assignment,
indexing, and method dispatch. Call arguments and operator operands are spelled
explicitly:
add(*p, *q),*p + *q. - Matching a
&T/&var Tscrutinee matches against the referent's own patterns. &*pis a reborrow that shares the referent's storage; reborrowing a&var Tas&Tdowngrades to shared.- Index-path write-through works through a reference:
xs[0] = 9forxs: &var i64[]. - Tuple elements are assignable —
t.0 = v,t.0 += v, and nested and chained forms — including through a&varreference.
Breaking changes:
- Assignment to a reference-typed binding now rebinds it, like every other
type.
*p = vis the spelling that writes through. Previously a barep = vwrote through the reference, which made repointing a&var Tunrepresentable. Migration is mechanical:p = vbecomes*p = v. - Write-through takes one
*per reference layer. The previous rule peeled every layer at once, sopp = 5on a&var &var i64wrote the innermost value; it is now**pp = 5. In exchange,*pp = &var mrepoints the inner reference, which the old rule could not express. &applied to a field or element now aliases the original storage instead of snapshotting a copy, so later writes are visible through it. It remains read-only.
Diagnostics:
==and!=on operand types the evaluator cannot compare — references, structs, enums, arrays, tuples, unit — are rejected at compile time (T0005) instead of aborting at run time with an internal error.- A binary operator whose operands disagree now names the operator:
operator `==` cannot be applied to an integer literal and `String`rather than a barecannot unify. - Address-of a non-addressable place — a literal, a call result, a struct or enum construction — is a compile-time error with a span, not a runtime internal error. The rule was always static-determinable.
&var *ron a shared reference is a compile-time error rather than a runtime failure.- Assigning to a tuple element out of range, or through an immutable binding, reports a type error instead of an internal error.
Fixes:
- A closure with no declared return type is no longer typed
()at the call site. Pass 1 inferred it correctly and pass 2 discarded it;let f = () -> { 42 }; let n = f(); n + 1failed. - Type-directed read-copy decides whether to peel against the substituted type,
so
let n: i64 = g();works forfun g() -> &i64— previously only the syntactically-a-reference forms did, and a call returning&Tdid not. - Generic bodies constructed at call time use the argument and receiver types
recorded at the call site, refined over the runtime-derived ones. An empty
collection has no element to sample, and the resulting
Nevercoerced without ever pinning a type parameter, so[].eq(&[])failed with an error pointing insidestd::core. - A bare variant that can never resolve is reported rather than silently accepted.
Dispatch and bounds:
- Two aspects may register a same-named method against the same generic or structural
target —
T[],Wrapper<T>— without silently aliasing. The single-slot registries previously kept whichever impl was registered last, regardless of which one's bounds the concrete instantiation actually satisfied, so calls could dispatch to the wrong impl. Affects nominal generic structs identically, not only arrays. - A generic struct or array implementing
Iterable<T>generically —extend<T> Wrapper<T>: Iterable<T>, rather than a concreteextend Counter: Iterable<i64>— now derives itsfor-in element type correctly. Two separate paths were wrong: inference read the registry's recorded type arguments, which for a still-generic impl are the impl's own parameter names rather than types, and construction searched only the concrete method environment, never the polymorphic one. - An associated type's declared bound is registered on its projection. An aspect may
write
type Item: Display;, but the placeholder minted forSelf::Itemnever carried that bound, so chaining directly onto a projection result —c.get().to_string()— failed with a spurious "cannot infer receiver type" (T0002).
v0.10.0
Released 2026-07-17.
Language surface:
public,var, andextendare now the canonical spellings. The oldpub,mut, andimpldeclaration spellings are removed.- Empty aspect declarations may be written as
aspect Name;. - Bodyless positive and negative aspect implementations are accepted:
extend Type: Aspect;andextend Type: !Aspect;. - Zero-field structs and zero-field enum variants may be constructed with or
without braces:
Empty/Empty {}andFlag::On/Flag::On {}. return,break, andcontinueare expressions of type!, so they work in bracelessifarms, match arms, loop tails, and other expression positions.
References and control flow:
- Reference types are now spelled
&Tand&var T. - Explicit dereference syntax is gone; field access, method calls, function calls through references, type-directed reads, and write-through assignment handle ordinary reference use.
- Reference operations chain through multiple layers such as
&&Tand&&var T. - The bottom type
!is user-writable, coerces to any type, participates in exhaustiveness for uninhabited enum variants, and is checked for-> !functions.
Aspect and type system:
- Conditional aspect implementations are enforced, including
whereclauses and negative bounds. - Aspect implementation coherence is enforced with orphan-rule and overlap checks.
- Negative bounds (
T: !Aspect) and negative implementations (extend Type: !Aspect;) participate in bound checking and coherence. - Associated types are supported in aspects and implementations, including
projections such as
T::AssocTypeand equality-constrained bounds. - Return-position
impl Aspectis supported as an opaque static return type. - Structural aspect implementations over built-in constructors such as arrays participate in aspect-bound satisfaction.
- Bare-parameter blanket implementations such as
extend<T> T: Aspectare allowed only when the aspect is local to the declaring module. - Coherence's disjoint-negation overlap check now recognizes structural targets (arrays, tuples, function types) the same way it already did for named types: two conditional implementations for the same structural target, distinguished only by a positive versus negative bound on the same type parameter, no longer incorrectly conflict.
Standard library:
PerhapsandResultgain.yolo().Perhapsgains.ok_or(error).Resultgains.map_err(f)and.ok().
Breaking changes:
- Replace
pubwithpublic. - Replace
mutbindings withvarbindings. - Replace
implblocks withextendblocks. - Replace
*T/*mut Twith&T/&var T. - Remove explicit
*pdereference syntax.
Fixes and cleanup:
- Generic method bodies now recover the receiver's own type parameters when reconstructing method dispatch.
- Zero-argument generic calls can use the caller's expected type when arguments alone do not determine all type parameters.
- Aspect dispatch, import resolution, and the runtime type registry now use stable symbol identities, avoiding same-name collisions across modules.
- Generic bounds are preserved when a type variable is aliased to another type variable during inference, instead of being silently dropped.
- The RFC/process documentation was reorganized around the current public docs and implementation state.
v0.9.1
Bug fixes.
Fixes:
printandprintlnnow print any value whose type implementsDisplay, dispatching the user'sto_string— previously a struct or enum with aDisplayimplementation typechecked but panicked at runtime, and had to be printed via an explicit.to_string()- A tuple type now accepts an array suffix:
(T, U)[]parses and typechecks in return, parameter, local-annotation, and struct-field positions (and a tuple is accepted as a generic type argument, e.g.List<(String, String)>)
Testing and tooling:
- The integration harness now runs evaluator and typechecking fixtures through the same full module pipeline as the shipped binary, eliminating the old single-program shortcut that drifted from real
std::corebehavior
v0.9.0
The first presentable standard library.
Language:
- Methods on generic types now work end-to-end. Generic structs and generic enums can carry methods with their own type parameters (
fun map<U>(self, f: (T) -> U) -> Box<U>), closures, andmatch self, and they dispatch correctly across module boundaries. This unblocks the standard library's methods onPerhaps,Result, andList
Standard library — std::core (auto-imported):
Perhaps<T>combinators:is_some,is_none,map,and_then,unwrap_or,unwrap_or_elseResult<T, E>combinators:is_ok,is_err,map,and_then,unwrap_or,unwrap_or_elseList<T>ergonomics:map,filter,fold,find,concat(in addition tonew/from/push/pop/len/get/as_slice)Stringutilities:is_empty,to_upper,to_lower,trim,trim_start,trim_end,contains,starts_with,ends_with,index_of,split,replace,repeat,chars,char_at,substring, and the associatedString::join. Index-based operations count Unicode scalars and are total (out-of-range clamps or returnsNone)OsError— the error type for the host modules, with aDisplayimplementation and amessage()accessor
Standard library — host modules (explicit import):
std::env—get(name) -> Perhaps<String>,vars() -> EnvVar[](read-only)std::fs— text-oriented file operations (read_to_string,write_string,append_string,exists,read_dir,create_dir,create_dir_all,remove_file,remove_dir,remove_dir_all), all returningResult<_, OsError>std::process—args()and shell-free synchronousrun(command, args) -> Result<ProcessOutput, OsError>
Known gaps (tracked):
std::mathand the comparison-dependentListmethods (sort,contains) await a forthcomingOrd/Eqaspect
(The print/println Display limitation and the tuple array-suffix parse gap noted here at release were fixed in v0.9.1.)
v0.8.3
Standard library expansion and module system clarifications.
New language features:
- Function overloading — a module may declare multiple free functions with the same name, distinguished by parameter types. Resolution is exact-match only: argument types must equal a candidate's parameter types exactly, with no implicit numeric coercion participating in selection (bare numeric literals default before selection, so
f(42)picks ani64overload). Overloaded functions must be non-generic with every parameter annotated; calls with no matching candidate list all available signatures in the error - Aspects can now be implemented for primitive types —
extend i64: Display { … }and the like typecheck and run; the standard library'sDisplayandFromimplementations for the primitives are declared this way nativedeclaration syntax for stdlib-only host-backed implementations — free functions, methods, and aspect methods can be markednativewith an explicit binding key. Reserved for the standard library; using it in user code is a compile error
Standard library (breaking):
print/printlnnow requireDisplayat compile time — passing a type with noDisplayimplementation is a type error (T0012) instead of a runtime panic- A module's function overloads extend, rather than replace, a same-named standard-library function: if no overload matches exactly, the call falls back to the outer binding (e.g. overloading
printfor specific types keeps the genericprintreachable for everything else) assertis now overloaded:assert(cond)andassert(cond, msg). The separateassert_msgfunction is removed — replaceassert_msg(c, m)withassert(c, m)string_len(s)is removed in favour of alenmethod onString: uses.len()string_concat(a, b)is removed — use the+operator:a + b
Module system:
- Using
stdas a top-level module name is now a compile error. A file atstd.mtlor anywhere understd/in the project tree produces:error: module path std::… is reserved for the standard library. Thestdkeyword was already reserved in the language syntax; the interpreter now enforces the same reservation at the module path level.
Internal improvements:
std::coreis now a real module compiled into the interpreter binary and checked through the normal module pipeline, rather than a set of hand-registered builtins — the entire core surface (Perhaps,Result,Display/From/Iterable,List<T>,print/println/assert/…) is declared in standard library source. No user-visible behaviour change;import std::core::…works as before- Overloaded calls dispatch by stable symbol identity rather than by name throughout the pipeline
- Symbol definition index — every declared symbol now has a stable definition site recorded during name resolution; used by diagnostics and future tooling
- Error span accessor — all error variants that carry source location now expose it through a uniform interface
v0.8.2
Generic function recursion and forward-reference fix.
Bug fixes:
- Generic self-recursion now type-checks correctly; a generic function can refer to itself inside its own body without triggering
T0003 undefined name - Generic forward references now work the same way as monomorphic forward references; a generic function can call a later generic function declared in the same scope
- Mutual recursion across generic functions now type-checks and evaluates correctly; the pre-inference hoist pass now registers generic function schemes before any body is inferred
Performance improvements:
- Incremental constraint solving —
InferContext::solve()now caches the solved substitution for the append-only prefix of the constraint list instead of re-solving the full set on every eager partial solve. This removes the dominant0.8.2baseline bottleneck in generic-heavy programs - Typechecker sub-phase profiling — the benchmark harness now reports registry, inference, solve, scheme-environment, construction, and finalize timings so optimization work can target the real hot paths rather than evaluator guesses
- Benchmark/profiling workflow —
metel-benchnow benchmarks evaluator integration fixtures through the same parse → typecheck → evaluate path used by the test suite and emits machine-readable summaries plus call-graph artifacts - Measured impact on the release benchmark suite — representative total runtime improvements from the original
0.8.2baseline:int_04_generic_algorithms.mtl:1662.887 ms→160.724 msint_01_statistics.mtl:675.241 ms→87.376 msint_03_generic_option_chain.mtl:431.502 ms→76.467 msint_05_generic_data_pipeline.mtl:357.644 ms→66.298 msint_11_generic_sized.mtl:157.804 ms→27.107 ms
Internal improvements:
hoist_fun_declsnow pre-registers generic function schemes and their aspect bounds, so generic visibility follows the same pre-pass architecture as monomorphic recursion instead of relying on per-function provisional bindings- Regression coverage added for generic self-recursion and generic mutual recursion in both the typechecking and evaluator integration suites
v0.8.1
Post-inference elaboration pipeline. No new language surface.
Internal (interpreter architecture):
- Elaboration pass — a dedicated
elaboratorstage runs between the typechecker and evaluator and resolves everyMethodDispatchcall site toInherentorAspect { aspect_id }before evaluation begins. The evaluator now acceptsElaboratedModuleGraph(a newtype proof that elaboration has run) instead ofTypedModuleGraphdirectly. - SymbolId infrastructure — every top-level declaration is assigned a stable
SymbolIdby the name resolver at declaration site, and every import binding carries the sameSymbolId. Builtin types and aspects have reserved IDs (1–99); user-defined symbols start at 1000. - SymbolId-keyed aspect dispatch —
RuntimeAspectImplcarriesaspect_id: Option<SymbolId>alongside its string name.RuntimeRegistry::get_aspect_method_by_idmatches onaspect_idfirst, eliminating cross-module name collisions where two unrelated aspects share a method name. - Ambiguous same-type aspect methods rejected — if two distinct aspects define the same method name on the same receiver type, elaboration now rejects the call with
T0013instead of silently picking one impl by traversal order. - Environment documentation —
TypeDefinitionRegistryis annotated with its elaboration interface;ElaboratedModuleGraphcarries a responsibilities table;architecture.md,typechecker.md, andevaluator.mdare updated to reflect the new stage. - Regression suite — four new full-pipeline fixtures cover: polymorphic calls across modules, cross-module aspect dispatch, two aspects with the same method name on different receiver types, and inherent/aspect method coexistence.
v0.8.0
Sized numeric types, Char, List<T>, fixed-size arrays, turbofish, and fat-pointer &var.
New language features:
- Sized numeric types —
i8,i16,i32,i64,u8,u16,u32,u64,f32,f64. Sized literal suffixes:42i32,3.14f32,255u8. All casts between sized types are explicit (as). Array indices must beu64. - Polymorphic numeric literals — unsuffixed integer and float literals unify with whatever numeric type the context demands (let annotation, function parameter, struct field, return type, or the other operand in a binary expression). Without context they default to
i64/f64.mutreassignment (m = 99wherem: i32) also propagates the declared type to the literal. Negative minimum literals (-128i8,-32768i16,-2147483648i32) are accepted at the lexer level. - Cross-sized numeric
Fromimpls — all 90 pairwise casts among the 10 numeric types are supported viaas(i8 as u32,f32 as i64, etc.). Previously onlyi64 ↔ f64was supported. Chartype — Unicode scalar value; single-quoted literals ('a','\u{1F600}');u32::from(c)andChar::from(n)conversions; implementsDisplayList<T>— standard growable-sequence type instd::core; replaces ad-hocarray_pushusage; methods:new,from,push,pop,len,get,as_slice- Fixed-size array type
[T; N]— compile-time-known length; repeat construction[v; N]; coerces toT[];.len()method; array patterns on[T; N] - Turbofish — explicit type arguments at call sites:
f::<T>(args),zip::<A, B>(as, bs) &varfor lvalue paths —&var obj.field,&var arr[i], and chains thereof produce a*mut Tthat writes back to the original storage location
Bug fixes:
- Generic functions with multiple independent type parameters (e.g.
fold_left<T, A>) no longer have their type parameters collapsed when a module-level constraint solve follows a single-parameter generic function - Same-tier glob import conflicts (
import a::*andimport b::*both exporting the same name) no longer raise an error at import resolution; the error fires at the first use site of the ambiguous name &var xon a non-varbinding is now a type error (T0006); previously accepted silently, allowing immutable bindings to be mutated through a pointer- Field assignment (
p.field = v) on a non-varbinding is now a type error (T0006); previously the field mutability check was missing, allowing struct fields to be mutated through an immutable binding
Breaking changes:
array_pushandarray_lenare removed as top-level built-in functions; useList<T>for mutation and.len()on arrays and lists- Code that previously relied on
&var xorp.field = vwith a non-varbinding will now fail typechecking
v0.7.0
Language quality, pointer semantics, closure stabilisation, and aspect bounds.
Breaking changes:
- Anonymous closure expressions now use
(...) -> ... { ... };fun(...)is no longer accepted in expression position, and function types are written as(T) -> U - Struct fields are module-private by default; cross-module field access and construction now require
pubon each exposed field - Mutable bindings now use
var; standalonevar x = value;is no longer accepted, andfor/for-inbindings use the samevarform
New language features:
- Explicit receiver semantics — methods may declare
&self(shared read) or&var self(shared mutable) receivers;&var selfmutations are visible to the caller without a writeback convention - Regular and mutable pointer types —
&exprand&var exprproducePointer<T>andMutPointer<T>values; assignment through*ptrand function-pointer auto-deref are supported - Aspect bounds on generic type parameters — functions, structs, and enums may now declare aspect bounds on their type parameters; bounds are enforced by the typechecker and violation is error
T0012:- Inline single bound:
fun foo<T: Comparable>(x: T),struct SortedList<T: Comparable> - Inline multi-bound with
+:fun foo<T: Comparable + Printable>(x: T) whereclause:fun foo<T>(x: T) where T: Comparable + Printableimpl Aspectanonymous parameters:fun foo(x: impl Display)- Aspect methods declared by a bound are available on the type parameter inside the function body
T0012is emitted at the call/construction site with span on the offending argument
- Inline single bound:
- String interpolation (
${expr}) — string literals may contain${…}placeholders; each hole desugars to.to_string()concatenated with surrounding fragments via+ - String concatenation —
String + String -> String - Aspect default methods — an aspect method may provide a default body;
implblocks may omit defaulted methods and inherit them automatically Selfin impl signatures —Selfmay be used as a parameter or return type inimplmethod signatures- Match arm blocks — match arm bodies may be a block in addition to a bare expression
Bug fixes:
- Computed index assignment (
arr[i + 1] = v,s.data[offset * 2] = v) now works correctly; previously any computed index expression caused an internal error &var selfmethods on nested struct fields now mutate in placeimplmethods withT-typed parameters on generic structs now resolve correctly in Pass 2- Bounded type parameter method dispatch correctly enforces arity and argument types
?(error propagation) — routed throughFrom-based coercion; typechecker emits T0007 when noFromimpl exists- Generic functions returning an ascribed
None : Perhaps<T>now correctly constrain the inferred return type
Tooling:
- CLI version is derived from
CARGO_PKG_VERSIONrather than a hardcoded string - Source file extension corrected to
.mtlthroughout public docs modanduseremoved from the reserved keyword list
Spec clarifications:
pubis not valid on top-levelletormutbindings
v0.6.4
Module system technical debt.
Internal improvements:
TypeDefinitionRegistryis now used as the cross-module type accumulator incheck_graph, replacing theVec<Decl>approach that cloned raw AST nodes; cross-module struct field type references now resolve correctly even when the field type comes from an indirect dependencyInferContext::newacceptsimported_schemesdirectly, enforcing the dual-registration invariant (inference + construction passes both see imported names) at the type leveldeclared_namesmap added toResolvedNamesduring name resolution, replacing an O(n) AST scan inbuild_import_schemesfor T0009/T0003 distinctionresolve_path_rootextracted tosrc/module_paths.rsas a single shared implementation for bothmodule_loaderandname_resolver; fixed a regression where theNamepath root incorrectly doubled the module name segmentStdPrelude::schemes()/ evaluator builtin parity assertion added as a compile-time-checked test
Compatibility:
- No language-visible changes.
v0.6.3
Module system — feature complete.
Bug fixes:
returnandbreakare now valid as bare match arm bodies without enclosing braces:arm => return value- Diamond module dependencies (same physical file reachable via two different logical paths) no longer fail with T0003; the name resolver now dereferences path aliases to their canonical form
Internal improvements:
?operator desugared in a pre-pass (path_normalizer::desugar_propagate_error) rather than carried through inference and construction;Expr::PropagateErrorno longer exists after normalizationType::PerhapsandType::Resultconvenience variants removed from theTypeenum; both types are now represented uniformly asType::Named("Perhaps", ...)andType::Named("Result", ...)- Per-module isolated runtime environments validated with cross-module closure-capture and mutual-recursion tests
- All aspect method dispatch key construction routed through
ImplMethodKey::to_env_key(), eliminating ad-hoc format strings in the evaluator
Compatibility:
- No language-visible changes except the match arm body fix, which is purely additive.
v0.6.2
Evaluator normalization.
Internal improvements:
Value::PerhapsandValue::Resultdedicated variants removed; allPerhapsandResultvalues now use the generalValue::Enum { name, variant, fields }representation, eliminating special-case dispatch throughout the evaluatorevaluate_graphnow initialises each module in its own isolatedEnvironmentseeded with builtins and cross-linked via theimported_namestable populated bycheck_graph; replaces the flat-merge strategy from v0.5.0
Compatibility:
- No language-visible changes. All existing programs produce identical output.
v0.6.1
Type system cleanup and std::core virtual module.
Internal improvements:
- Unified
TypeDefinitionRegistryreplaces four separate flat maps (struct_env,method_env,enum_env, aspect impls) in the type inference and construction passes; a single registry instance is now the source of truth for all type and impl data ImplMethodKeyenum replaces flat string concatenation for impl method dispatch keys in the evaluatorStdPrelude::default()is the single source of truth for all built-in function schemes, eliminating the previous divergence between the inference and construction registries
New language features:
std::corevirtual module:Perhaps,Result,Display,Iterable,From, and all built-in functions are available in every module without any explicit import- Glob import tiers: the runtime auto-imports
std::coreatStdtier (lowest priority); userimport path::*declarations useUsertier and silently win overStdtier without a conflict error
Compatibility:
- All existing programs are unaffected;
std::corenames that were previously available globally continue to work without import statements
v0.6.0
Module semantics.
Enforced module semantics (previously deferred from v0.5.0):
- Visibility enforcement:
pubis required for a declaration to be importable; private items produce a compile-time error (T0009) when referenced from another module - Import scoping: only names brought in scope by
importare accessible; accessing an undeclared name is a compile-time error (T0003) - Alias resolution:
import mod::name as aliasmakesaliascallable and removesnamefrom scope - Import conflict detection: two imports binding the same local name produce a compile-time error (T0011); explicit imports silently win over conflicting glob imports
- Glob visibility filtering:
import mod::*now includes onlypubitems from the source module; private items are excluded - Re-export propagation: names re-exported via
exportare part of the facade module's public API and importable by consumers without importing the underlying module directly pubdeclarations require complete type annotations (T0010): every parameter and the return type must be annotated on apub fun
Internal improvements:
- Name resolver wired into the type-checking pipeline (
load_root → resolve → normalize → check_graph → evaluate_graph) - Flat-merge compatibility shim and last-segment fallback removed
root::,self::, andsuper::path roots now compute correct module paths in both the loader and name resolver
Compatibility:
- Single-file programs and programs using only
pubitems across module boundaries are unaffected - Programs that imported private items or relied on global declaration visibility will need
pubannotations added
v0.5.0
Module system.
New language features:
- Multi-file programs: each
.mtlfile is a module; the module graph is built fromimportdeclarations import path::Name;both loads the referenced file and bringsNameinto scope- Import forms: single name, alias (
as), group ({A, B}), glob (*), module handle export path::Name;re-exports a name from a submodule into the current module's public APIpubonfun,struct,enum, andaspectmarks declarations as externally accessible- Absolute and relative path roots:
root::,std::,self::,super:: - Fully-qualified paths valid in type and expression position without a preceding
import - Circular imports detected at load time with a full chain in the error message
- Facade modules:
parser.mtlalongsideparser/directory — no specialmod.mtlfile - File-to-module mapping via
::→/with no special cases
Shipped in v0.6.1:
std::coreauto-import and standard library core types
Compatibility:
- Single-file programs with no
importorexportdeclarations remain valid without modification
v0.4.2
Evaluator refactor, test restructure, and keyword cleanup.
Breaking changes:
Perhaps::Noperenamed toPerhaps::None; the standalonenopekeyword is nowNone
v0.4.1
Technical debt, bug fixes, and internal cleanup.
Bug fixes:
TypeErrorCode::T0005("Invalid operand types") is now emitted for arithmetic operators (+,-,*,/,%) applied to non-numeric types (e.g.true + falseis now a type error)- Unary negation (
-) on non-numeric types is now a type error - Ordering comparisons (
<,<=,>,>=) on non-comparable types (non-numeric, non-String) are now type errors Pattern::Nopelatent bug eliminated —nopevalues are now exclusivelyValue::Perhaps(None), so the pattern can no longer silently miss theValue::Enum { name: "Perhaps", variant: "Nope" }form
Internal improvements:
Value::YoloResultrenamed toValue::Result;PerhapsandResultvalues are now first-class runtime variants — no longer stored asValue::Enum- Large enum variants boxed in
Decl,Stmt,TypedDecl,TypedStmt(stack frame sizes reduced from 896–1040 bytes to 8 bytes) - Dead utility methods removed (
Program::new,Type::is_numeric,Type::is_unit); reserved fields annotated with#[allow(dead_code)] - All clippy style/idiom warnings resolved
v0.4.0
Aspects and upgraded builtins.
New language features:
- Aspect declarations —
aspect Foo { fun method(self) -> T; } extend Type: Aspectblocks with method dispatch via.method()syntaxIterable<T>aspect — user-defined types usable infor-inloopsFrom<S>aspect —ascast desugars toT::from(value); user-defined casts for any type pairDisplayaspect —.to_string()oni64,f64,boolean,String;print/printlnpolymorphic via Display?operator now supports cross-type error coercion: if the function's error typeE2implementsFrom<E1>,?callsE2::from(e)automatically
Builtin changes:
print(v)andprintln(v)are now polymorphic (<T: Display>) — accept any Display typei64::from(f: f64)andf64::from(n: i64)built-in From impls replace the hardcodedasspecial case- Deprecated:
print_int,println_int,print_float,println_float,int_to_string,float_to_string,bool_to_string(use.to_string()and polymorphicprint/println)
Bug fixes:
- Keyword-prefix identifiers (
break_sum,return_value,let_x) now parse correctly as identifiers - Multiple
extend Y: From<X>blocks with different source types now dispatch independently
v0.3.0
Generics and type-inference improvements.
New language features:
- User-defined generic functions —
fun id<T>(x: T) -> T— monomorphised at each call site - User-defined generic structs —
struct Box<T> { value: T },struct Pair<A, B> { ... } - User-defined generic enums —
enum Maybe<T> { Some { value: T }, None {} } - Let-polymorphism — unannotated
let-bound closures are generalised to polymorphic schemes (let id = fun(x) { x }works ati64,boolean, andStringin the same scope) - Braceless
ifbody —if (c) exprandif (c) a else b structandenumdeclarations are allowed inside function bodies
Type-inference improvements:
expected_typropagates into match arm bodies — bare[]andnoperesolve without ascription when the surrounding return type is known- Callee parameter types propagate into argument construction —
find(words, nope)resolves without ascription when the parameter type isPerhaps<String> - Lvalue path assignment —
obj.field = valandarr[i] = valwork on non-bare receivers (e.g.get_foo().bar = 1)
v0.2.0
Evaluator improvements, DX features, and language quality fixes.
New language features:
- Type ascription operator
:—[] : i64[]guides type inference without runtime cost - Shorthand struct field initialisation —
Point { x, y }desugars toPoint { x: x, y: y } - Trailing commas allowed in function parameter lists and argument lists
New built-in functions:
assert(cond: boolean)— panics with"assertion failed"ifcondisfalseassert_msg(cond: boolean, msg: String)— panics withmsgifcondisfalsedbg<T>(v: T) -> T— prints[dbg] <value>to stderr and returns the value unchangedprint_int(n: i64),println_int(n: i64)— print ani64without/with newlineprint_float(f: f64),println_float(f: f64)— print af64without/with newline
Bug fixes:
- Arrays now have value semantics — binding an array to a new variable produces an independent copy
- Error spans now report
file:line:colinstead of raw byte offsets - Complex expressions (field access, calls) are now valid array index operands
Developer experience:
- Runtime panics now include a call-stack trace showing function name and call site
v0.1.0
Initial language version. Implemented by the tree-walk interpreter.
Features included:
- Primitive types:
i64,f64,boolean,String,() - Variables:
let(immutable),mut(mutable), lexical scoping,fun/type hoisting - Functions: first-class values, closures with mutable capture,
?operator (exact error type match only) - Structs: literals, field access, methods (
impl),var self, associated functions - Enums: unit and struct-like variants,
implblocks - Built-in generic types:
Perhaps<T>,Result<T, E>,Array<T>/T[](as special cases; user-defined generics are v0.3.0) - Exhaustive pattern matching: all pattern kinds (see Pattern Kinds)
- Control flow:
if/else,while,for,for-in(arrays and ranges only),loop,break/continue,return - Type casting:
asfori64 ↔ f64 - Never type (
!) - Tuples
- Built-in functions (see Built-in Functions)
Not included (v0.3.0+):
- User-defined generic functions and types (see Generics)
- User-defined aspects and
extend Type: Aspect(see Aspects) From-based?coercion across different error types (see The ? Operator)- User-defined
Iterable<T>implementations (see For-In)