Milk Tea (Simple Game) Programming Language
A statically typed, indentation-based systems language for games. Compiles to readable C. Safe by default, explicit by design.
gem install mt-lang
mtc run hello.mt
VS Code extension — syntax highlighting, IntelliSense, debugging
Language Reference — full syntax, types, and standard library
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.
1. File Kinds And Layout
- 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 -
discount
This also applies to range expressions:
let values = 1 ..
4
Do 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.
2. Literals And Tokens
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
3. Top-Level Declarations
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.
4. Variables And Guards
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 1
Tuple destructuring:
let (a, b) = pair()
let (x, y) = (1, 2)
let (_, _, title) = triple # _ may appear more than once
Struct destructuring:
struct Vec2:
x: float
y: float
let p = Vec2(x = 1.0, y = 2.0)
let Vec2(x, y) = p
Rules 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.
5. Data Declarations
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
Backing type defaults to int; values auto-increment from 0:
enum Color:
red
green
blue
flags Mask: uint
a = 1 << 0
b = 1 << 1
opaque SDL_Window
variant Token:
ident(text: str)
number(value: int)
eof
Rules:
structandopaquemay declare nominal interface conformance withimplements.attribute[target, ...]declares reusable declaration attributes forstruct,field,callable,const,event,enum,flags,union, andvarianttargets.- Attributes are applied with one or more leading
@[name(...)]blocks. Built-inpacked,align(bytes), anddeprecated(message)are predefined attributes. variantarms may carry named payload fields.- Payload arm construction uses named fields:
Token.ident(text = "hello"). - No-payload arms are bare member expressions:
Token.eof. enumandflagsbacking types must be integer primitives.enumandflagsmembers must be compile-time integer constants.enumbacking types default tointwhen omitted; member values auto-increment from 0 or the previous explicit value.flagsmembers may reference earlier members to spell composite aliases such asread_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, andcallable_of. field_of(...),callable_of(...), andattribute_of(...)produce compile-time handle values with source-visible handle typesfield_handle,callable_handle, andattribute_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.exeis not a supported backend for these attributes today. On wasm/browser targets the same feature works through Emscriptenemcc, 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:
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.
7. Functions, Externals, And Foreign Functions
Ordinary functions:
- Parameters must be typed.
- Parameters are non-rebindable.
- Parameters may include default values using
name: type = default_expr. All required parameters must appear before defaulted parameters. Default calls are resolved at compile time by inlining the expression at each call site that omits the argument. Defaults are not supported onexternal functionorforeign function. - Return type defaults to
voidif omitted. - Generic functions are supported.
- Generic function and method type parameters may use
implementsconstraints.
function configure(host: str, port: int = 8080, debug: bool = false) -> void:
pass
configure("localhost") # port=8080, debug=false
configure("localhost", port = 3000) # debug=false
configure(host = "remote", debug = true) # port=8080
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 time
const function also generates a normal runtime function, callable from ordinary runtime code.
External functions:
external function printf(format: cstr, ...) -> int
Raw 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.CloseWindow
Parameter 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.
8. Statements And Control Flow
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.ifsupports an inline single-statement form:if cond: stmt else: stmt. The body is a single statement on the same line instead of an indented block.
match supports the following scrutinee types:
- enum scrutinees
- variant scrutinees
- integer scrutinees
strscrutinees- tuple 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"
Integer match arms also support range patterns via start..end:
let tier = match score:
0..10: "low"
11..50: "mid"
_: "high"
Range arms are refutable — they do not count toward exhaustiveness — and require _.
match rules:
- Enum and variant matches must be exhaustive unless
_is present. - Integer,
str, and tuple matches require_. Integer match arms accept integer literals and char literals;strmatch arms accept string literals; tuple match arms accept tuple literal patterns whose elements may be literals, char literals, string literals, or_discard. - 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)
Tuple match example:
# Expression form
let label = match rgb:
(255, 0, 0): "red"
(0, 255, 0): "green"
_: "other"
# Statement form with wildcards
match point:
(_, y) if y > 0:
handle_above(y)
_:
handle_other()
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._may appear more than once. - 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] * dt
Rules:
- 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, b
Rules:
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.
- A compile-time type comparison is a valid condition:
T == int,field.type == float, etc., whereTis a generic type parameter andfield.typeis a reflected field type — enabling type-based dispatch in generic bodies. - 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.
9. Expressions And Operators
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:
orandnot(unary prefix)is|^&==,!=<,<=,>,>=<<,>>+,-*,/,%
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- - SIMD vectors (
simd[T, N]):+,-,*,/(component-wise same-type or scalar),%,&,|,^,~(integer lanes),<<,>>(integer lanes, scalar shift), unary-/~(integer lanes for~)
10. Type System
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]own[T]— owning heap pointer: auto-dereferences likerefbut storable, returnable, and nullable. Created viaheap.must_alloc[T](count). Compiles toT*.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 rowisimd[T, N]— SIMD vector:Nnumeric lanes (128 or 256 bits). Component-wise+-*/%&|^~<<>>. Lane access via[i](compile-time index). Lowers to GCC/Clang vector extensions for portable x86/ARM vector code.dyn[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?and is valid for any type. - For pointer-like bases (
ptr[T],const_ptr[T],own[T],cstr,fn(...),proc(...), opaque),T?is a nullable pointer andnullis the absent value. - For non-pointer value bases (
int,bool,float, structs, ...),T?is stored inline by value as a tagged optional (a presence flag plus the value). It copies by value with no hidden heap allocation or pointer aliasing. - Use
nullfor absence in any nullable context. - In nullable pointer-like contexts, prefer
nulloverzero[ptr[T]]. ref[T]is non-null and cannot be nullable.- At an FFI boundary (
external/foreign functionparameters and returns), only pointer-likeT?is allowed. A non-pointer value nullable such asint?is rejected — useptr[T]?or pass an explicit struct.
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.
11. Built-In Callable Surface
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.
A field_handle's .type is usable directly in type position within a compile-time context (e.g. inside inline for field in fields_of(T)): const_ptr[field.type], equal[field.type](...), Vec[field.type], etc. resolve to the field's concrete type. This powers reflective generics such as std.hash's equal_struct/hash_struct/order_struct (per-field canonical-hook dispatch) and std.fmt.format_value[T], which renders any struct as { field = value, ... }.
Standard library
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 the primitive integer types (byte/ubyte/short/ushort/int/uint/long/ulong/ptr_int/ptr_uint),bool,float,double, andcharwith canonicalhash/equal/orderhooks; import once to use primitives as Map/Set/BinaryHeap/OrderedMap keys (strkeys come fromstd.str). Also provides generichash_struct[T],equal_struct[T],order_struct[T]that dispatch each field through its own canonical hook viafield.type(content-correct, includingstrand nested-struct fields).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,std.mem.tracking,std.mem.endian— allocators and memory utilitiesstd.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], std.bitset.Bitset, std.spatial.SpatialGrid[T], std.ring_buffer.RingBuffer[T], std.sparse_set.SparseSet[T], std.lru_cache.LruCache[K,V]
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),RingBuffer,SparseSet next() -> bool+current():OrderedMap.entries/iter,Map.entries/iter,LinkedMap.entries/iter,Counter.counts/entries/iter,MultiSet.entries/iter,SnapshotValues/SnapshotEntries
12. Strings, Text, And Builders
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.
13. Safety And Conversion Rules
- 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.
- Non-widening integer conversions (narrowing, signed → unsigned) require an explicit cast. Lossless widening conversions (same signedness to wider or equal width, unsigned → wider signed) are implicit.
%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.
14. Async
Example:
async function child() -> int:
return 41
async function parent() -> int:
let v = await child()
return v + 1
Rules:
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}")
15. Events
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.
16. Language Limitations
Current compiler rejects:
Function and external restrictions
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 statementsexternal functionandforeign functioncannot use a non-pointer value nullable (such asint?) as a parameter or return type; only pointer-likeT?may cross an FFI boundarymaincannot 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
Parameters and variables
- 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]
Event restrictions
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 and unsafe requirements
- 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
Type system restrictions
- conditions must be
bool; integers and pointers have no implicit truthy or falsy coercion - mixed signed and unsigned integer arithmetic requires an explicit cast
- non-widening integer conversions (narrowing, signed → unsigned) require an explicit
T<-valuecast; lossless widening is implicit - 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 restrictions
- 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
Operator and expression restrictions
+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
Control flow restrictions
breakandcontinuemust be inside loopsreturnis not allowed insidedeferblocksmatchonenum/variantmust be exhaustive unless_is present;matchon integer orstrrequires_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
Format string restrictions
:.Nprecision is only valid onfloatanddoubleinterpolations:x/:X,:o/:O, and:b/:Bare only valid on integer primitives and integer-backed enums/flags
External file restrictions
- 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
Attribute restrictions
- 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
Concurrency restrictions
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
17. CLI Commands
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 test <path> # Discover and run @[test] functions (--timeout, --mem, --jobs)
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).
Global options work with any command (before or after the subcommand, up to a -- separator): -h/--help (also mtc help <command> for command-specific help), -V/--version, -q/--quiet (suppress informational output), -v/--verbose (per-file progress), and --color auto|always|never. Generate shell completions with mtc completions bash|zsh|fish. Timing breakdowns use --timings (on format, lint, build, run, test); lint reports warnings with per-file progress.
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).
18. Minimal Example
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