This README is the primary, implementation-focused language reference for the project. It is the preferred first entry point for getting to know the language.
If this file conflicts with docs/language-manual.md, the manual wins.
docs/language-design.md is for design direction and rationale, not the authoritative implementation spec.
Package manifests, build workflow, and run workflow are documented separately in docs/build-guide.md.
- Source files use the
.mtextension. - Ordinary files are the normal source form. They have no module header; module identity is inferred from the file path.
- Raw ABI binding files use a leading
externalheader. - External files are the dedicated raw ABI surface, usually for generated or low-level
std.c.*bindings. - Module lookup resolves
a.b.ctoa/b/c.mt. - Inside a package, the file path relative to
package.source_rootdefines the module name; platform-specific files such asname.linux.mtstill map to modulename. - In ordinary files,
importstatements appear only at the top. - In external files, leading
importstatements are allowed afterexternal. - Only external files accept
include,link, andcompiler_flagdirectives. - After those imports and directives, external files stay narrow: they contain raw ABI declarations, not ordinary module logic.
Blocks are indentation-based:
:starts a block.- Indentation must be spaces only.
- Tabs are rejected.
- Indentation must be a multiple of 4 spaces.
- Indentation can increase by only one level at a time.
- Newlines end statements except inside
()and[], or when the previous physical line ends with a binary operator such as+,and, or==. - Comma-separated lists inside
()and[]accept trailing commas. Prefer them for multiline parameters, arguments, and type lists.
Long expressions should usually be wrapped with delimiters, following the same broad shape as Python's implicit line joining:
let total = (
subtotal
+ tax
- discount
)Milk Tea also accepts operator-led continuation when the previous line ends with a binary operator:
let total = subtotal +
tax -
discountThis also applies to range expressions:
let values = 1 ..
4Do not rely on starting the next physical line with the operator; wrap the expression in () instead if that layout reads better.
Comments:
#starts a line comment.##starts documentation comments attached to the next declaration if no blank line intervenes.
Supported literals:
- integers:
42,0xff,0b1010,_separators allowed. Integer type suffixes:42u(uint),0xFFub(ubyte),100z(ptr_uint),7i(int),-1l(long), etc. - floats:
3.14,1.2e-3,1.0f(float suffix),1.0d(double suffix) - character:
'a','\n','\t','\\','\'','\0','\x41'. Type isubyte. Escape sequences:\n,\r,\t,\\,\',\",\0(null byte),\xNN(hex byte). - booleans:
true,false - string:
"hello"->str - cstring:
c"hello"->cstr - heredoc string:
<<-TAG ... TAG - heredoc cstring:
c<<-TAG ... TAG - format string:
f"count=#{count}" null, including typed forms likenull[ptr[char]]when context does not determine the target type
Common punctuation and operators:
- delimiters:
()[] - access and separators:
:,. - type markers:
->? - arithmetic:
+ - * / % - bitwise:
~ & | ^ << >> - comparison:
== != < <= > >= - assignment:
= += -= *= /= %= &= |= ^= <<= >>= - variadic marker:
... - word operators:
and,or,not - pattern test:
is— desugars tomatchexpression for variant arm membership test
Supported top-level declarations:
const(expression form and block-bodied form with->)const function— compile-time-evaluable function, callable from compile-time and runtimevartypeattributeinterfacestructunionvariantenumflagsopaqueextendingfunctionasync functionexternal functionforeign functioneventstatic_assert(...)emit— compile-time code generation insideconst functionorinlinebodieswhen(compile-time conditional; may appear at module level or inside function bodies)
File-kind note:
- Ordinary files may use the full declaration surface above.
- External files are intentionally narrower: after optional imports and directives, they allow only
const,type,struct,union,enum,flags,opaque, andexternal function. External files cannot declare new attributes, but supported attribute applications such as@[packed]and@[align(...)]may still appear on declarations that accept them. eventdeclarations are not allowed in external files.
Visibility:
publicis allowed on exportable ordinary declarations.publicis rejected onextendingblocks.publicis rejected on ordinaryexternaldeclarations andstatic_assert.- In external files, declarations are implicitly exported and
publicis rejected.
constrequires an explicit type and initializer. A block-bodied formconst NAME -> TYPE:followed by an indented block is also supported; the block is evaluated at compile time and must end with areturn.- Top-level
varrequires an explicit type. Its initializer is optional but must be static-storage-safe when present. - Local
letis immutable. - Local
varis mutable. - A local declaration without an initializer requires an explicit type and must be zero-initializable.
Guard form:
let value = maybe_value else:
return 1
var runtime = maybe_runtime else:
return 1
let parsed = parse(input) else as error:
return error
let _ = initialize() else:
return 1Tuple destructuring:
let (a, b) = pair()
let (x, y) = (1, 2)Struct destructuring:
struct Vec2:
x: float
y: float
let p = Vec2(x = 1.0, y = 2.0)
let Vec2(x, y) = pRules for let ... else: and var ... else::
- Both
letandvarsupport anelseblock. - The initializer must have type
T?,Option[T], orResult[T, E]. - For
T?, the bound name has typeT. - For
Option[T], the bound name issome.value. - For
Result[T, E], the bound name issuccess.value. else as error:optionally binds thefailure.errorvalue.let _ = expr else:checks success without binding a name.- The
elseblock must terminate control flow.
Postfix Result/Option propagation:
let parsed = parse(input)?
let lowered = lower(parsed)?
return Result[Output, Error].success(value= lowered)expr?requiresOption[T]orResult[T, E]with a non-voidsuccess type. Any variant with matching arm structure (some(value: T)/noneorsuccess(value: T)/failure(error: E)) also works.- On success,
expr?evaluates to the unwrappedT. - On failure,
expr?returnsOption[_].noneorResult[_, E].failure(error= ...)from the enclosing function or proc. - As an expression statement,
expr?also acceptsOption[void]orResult[void, E]; success continues and failure returns early. expr?is only allowed inside function and proc bodies.- Inside
asyncfunctions, failure completes the task early. expr?is not allowed insidedeferblocks.- The enclosing function or proc must return a compatible type —
Option[_]orResult[_, E]with the same error typeE. let _ = expr else:is still useful when you need an explicitelseblock orelse as error:binding.
Callable and ref[...] rules:
- Plain stored
ref[T]values are rejected in constants, module variables, and nested local storage such as arrays or other generic containers. - In struct or union fields, bare
ref[T]auto-generates an implicit lifetime parameter. The struct becomes non-owning, inheriting ref-like restrictions: allowed as function params and localletvariables, rejected as returns and module storage. Explicit lifetime parameters (struct Cursor[@a]: data: ref[@a, span[ubyte]]) are still supported when the lifetime needs to be shared across multiple fields. - Ordinary local bindings may still hold a direct
ref[T]value, for examplelet handle = ref_of(counter). fn(...)andproc(...)parameter types may useref[...]directly in parameter position.- Stored callable values may use
ref[...]only in direct callable parameter positions. This includesfn(...)values andproc(...)closure values stored in locals, struct fields, and generic containers such asarray[...]. - Stored callable values may not use
ref[...]in return types. - Stored callable values may not nest
ref[...]anywhere except direct callable parameter positions. - External functions still cannot take
ref[...]parameters, and ordinary functions still cannot returnref[...]. proccaptures are value captures. A captured local is not a mutable alias back to the outer binding. Any storable type may be captured, including scalars, arrays, structs, and otherprocvalues. Capturedprocvalues participate in the ref-counted lifecycle: the capturing proc retains the captured proc on creation and releases it when the env is freed.ref[T]values are not capturable by design since they are non-owning.- Shared mutable proc state should use explicit storage such as
std.cell.alloc[T](...)or other explicit pointer-backed state, not implicit mutable capture.
Examples:
type Seconds = float
struct Vec2:
x: float
y: float
@[packed]
struct Header:
tag: ubyte
@[align(16)]
struct Mat4:
data: array[float, 16]
Nested structs declare scoped types inside an enclosing struct body:
```mt
struct Rectangle:
x: float
y: float
struct Edge:
start: float
end: float
top_edge: Edge
left_edge: Edge- Nested structs are full independent types scoped inside their parent. Their qualified name is
Parent.Nested(e.g.,Rectangle.Edge). - Inside the parent struct, bare names resolve to nested types (e.g.,
EdgemeansRectangle.Edge). - Outside, use the qualified name:
var e: Rectangle.Edge. - Nested structs may themselves contain nested structs to arbitrary depth.
- The generated C name uses underscore-separated path components (e.g.,
module_Rectangle_Edge).
union Number: i: int f: float
enum State: ubyte idle = 0 running = 1
flags Mask: uint a = 1 << 0 b = 1 << 1
opaque SDL_Window
variant Token: ident(text: str) number(value: int) eof
Rules:
- `struct` and `opaque` may declare nominal interface conformance with `implements`.
- `attribute[target, ...]` declares reusable declaration attributes for `struct`, `field`, `callable`, `const`, `event`, `enum`, `flags`, `union`, and `variant` targets.
- Attributes are applied with one or more leading `@[name(...)]` blocks. Built-in `packed`, `align(bytes)`, and `deprecated(message)` are predefined attributes.
- `variant` arms may carry named payload fields.
- Payload arm construction uses named fields: `Token.ident(text = "hello")`.
- No-payload arms are bare member expressions: `Token.eof`.
- `enum` and `flags` backing types must be integer primitives.
- `enum` and `flags` members must be compile-time integer constants.
- `flags` members may reference earlier members to spell composite aliases such as `read_write = Permission.read | Permission.write`.
- `align(...)` must be a positive power of two.
- Compile-time reflection over validated attributes uses `has_attribute`, `attribute_of`, `attribute_arg[T]`, `field_of`, and `callable_of`.
- `field_of(...)`, `callable_of(...)`, and `attribute_of(...)` produce compile-time handle values with source-visible handle types `field_handle`, `callable_handle`, and `attribute_handle`.
- The current C backend lowers `packed` / `align(...)` attributes with GNU-style `__attribute__((...))`, so these layout controls currently require a Clang/GCC-family compiler. On Windows that means Clang or GCC-family toolchains such as MinGW; `cl.exe` is not a supported backend for these attributes today. On wasm/browser targets the same feature works through Emscripten `emcc`, which is Clang-based.
Enum and flags values support the full set of comparison operators (`==`, `!=`, `<`, `<=`, `>`, `>=`) against values of the same enum type and against their backing integer type. Comparisons use the underlying integer backing values. Flags also support bitwise operators (`|`, `&`, `^`, `~`).
Generic variants and structs are supported, for example `Option[int]`.
## 6. Interfaces And Methods
Interface example:
```mt
public interface Damageable:
editable function take_damage(amount: int) -> void
function is_alive() -> bool
Interface rules:
- Interface bodies contain
function,editable function, orstatic functionsignatures. - Generic interfaces are supported:
interface Mapper[T]: function map(x: T) -> T. - Interface methods may not have their own type params —
Tcomes from the interface. - Interface methods may not be
async. - Interface methods do not have bodies.
- Bare interface names are not runtime storage types.
- Interfaces are used by
implementsand constrained generics, not as runtime value types. - Runtime interface values use
dyn[InterfaceName]— a fat pointer carrying a data pointer and a vtable pointer. Construct withadapt[Interface](value: ref[T]), which verifiesT implements Interfaceat compile time. - Generic interfaces instantiated through
dynmust be fully specified:dyn[Mapper[int]]is valid;dyn[Mapper]is rejected. - Conformance with generic interfaces uses type substitution:
struct Foo implements Mapper[int]checks thatFoo's methods matchMapper's methods withTreplaced byint.
Method kinds:
function-> value receivereditable function-> editable receiverstatic function-> no receiver
Method notes:
- Async methods are supported.
- Generic methods are supported.
- There is no constructor keyword. Names like
initanddefaultare ordinary static methods.
Ordinary functions:
- Parameters must be typed.
- Parameters are non-rebindable.
- Return type defaults to
voidif omitted. - Generic functions are supported.
- Generic function and method type parameters may use
implementsconstraints.
const function:
A const function is evaluable at compile time. Its body follows the same restrictions as a block-bodied const. When called from a compile-time context (const, when, inline if, inline for), the call is constant-folded:
const function square(x: int) -> int:
return x * x
const RESULT: int = square(5) # folded to 25 at compile timeconst function also generates a normal runtime function, callable from ordinary runtime code.
External functions:
external function printf(format: cstr, ...) -> intRaw std.c.* modules usually group many external function declarations inside an external file, but external function is also allowed in ordinary files for small manual ABI bridges.
Rules:
- No body.
- Variadic
...is supported. - Cannot be generic.
- Cannot be async.
- Cannot take arrays.
- Cannot take
refparameters. - Cannot take
procparameters. - Cannot return arrays.
- Calls may pass enum or flags values to same-width fixed-width integer parameters without an explicit cast for C ABI interop.
Foreign functions:
foreign function init_window(width: int, height: int, title: str as cstr) -> void = c.InitWindow
foreign function load_file_data(file_name: str as cstr, out data_size: int) -> ptr[ubyte]? = c.LoadFileData
foreign function close_window(consuming window: Window) -> void = c.CloseWindowParameter modes:
- plain
inoutinoutconsuming
Boundary projection syntax:
name: PublicType as BoundaryType
Foreign-function rules:
asis only allowed on plain andinparameters.in,out, andinoutare declared on the parameter, not at the call site.- Legacy call syntax like
load_file_data(path, out size)orinspect(in value)is rejected. consumingforeign functions must returnvoid.- Consuming foreign calls must appear as top-level expression statements.
- A consuming argument must be a bare nullable local or parameter binding.
Supported statements:
- local declaration (
let,var) - assignment
if/else if/elsematchunsafestatic_assertforparallel for— data-parallel loop dispatched across CPU coreswhilewhen— compile-time conditional; only the chosen branch is type-checked and emittedinline for— loop over a compile-time-known array, unrolled at compile timeinline while— loop with a compile-time-known condition, unrolled at compile timeinline match— match with a compile-time-known scrutinee, unrolled at compile timeinline if— if with a compile-time-known condition; only the chosen branch is type-checked and emittedpassbreakcontinuereturndeferemit— only insideconst functionorinline for/while/if/matchbodies- expression statement
Rules:
- Conditions must be
bool. - There is no truthy or falsy coercion from integers or pointers.
passis an explicit no-op statement for intentionally empty block bodies.
match supports the following scrutinee types:
- enum scrutinees
- variant scrutinees
- integer scrutinees
match may also be used as an expression, producing a value from the matched arm:
let label = match code:
1: "one"
2: "two"
_: "other"match rules:
- Enum and variant matches must be exhaustive unless
_is present. - Integer matches require
_. Match arms accept integer literals and char literals (e.g.,'('against aubytescrutinee). - Multiple pattern values may share the same arm body using
|:kindmatcheskind_a | kind_b. - Variant payload arms may bind with
as name. - Variant payload arms may destructure fields inline with struct patterns:
Variant.arm(field > 0, other)— comparisons are guards (arm skipped if false), identifiers are bindings (field becomes a local), andfield = valueis an equality guard.
match token:
Token.ident(text):
use_name(text)
Token.number as n:
use_value(n.value)
Token.eof:
return
# Discard unneeded fields with _
match multi_field:
Entity.tag(_, _, _, label):
use_label(label)Struct pattern rules:
- Guards (
hp > 0,level >= 3) skip the arm if the condition is false; the match tries the next arm. Supported guard operators:==,!=,<,<=,>,>=. - Equality patterns (
kind = Kind.boss) skip the arm if the field does not equal the value. - Bindings (
position) create immutable local variables bound to the field value. - Discard (
_) skips a field position without binding it; useful when you only need a subset of a multi-field payload arm. - Guards and equality patterns are refutable: they do not count toward exhaustiveness. Exception: when equality patterns for an enum-typed field gatherively cover every member of the enum, the arm is considered exhaustive.
- For variant payload arms, struct patterns compose with
as namebindings. - When a variant arm has exactly one payload field of struct type, and no pattern argument references that field name, the struct's own fields are transparently destructured. For example,
Entity.positioned(x, y)wherepositioned(loc: Pos)destructures throughPosto bindxandy.
Loop forms:
for i in 0..count:for exclusive integer rangesfor item in items:for arrays, spans, and custom iterables- Custom iterable protocol:
items.iter()must take no arguments, be a non-editable method, and return the iterator value. - Iterator forms: either
next() ->nullable pointer-like item, ornext() -> booltogether withcurrent() -> T. for left, right in xs, ys:for parallel array/span iteration- Parallel
fordoes not accept ranges.
parallel for dispatches loop iterations across multiple CPU cores using real OS threads:
parallel for i in 0..entity_count:
positions[i] += velocities[i] * dtRules:
- Only range iteration is supported (
0..N). - The loop body must not contain
break,continue,return,defer, or nestedparallel for. - Captured
ref[T]values are rejected at compile time. - Array captures are passed by pointer; span and scalar captures are passed by value.
- The compiler automatically links libuv for thread dispatch when
parallel foris used.
parallel: blocks run each statement concurrently, blocking the caller until all complete:
parallel:
textures = load_textures(path)
sounds = load_sounds(path)Rules:
- A
parallel:block must contain at least two statements. - Each statement must not contain
break,continue,return, ordefer. - The compiler enforces single-writer-or-multiple-readers: if a variable is written in one statement, no other block may access it.
- Captured
ref[T]values are rejected at compile time.
detach spawns work on a separate thread and returns a Handle. gather blocks until all handles complete:
let a = detach load_textures(path)
let b = detach load_sounds(path)
process_other_stuff()
gather a, bRules:
detachis an expression returning aHandle— must be bound withletorvar.- Currently only supports global function calls with no captured local variables.
gathertakes one or moreHandlevalues, joined in order.- Captured
ref[T]values are rejected at compile time. - The compiler automatically links libuv for thread dispatch.
defer:
defer exprdefer:block formreturnis not allowed inside defer blocks.
unsafe is required for:
- pointer indexing
- raw pointer dereference
- pointer arithmetic
- pointer casts
reinterpret[...]
Range index assignment is supported:
buf[0..3] = (1.0, 2.0, 3.0)Rules:
- The bounds must be integer literals.
- The range is start-inclusive and end-exclusive.
- The right-hand side must be an expression list with exactly matching width.
Compile-time control flow:
when evaluates its discriminant at compile time and emits only the chosen branch:
when TARGET_OS:
TargetOs.linux:
return open_linux(path)
TargetOs.windows:
return open_windows(path)- The discriminant must be a compile-time constant.
- Only the chosen branch is type-checked and lowered.
- An
elsebranch is required unless the discriminant is an enum and every member is covered. whenmay appear at module level to conditionally include declarations.
inline for unrolls a loop over a compile-time-known array:
inline for field in fields_of(Particle):
static_assert(field.type == float, "Particle fields must be float")- The iterable must be a compile-time-known array (from reflection builtins or a literal array).
inline while unrolls a loop with a compile-time-known condition:
inline while n < 1024:
n = n * 2- The condition must be a compile-time constant. The loop unrolls to a fixed number of iterations.
inline match unrolls a match with a compile-time-known scrutinee; only the chosen arm emits code. It is not required to be exhaustive.
inline if branches on a compile-time-known boolean condition:
const DEBUG_RENDER: bool = false
function draw() -> void:
inline if DEBUG_RENDER:
debug_overlay()- The condition must be a compile-time constant.
- Only the chosen branch is type-checked and emitted. The dead branch may reference types and symbols that do not exist.
inline ifsupportselseandelse ifbranches; the chosen branch follows the same dead-elimination rule.
Primary expressions:
- identifiers
- literals
- parenthesized expressions
- tuple literal:
(a, b)— positional;(x = 1, y = 2)— named size_of(T)align_of(T)offset_of(T, field)
size_of and offset_of accept compile-time expressions for the type and field arguments respectively, enabling generic per-field introspection through inline for:
inline for field in fields_of(Point):
let s = size_of(field.type)
let o = offset_of(Point, field)proc(...) -> T: ...proc(...) -> T: exprfor a single expression body, implicitly returnedif cond: a else: bmatch scrutinee: arm: value ...— produces a value from the matched arm
Postfix forms:
- member access:
a.b - indexing:
a[i] - call:
f(x) - partial field update:
v.with(x = 10.0)— returns a copy with specified fields replaced - specialization:
name[T],name[32],mod.name[T] - explicit specialization is only accepted on bare or module-qualified names;
value.member[32](...)remains indexed-call syntax, so value-member calls rely on inference instead of explicit literal specialization
Operator precedence, low to high:
orand|^&==,!=<,<=,>,>=<<,>>+,-*,/,%
Native type operators:
- Vectors (
vecN/ivecN):+,-,*(component-wise) with same-type vectors;*,/with scalar; unary- - Matrices (
matN):+,-with same-type matrices;*,/with scalar; unary- - Quaternions (
quat):+,-,*(component-wise) with same-type quaternions; unary-
Primitive types:
boolbyte,short,int,longubyte,ushort,uint,ulongcharptr_int,ptr_uintfloat,doublevoidstrcstrvec2,vec3,vec4— float vectors with.x.y.z.wfieldsivec2,ivec3,ivec4— integer vectors with.x.y.z.wfieldsmat3,mat4— column-major matrices;mat3hasvec3columns.col0–.col2,mat4hasvec4columns.col0–.col3quat— quaternion with.x.y.z.wfields (layout-compatible withvec4)
Native vector, matrix, and quaternion types support aggregate construction with named fields, same as struct literals. Omitted fields default to zero.
let direction = vec3(x = 1.0, y = 0.0, z = 0.0)
let identity = mat4(
col0 = vec4(x = 1.0, y = 0.0, z = 0.0, w = 0.0),
col1 = vec4(x = 0.0, y = 1.0, z = 0.0, w = 0.0),
col2 = vec4(x = 0.0, y = 0.0, z = 1.0, w = 0.0),
col3 = vec4(x = 0.0, y = 0.0, z = 0.0, w = 1.0),
)
let q = quat(x = 0.0, y = 0.0, z = 0.0, w = 1.0)Primitive type names are reserved. They cannot be reused for value bindings, parameters, locals, import aliases, or type parameters.
Type constructors:
ptr[T]const_ptr[T]ref[T]span[T]array[T, N]str_buffer[N]Task[T]Option[T]Result[T, E]fn(params...) -> Rproc(params...) -> RSoA[T, N]— Structure-of-Arrays: each struct field becomes a separate array of lengthN; accesssoa[i].fieldreads from columnfieldat rowidyn[InterfaceName]— runtime interface value (fat pointer:{ void* data, void* vtable }). Constructed viaadapt[Interface](value: ref[T]). @see §6.atomic[T]— atomic value for lock-free concurrent access.Tmust be a primitive integer orbool. Methods:load() -> T,store(value: T),add(value: T) -> T,sub(value: T) -> T,exchange(value: T) -> T. All operations use sequential consistency. Lowers to C11_Atomic Twith__atomic_*builtins.(T, U)— tuple type. Positional fields auto-named_0,_1. Named fields use(x = T, y = U). Copy by value, returns supported.
When a span[T] is expected, an addressable array[T, N] value may be passed directly via implicit boundary coercion. For explicit conversion, array.as_span() returns span[T] without requiring a boundary context.
Nullability:
- Nullable form is
T?for pointer-like types. - Use
nullfor absence. - In nullable pointer-like contexts, prefer
nulloverzero[ptr[T]]. ref[T]is non-null and cannot be nullable.
Generics:
- Generic structs, variants, functions, methods, and foreign functions are supported.
- Generic interfaces are supported:
interface Mapper[T]: function map(x: T) -> T. - Generic type parameter constraints use
implementson structs, variants, interfaces, functions, and methods. implementsis the interface constraint kind.- Multiple interface constraints are joined with
and. - There are no separate
hashesorequatesconstraints. Generic bodies that callhash[T](...),equal[T](...), ororder[T](...)rely on specialization-time checking of the canonical associated functions. - Current type parameters can be used as type expressions for associated function calls in generic bodies, for example
T.default()orT.tag(). - Generic value parameters use the form
[N: int]to declare a compile-time integer usable in expressions. The call site specializes with a literal:int_with_bits[64]. typeis a built-in type name representing the type of types. A function may returntypeto pick a type at compile time from its value parameters.
Special recognized callables:
fatal(message)ref_of(x)const_ptr_of(x)read(r)read(p)ptr_of(x)T<-valuereinterpret[T](value)zero[T]default[T]hash[T](value)equal[T](left, right)order[T](left, right)array[T, N](...)span[T](data = ..., len = ...)get(coll, index)— recoverable array/span indexing returningptr[T]?; null on out‑of‑bounds instead of abortingadapt[I](value)— constructs adyn[I]runtime interface value; verifiesvalue's type implements interfaceIat compile time
Reference and pointer notes:
read(ref_value)explicitly projects the referent value. Useread(handle) = valueto write through a bareref[T]value.- Member access and method calls auto-dereference
ref[T]receivers. For mutable field access throughref[Struct], usehandle.field += xdirectly — noread()needed. - Passing a mutable addressable
Tto a parameter of typeref[T]implicitly borrows it. hash[T](value),equal[T](left, right), andorder[T](left, right)lower toT.hash(...),T.equal(...), andT.order(...)associated functions. Each argument must be a safe storedTlvalue that can be borrowed, or an existingref[T],ptr[T], orconst_ptr[T].T.order(left: const_ptr[T], right: const_ptr[T]) -> intreturns a negative value whenleft < right,0when equal, and a positive value whenleft > right.- There are no separate
hashesorequatesconstraints; the builtins themselves force those hook requirements at specialization time. - There is no separate
defaultsconstraint. A generic body that usesdefault[T]relies on specialization-time checking thatT.default()exists.
Compile-time reflection builtins:
field_of(T, name)— returns afield_handlefor the named field ofT.callable_of(T, name)— returns acallable_handlefor the named callable ofT.attribute_of(T, name)— returns anattribute_handlefor the named attribute onT.has_attribute(T, name)— returnsbool; true ifThas the named attribute applied.attribute_arg[T]— returns theT-typed argument of a resolved attribute handle.fields_of(T)— returnsarray[field_handle, N]of all fields of structT, in declaration order.members_of(E)— returnsarray[member_handle, N]of all members of enum or variantE.attributes_of(T)— returnsarray[attribute_handle, N]of all attributes onT.attributes_of(T, name)— returnsarray[attribute_handle, N]of attributes whose kind matchesname.
Handle types expose: field_handle has .name and .type; member_handle has .name and optionally .value; attribute_handle provides access to attribute arguments.
Core modules in std/:
std.linear_algebra— extends native vector/matrix/quaternion types withdot,cross,length,normalized,lerp,identity,transpose,conjugate(pure Mt, no C dependency beyondstd.mathforsqrt)std.graph.Graph[T]— adjacency-list graph withadd_node,add_edge,has_edge,remove_edge,neighbors,bfs,dfs,toposort; directed or undirected;compile()converts to CSR-basedDenseGraph[T]for O(degree) neighbor iterationstd.str— extendsstrwithbyte_at,equal,starts_with,ends_with,find_substring,is_valid_utf8,slice,to_cstr,hash,orderstd.hash— extends primitive types (int,uint,bool,float,double,char) with canonicalhash/equal/orderhooks; import once to use primitives as Map/Set/BinaryHeap/OrderedMap keys. Also provides generichash_struct[T],equal_struct[T],order_struct[T]using compile-time reflection.std.cstring— C string helpers (cstr_len,cstr_as_str)std.math—sqrt,sin,cos,abs,pow, etc. via C mathstd.encoding— UTF-8 validation (is_valid_utf8,utf8_codepoint_count,decode_utf8_codepoint,utf8_overlong_check)std.string.String— growable owned UTF-8 textstd.mem.heap,std.mem.arena,std.mem.pool,std.mem.stack— allocatorsstd.async— task runtime (sleep,work,completed,result,wait,run)std.option.Option[T]— optional value withis_some,is_none,unwrap,expect,unwrap_or,unwrap_or_else(auto-imported via prelude)std.result.Result[T, E]— fallible computation withis_success,is_failure,unwrap,unwrap_error,unwrap_or,unwrap_or_else,ok,error,map_error(auto-imported via prelude)
Collections: std.vec.Vec[T], std.deque.Deque[T], std.map.Map[K,V], std.set.Set[T], std.ordered_map.OrderedMap[K,V], std.ordered_set.OrderedSet[T], std.binary_heap.BinaryHeap[T], std.priority_queue.PriorityQueue[T], std.linked_map.LinkedMap[K,V], std.linked_set.LinkedSet[T], std.counter.Counter[T], std.multiset.MultiSet[T], std.queue.Queue[T], std.stack.Stack[T]
Serialization: std.json, std.toml, std.uri, std.serialize
System: std.time, std.fs, std.path, std.process, std.cli, std.stdio, std.terminal
Concurrency: std.sync, std.thread, std.jobs
AI/State: std.fsm (finite state machine), std.goap (goal-oriented action planning), std.behavior_tree
Networking: std.http, std.tls, std.net (see also std.net.manager, std.net.discovery)
Compression: std.gzip, std.tar
Other: std.bytes, std.ctype, std.asset_pack, std.cell
See module source for full method surface. Iterator forms:
- Pointer-returning (
next() -> nullable ptr[T]):Vec,Deque,BinaryHeap/PriorityQueue/OrderedSet(read-only),OrderedMap.keys/Map.keys/Set/LinkedMap.keys/LinkedSet/Counter.keys/MultiSet.values,Queue/Stack(mutable) next() -> bool+current():OrderedMap.entries/iter,Map.entries/iter,LinkedMap.entries/iter,Counter.counts/entries/iter,MultiSet.entries/iter,SnapshotValues/SnapshotEntries
Text categories:
str-> string viewcstr-> C ABI stringstr_buffer[N]-> fixed-capacity mutable UTF-8 text buffer
str_buffer[N] methods:
clear()assign(str)append(str)assign_format(str)append_format(str)len()capacity()as_str()as_cstr()
Format strings:
f"count=#{count}"has typestr.- Allowed interpolations:
str,cstr,bool, numeric primitives, integer-backed enums and flags, plus types implementingformat_len() -> ptr_uintandappend_format(output: ref[std.string.String]) -> void. f"..."is a borrowed temporary on the stack — it cannot be returned from a function asstr. Usestd.fmt.format(f"...")returningstring.Stringwhen ownership must escape.- Float and double interpolations support
:.Nprecision. - Integer primitive and integer-backed enum/flags interpolations support
:x(lowercase hex) and:X(uppercase hex). - Integer primitive and integer-backed enum/flags interpolations support
:o/:O(octal) and:b/:B(binary). std.fmt.format(...)receives special lowering and returnsstring.String.std.fmt.append_format(...)/std.fmt.assign_format(...)receive special lowering when passed a format string and write directly into an existingstring.Stringsink.string.String.append_format(...)/string.String.assign_format(...)receive the same direct-sink lowering when passed a format string.str_buffer[N].append_format(...)/str_buffer[N].assign_format(...)receive the same direct-sink lowering for fixed-capacity buffers.- Custom interpolation hooks use the direct sink when formatting into
string.String; plainf"..."expressions andstr_buffersinks pass a borrowedstring.Stringview onto the destination slice, so those paths stay allocation-free as long as the hook writes exactlyformat_len()bytes.
Heredoc notes:
<<-TAG ... TAG->strc<<-TAG ... TAG->cstrf<<-TAG ... TAG->str- Content is dedented by shared leading spaces of nonblank lines.
- The trailing newline before the terminator is preserved.
- Conditions must be
bool. - No truthy or falsy coercion.
- Outside external-call boundaries, enum and flags values do not implicitly coerce to their backing integer types.
- Mixed signed and unsigned integer arithmetic requires an explicit cast.
%requires integer-compatible operands.- Bitwise operators require matching integer or flags types.
- Shift operators require integer operands.
- Safe array indexing requires an addressable array value.
- Safe indexing (
arr[i]) is bounds-checked and callsfatalon out-of-bounds access. - Use
get(arr, i)for recoverable indexing that returnsptr[T]?(null on out-of-bounds) instead of aborting. - Pointer indexing requires
unsafe. read(ptr)requiresunsafe.- Pointer casts require
unsafe. reinterpret[...]requiresunsafe, non-array concrete sized types, and equal-size source and target types.
Example:
async function child() -> int:
return 41
async function parent() -> int:
let v = await child()
return v + 1Rules:
async functionlifts its declared return type toTask[T].awaitis only allowed inside async functions.async mainis compiler-bootstrapped.async mainpre-lift return type must beintorvoid.aio.wait(...)andaio.run(...)accept either zero-arg task roots or direct task expressions.
Supported await contexts include:
- plain expression positions
- call arguments (normalization hoists to
letbindings) - binary operations (
and,or, arithmetic, comparison) ifexpressionsif/else if/elsebodies and conditionswhilebodies and conditions- single-form and parallel
forbodies and iterables matchdiscriminants and arms- member access (
a.b) and indexing (a[i]) let ... else:initializers and else bodiesunsafeblocks- short-circuit
and/or - assignment targets
defercleanup bodies inside async functions- format strings (
f"#{await expr}")
Event declarations provide a built-in typed publisher/subscriber surface with fixed-capacity listener storage.
Declaration forms:
event name[capacity]
event name[capacity](PayloadType)
public event name[capacity]
public event name[capacity](PayloadType)Examples:
public event closed[4]
public event resized[8](ResizeEvent)Rules:
- The capacity expression must be a compile-time positive integer literal.
- An event carries either no payload or exactly one payload value.
- The payload type must be a storable type;
ref[T]payloads are rejected. - Event declarations are valid at top level and as struct members.
emitis only callable from within the declaring module.
Built-in event operations:
event.subscribe(listener) -> Result[Subscription, EventError]event.subscribe_once(listener) -> Result[Subscription, EventError]event.subscribe(state: ptr[State], listener: fn(ptr[State], ...)) -> Result[Subscription, EventError]— stateful overload (detected by passing 2 positional arguments)event.subscribe_once(state: ptr[State], listener: fn(ptr[State], ...)) -> Result[Subscription, EventError]— stateful one-shot (detected by passing 2 positional arguments)event.unsubscribe(subscription) -> bool— returnstrueif the listener was active and removedevent.emit()orevent.emit(payload)— only callable from the declaring moduleevent.wait() -> Task[Result[T, EventError]]— async wait for the next emission; for no-payload events the result type isResult[void, EventError]
EventError is a built-in enum with a single member full (value 0), returned when listener capacity is exhausted.
Subscription is a built-in opaque handle type returned by subscribe and subscribe_once, used to identify a listener for unsubscribe.
Current compiler rejects:
external functioncannot be generic, async, or array-taking / array-returningexternal functioncannot takeref[...]orproc(...)parametersexternal functionparameters cannot useas,in,out, orinoutforeign functioncannot be async and cannot takeproc(...)parameters- a
foreign functionwithconsumingparameter(s) must returnvoid consumingforeign calls must appear as top-level expression statementsmaincannot be genericasync mainpre-lift return type must beintorvoid- a function's return type cannot be
ref[T]or a non-owning struct (containsrefvia auto-generated lifetime) const functionbodies follow the same restrictions as block-bodiedconst
- module variables must have an explicit type; initializer must be static-storage-safe
ref[T]values are rejected in constants, module variables, and nested local storage such as arraysproc(...)values that capture local state are rejected in constants and module variables; capture-free procs (body only references module-level functions, constants, and types) are allowed in module variablesref[T]values are not capturable byproc- stored callable values (
fn(...),proc(...)) may useref[...]only in direct callable parameter positions, not in return types - bare interface names are not runtime storage types; use
dyn[Interface]
eventdeclarations are not allowed in external files- event payload cannot be
ref[T]in v1; the payload type must be storable - event payload cannot use event storage types or unsupported proc nesting
- event storage types cannot be returned from functions or passed through non-pointer/non-ref parameters; locals cannot copy event storage
emitis only callable from within the declaring module- event methods (
subscribe,subscribe_once,unsubscribe,emit,wait) do not support named arguments
- pointer indexing, raw pointer dereference, pointer arithmetic, and pointer casts require
unsafe reinterpret[T]requiresunsafeand non-array concrete sized typesptr_of,const_ptr_of, andref_ofrequire an addressable source expression- safe array indexing requires an addressable array value;
get(arr, i)provides recoverable bounds-checked access - legacy call-site markers
in,out, andinoutare rejected; parameter modes are declared onforeign function ptr_of/ref_ofcannot target arefvalue directly- pointer comparison is not treated as boolean truthiness
- conditions must be
bool; integers and pointers have no implicit truthy or falsy coercion - mixed signed and unsigned integer arithmetic requires an explicit cast
- enum and flags values do not implicitly coerce to their backing integer types outside external-call boundaries
enumbacking types must be integer primitives;flagsmembers must be compile-time integer constantsvariantarm payloads cannot useref[T]in v1- struct field
ref[T]auto-generates an implicit lifetime parameter; the struct becomes non-owning nullmust be used instead ofzero[ptr[T]]in typed nullable pointer-like contexts- bare interface names (
Damageable) are not a valid field, local, parameter, or return type; usedyn[Interface] dyn[Interface]for a generic interface must be fully specified:dyn[Mapper[int]], notdyn[Mapper]
- interface method signatures cannot be
asyncor generic - interface conformance must be declared on the nominal type, not on an
extendingblock - a type implementing an interface must match method kinds, receiver editability, parameter types, return type, and asyncness exactly
+does not supportstr/cstrconcatenation; use format strings orstd.strhelpers==and!=are not supported on struct types; useequal[T]. Variant types support==/!=(generates per-variant comparison helper).- range expressions are restricted to
for-loop iterables and range-index assignment targets - functions, methods, generic functions, and variant arms must be called — they are not usable as bare values
read(...)of a raw pointer requiresunsafe
breakandcontinuemust be inside loopsreturnis not allowed insidedeferblocksmatchonenum/variantmust be exhaustive unless_is present;matchon integer requires_let ... else:andvar ... else:requireT?,Option[T], orResult[T, E]; theelseblock must terminate control flow?propagation is only allowed inside function and proc bodies, not indeferblocksawaitis only allowed inside async functions
:.Nprecision is only valid onfloatanddoubleinterpolations:x/:X,:o/:O, and:b/:Bare only valid on integer primitives and integer-backed enums/flags
- external files cannot contain
attribute,event,var,variant,interface,extending,foreign function, ordinaryfunction,async function, orstatic_assertdeclarations publicis rejected in external files (declarations are implicitly exported)importstatements must appear before directives and declarations
- user-defined
attributedeclarations targetstruct,field,callable,const,event,enum,flags,union, orvariant @[...]attribute applications are only accepted on the above declaration kindsattributedeclarations are not allowed in external files- only built-in
packedandalign(...)struct attributes are allowed in external files
parallel foronly supports range iteration (0..N); collection iteration is not supportedparallel forand bodies rejectbreak,continue,return,defer, and nestedparallel forref[T]captures are rejected at thread boundaries in bothparallel forandparallel:blocksparallel:blocks enforce single-writer-or-multiple-readers: a variable written in one statement cannot be accessed by anotheratomic[T]requiresTto be a primitive integer type orboolatomic[T]methods (store,add,sub,exchange) require an editable (mutable) receiveratomic[T].compare_exchangeis accepted by the type checker but not yet implemented in the lowering; usestd.sync.AtomicUintfor compare-exchange operations
The mtc CLI is the primary tool for checking, building, and running Milk Tea programs.
Essential commands:
mtc check <path> # Type-check + lint; reports all diagnostics sorted by line
mtc build <path> # Build only (emit C, compile, link, --no-cache to build without cache)
mtc run <path> # Build and execute (--no-cache to build and run without cache)
mtc debug <file.mt> # Print debug info (tokens, AST, facts, bindings, diagnostics)
mtc emit-c <path> # Emit generated C to stdout
mtc format <path> # Format sources in place (--check for dry-run)
mtc lint <path> # Run linter (--fix to apply fixes, --select/--ignore to filter)
mtc new <name> # Scaffold a new package (package.toml + src/main.mt)
mtc cache status # Show build cache stats
mtc lex <file.mt> # Print lexer token stream
mtc parse <path> # Print parsed AST
mtc lower <path> # Print lowered IR
Package management:
mtc deps tree <path> # Print the dependency graph
mtc deps lock <path> # Write/refresh package.lock
mtc deps add <path> <name> # Add a dependency
mtc deps remove <path> <name> # Remove a dependency
mtc deps update <path> # Update dependencies
mtc deps publish <path> # Publish a package to the local registry
mtc deps fetch <path> # Materialize cache-backed sources
Run a pre-built module (no compilation):
mtc run-module <module> # Run compiled module by name (e.g. std.fmt.bench)
Toolchain maintenance:
mtc toolchain bootstrap # Bootstrap the native toolchain
mtc toolchain doctor # Diagnose toolchain setup
mtc toolchain tools # List available native tools
Build and run commands support --profile, --platform, --cc, --keep-c, --locked, --frozen, and -I include paths. Dependency-locked flows support --locked (use package.lock) and --frozen (require current package.lock).
Diagnostic output uses standard compiler format (file:line:column with source context, error codes, and caret highlighting):
[E0001] error: unknown type floa
--> file.mt:1:16
|
1 | type Seconds = floa
| ^~~~
note: did you mean 'float'?
error: could not check due to 1 previous error
mtc check surfaces both errors and linter warnings:
| Severity | Label | Meaning |
|---|---|---|
error: |
red | semantic errors, linter errors |
warning: |
yellow | linter warnings (dead-assignment, etc.) |
hint: |
cyan | style suggestions (prefer-let, etc.) |
Exit code is 0 on success, 1 when errors are present (warnings/hints alone do not fail).
struct Counter:
value: int
extending Counter:
editable function bump() -> void:
this.value += 1
function read() -> int:
return this.value
function main() -> int:
var c = Counter(value = 0)
for i in 0..3:
c.bump()
let text = f"count=#{c.read()}"
return 0