Milk Tea Reference
A statically typed, indentation-based systems language for games. Safe by default, explicit by design, compiling to beautiful C.
$ gem install mt-lang
$ mtc run hello.mt
gem install mt-lang
💻 VS Code Extension
Overview
Milk Tea code looks like code, not punctuation. It uses indentation for blocks, words over symbols, and explicit surface for allocation and unsafe operations. The compiler mtc checks, builds, and runs .mt source files, emitting readable C that a human can debug.
## A minimal Milk Tea program.
import std.stdio as io
function main() -> int:
io.print_line("Hello, Milk Tea!")
return 0
Core Design
| Principle | Description |
|---|---|
| What you see is what runs | No hidden allocation, dispatch, or heap traffic. Every cost is spelled in source. |
| C is the ABI ground truth | Structs, unions, pointers, and calling conventions map directly without lossy translation. |
| Safe by default, unsafe by choice | Pointer operations require unsafe. Indexing is bounds-checked. Conversions are explicit. |
| Data-oriented first | Plain structs, arrays, spans, pools, and arenas matter more than elaborate object systems. |
| One job, one canonical surface | No duplicated everyday spellings for the same concept. |
Getting Started
Hello World
function main() -> int:
return 0
Save as hello.mt and run:
mtc run hello.mt
Essential CLI
| Command | Description |
|---|---|
mtc check <path> | Type-check + lint; reports all diagnostics sorted by line |
mtc run <path> | Build and execute |
mtc build <path> | Build only (emit C, compile, link) |
mtc format <path> | Format sources in place (--check for dry-run) |
mtc lint <path> | Run linter (--fix to apply fixes) |
mtc test <path> | Discover and run @[test] functions |
mtc new <name> | Scaffold a new package |
See CLI Reference for the full command surface.
Generated C
Milk Tea compiles to readable C that mirrors the source structure. No hidden runtime, no hidden heap traffic, no hidden dispatch. The output is designed for a human to debug, diff, and ship.
## Milk Tea
function add(a: int, b: int) -> int:
return a + b
struct Vec2:
x: float
y: float
extending Vec2:
editable function scale(factor: float):
this.x *= factor
this.y *= factor
/* Generated C */
int main_add(int a, int b) {
return a + b;
}
typedef struct { float x; float y; } main_Vec2;
void main_Vec2_scale(main_Vec2* this, float factor) {
this->x *= factor;
this->y *= factor;
}
editable function receives a writable pointer prefix. function (value receiver) passes the struct by value. static function receives no receiver. defer lowers to C cleanup labels. Module names prefix generated symbols to avoid collisions.
Milk Tea at a Glance
A single program demonstrating the breadth of Milk Tea: data types, interfaces, generics, control flow, error handling, closures, compile-time reflection, unsafe, events, async, and more. Section headers in ## doc comments mark each feature.
## ── Imports ──────────────────────────────────────────────────────────
import std.stdio as io
import std.hash
import std.vec
import std.map
import std.math
import std.mem.heap as heap
import std.fmt
## ── Constants ────────────────────────────────────────────────────────
const MAX_ENTITIES: int = 256
const GRAVITY: float = 9.81
## Block-bodied constant (computed at compile time)
const BUFFER_SIZE -> int:
var n: int = 64
while n < MAX_ENTITIES:
n = n * 2
return n
## const function: evaluable at compile time, also callable at runtime
const function square(x: int) -> int:
return x * x
const TILE_AREA: int = square(16)
## ── Type Alias ───────────────────────────────────────────────────────
type EntityId = uint
## ── Enum (explicit backing type, exhaustive match) ───────────────────
enum EntityKind: ubyte
player = 0
enemy = 1
npc = 2
## ── Flags (named bitmask with composite alias) ──────────────────────
flags StatusFlags: uint
poisoned = 1 << 0
stunned = 1 << 1
shielded = 1 << 2
debuffed = StatusFlags.poisoned | StatusFlags.stunned
## ── Struct with nested struct ────────────────────────────────────────
struct Entity:
id: EntityId
kind: EntityKind
name: str
hp: int
max_hp: int
position: vec3
struct Stats:
attack: int
defense: int
speed: float
stats: Stats
## ── Variant (tagged union with payload arms) ────────────────────────
variant Action:
move(dx: float, dy: float)
attack(target_id: EntityId, damage: int)
heal(amount: int)
idle
## ── Interfaces (nominal contracts, no hidden dispatch) ──────────────
public interface Damageable:
editable function take_damage(amount: int) -> void
function is_alive() -> bool
interface Named:
function display_name() -> str
## ── Struct implementing multiple interfaces ─────────────────────────
struct Player implements Damageable, Named:
entity: Entity
score: int
level: int
## ── User-defined attribute ──────────────────────────────────────────
attribute[field] column(name: str)
struct SaveData:
@[column(name = "lvl")]
level: int
@[column(name = "pts")]
score: int
## ── Opaque type (unknown C layout) ──────────────────────────────────
opaque NativeHandle
## ── Methods via extending ───────────────────────────────────────────
## function = value receiver, editable function = mutable, static = no receiver
extending Entity:
function is_alive() -> bool:
return this.hp > 0
editable function apply_damage(amount: int) -> void:
let actual = amount - this.stats.defense
if actual > 0:
this.hp -= actual
if this.hp < 0:
this.hp = 0
static function create(id: EntityId, kind: EntityKind, name: str) -> Entity:
return Entity(
id = id,
kind = kind,
name = name,
hp = 100,
max_hp = 100,
position = vec3(x = 0.0, y = 0.0, z = 0.0),
stats = Entity.Stats(attack = 10, defense = 5, speed = 1.0),
)
extending Player:
editable function take_damage(amount: int) -> void:
this.entity.apply_damage(amount)
function is_alive() -> bool:
return this.entity.is_alive()
function display_name() -> str:
return this.entity.name
## .with() returns a copy with specified fields replaced
function with_score(new_score: int) -> Player:
return this.with(score = new_score)
## ── Hash/equal hooks (enable use as Map/Set keys) ───────────────────
extending Entity.Stats:
static function hash(value: const_ptr[Entity.Stats]) -> uint:
return unsafe: uint<-read(value).attack ^ uint<-read(value).defense
static function equal(a: const_ptr[Entity.Stats], b: const_ptr[Entity.Stats]) -> bool:
let la = unsafe: read(a)
let ra = unsafe: read(b)
return la.attack == ra.attack and la.defense == ra.defense
## ── Generic function with interface constraint ──────────────────────
function damage_all[T implements Damageable](targets: span[T], amount: int) -> void:
for i in 0..targets.len:
if targets[i].is_alive():
targets[i].take_damage(amount)
## Multiple constraints with 'and'
function describe[T implements Damageable and Named](target: ref[T]) -> str:
let alive = if target.is_alive(): "alive" else: "dead"
return f"#{target.display_name()} (#{alive})"
## ── Value parameter generic [N: int] ────────────────────────────────
function make_buffer[N: int]() -> str_buffer[N]:
var buf: str_buffer[N]
return buf
## ── Error handling with Result[T, E] ────────────────────────────────
enum LoadError: ubyte
not_found = 1
invalid = 2
function load_entity(id: EntityId) -> Result[Entity, LoadError]:
if id == 0:
return Result[Entity, LoadError].failure(error = LoadError.not_found)
return Result[Entity, LoadError].success(
value = Entity.create(id, EntityKind.player, "Hero"),
)
## Postfix ? propagation (enclosing must return compatible Result or Option)
function load_pair() -> Result[bool, LoadError]:
let first = load_entity(1)?
let second = load_entity(2)?
io.print_line(f"Loaded #{first.name} and #{second.name}")
return Result[bool, LoadError].success(value = true)
## ── Events (fixed-capacity pub/sub, zero heap during dispatch) ──────
struct DamageEvent:
target_id: EntityId
amount: int
event entity_damaged[8](DamageEvent)
function on_damage(evt: DamageEvent) -> void:
io.print_line(f"entity #{evt.target_id} took #{evt.amount} damage")
## ── Proc (closure with value captures) ──────────────────────────────
function make_scaler(factor: float) -> proc(x: float) -> float:
return proc(x: float) -> float: x * factor
## ── Async / await ───────────────────────────────────────────────────
async function fetch_score(id: EntityId) -> int:
return 42
async function load_scores() -> int:
let a = await fetch_score(1)
let b = await fetch_score(2)
return a + b
## ── Compile-time constants for inline if ─────────────────────────────
const DEBUG: bool = false
## ── Main entry point ────────────────────────────────────────────────
function main() -> int:
## Local declarations: let (immutable), var (mutable)
let width: int = 800
var score: int = 0
## str_buffer: fixed-capacity mutable UTF-8 text
var name_buf: str_buffer[64]
name_buf.assign("World")
name_buf.append("!")
io.print_line(name_buf.as_str())
## Struct construction with named fields
var player = Player(
entity = Entity.create(1, EntityKind.player, "Hero"),
score = 0,
level = 1,
)
## Tuple construction & destructuring
let pair = (42, "hello")
let (count, greeting) = pair
let named_tuple = (x = 10, y = 20)
let (tx, ty) = named_tuple
## Struct destructuring
let Entity.Stats(attack, defense, speed) = player.entity.stats
## Guard binding: let...else: (initializer must be T?, Option[T], or Result[T, E])
let entity = load_entity(1) else:
return 1
## Guard with error binding
let loaded = load_pair() else as error:
io.print_line(f"load failed: #{error}")
return 1
## Native vector types & arithmetic
var position = vec3(x = 1.0, y = 2.0, z = 3.0)
let direction = vec3(x = 0.0, y = 1.0, z = 0.0)
position = position + direction * 0.5
let dist = math.sqrt(double<-(position.x * position.x + position.y * position.y))
## Array, span, recoverable indexing
var scores = array[int, 4](10, 20, 30, 40)
scores[0] = 100
let view: span[int] = scores.as_span()
let maybe_val = get(scores, 99)
if maybe_val == null:
io.print_line("Index out of bounds (safe)")
## Range index assignment
var buf: array[float, 4]
buf[0..3] = (1.0, 2.0, 3.0)
## Collections: Vec and Map (import std.hash for primitive keys)
var entities = vec.Vec[Entity].create()
defer entities.release()
entities.push(Entity.create(1, EntityKind.player, "Hero"))
entities.push(Entity.create(2, EntityKind.enemy, "Goblin"))
var name_counts = map.Map[str, int].create()
defer name_counts.release()
name_counts.set("player", 1)
name_counts.set("enemy", 5)
## for loop: range
for i in 0..3:
io.print_line(f"index #{i}")
## for loop: array iteration with break / continue
let values = array[int, 5](1, 2, 3, 4, 5)
for v in values:
if v == 3:
continue
if v == 5:
break
io.print_line(f"value #{v}")
## Parallel for (arrays/spans only, lengths must match)
let xs = array[float, 3](1.0, 2.0, 3.0)
let ys = array[float, 3](4.0, 5.0, 6.0)
for x, y in xs, ys:
io.print_line(f"#{x:.1} + #{y:.1}")
## while loop
var n: int = 1
while n < 100:
if n > 50:
break
n = n * 2
## if / else if / else (conditions must be bool, no truthy coercion)
if player.level > 10:
io.print_line("veteran")
else if player.level > 5:
io.print_line("experienced")
else:
io.print_line("novice")
## if-expression
let rank = if player.level >= 10: "master" else: "apprentice"
## match: enum (must be exhaustive or have _)
match player.entity.kind:
EntityKind.player:
io.print_line("is player")
EntityKind.enemy:
io.print_line("is enemy")
_:
io.print_line("other")
## match: variant with struct patterns and guards
let action = Action.attack(target_id = 2, damage = 15)
match action:
Action.move(dx, dy):
io.print_line(f"move #{dx:.2}, #{dy:.2}")
Action.attack(target_id, damage > 10):
io.print_line(f"heavy attack on #{target_id}")
Action.attack(target_id, damage):
io.print_line(f"light attack on #{target_id}")
Action.heal(amount):
io.print_line(f"heal #{amount}")
Action.idle:
pass
## match expression
let label = match player.level:
1: "beginner"
2: "novice"
_: "advanced"
## Explicit cast: T<-value
let raw_kind = ubyte<-player.entity.kind
let ratio = float<-score
## Format strings: interpolation, precision, hex, owned text
io.print_line(f"Player: #{player.entity.name} HP=#{player.entity.hp}")
io.print_line(f"Position: #{position.x:.2}, #{position.y:.2}")
io.print_line(f"ID hex: #{player.entity.id:x}")
var msg = fmt.format(f"Score: #{score}")
defer msg.release()
## Heredoc string
let help = <<-HELP
Commands:
move, attack, quit
HELP
io.print_line(help)
## Defer: cleanup at scope exit (lowers to C cleanup labels)
let data: own[int] = heap.must_alloc[int](1)
defer heap.release[int](data)
## Proc / closure
let scale = make_scaler(2.5)
let scaled = scale(10.0)
## Nullability: T? for any type (pointer-like shown here)
var maybe_target: ptr[Entity]? = null
if maybe_target == null:
io.print_line("no target")
## unsafe: required for pointer indexing, dereference, arithmetic, casts
unsafe:
let p = ptr_of(score)
read(p) = 42
## ref[T]: safe non-null writable alias (implicit borrow at call site)
let handle = ref_of(player)
handle.take_damage(10)
io.print_line(f"HP after damage: #{player.entity.hp}")
## dyn[Interface] + adapt: runtime polymorphism (fat pointer)
let d: dyn[Damageable] = adapt[Damageable](ref_of(player))
d.take_damage(5)
## Events: subscribe, emit (emit only from declaring module)
let sub = entity_damaged.subscribe(on_damage) else:
return 1
entity_damaged.emit(DamageEvent(target_id = 1, amount = 25))
entity_damaged.unsubscribe(sub)
## .with() partial field update
let updated = player.with_score(999)
## Compile-time reflection: inline for over struct fields
inline for field in fields_of(Entity.Stats):
io.print_line(f"field: #{field.name}")
## inline if: dead branch elimination (dead branch may reference nonexistent symbols)
inline if DEBUG:
io.print_line("debug mode")
## ── Concurrency ─────────────────────────────────────────────────────
## parallel for: data-parallel loop dispatched across CPU cores
var positions = array[float, 4](0.0, 1.0, 2.0, 3.0)
var velocities = array[float, 4](0.1, 0.2, 0.3, 0.4)
let dt: float = 0.016
parallel for i in 0..4:
positions[i] += velocities[i] * dt
## parallel: block for structured fork-join
var pa: int = 0
var pb: int = 0
parallel:
pa = 42
pb = 99
## atomic[T]: lock-free concurrent access
var counter: atomic[int]
counter.store(0)
let prev = counter.add(1)
## detach + gather: detached concurrency with explicit join
let a = detach load_entity(100)
let b = detach load_entity(200)
gather a, b
## static_assert: compile-time assertions
static_assert(size_of(int) == 4, "expected 32-bit int")
static_assert(MAX_ENTITIES > 0, "need at least one entity")
io.print_line(f"All done! Score=#{score}")
return 0
const function, type aliases, enums, flags, structs (nested, attributed), variants, interfaces, extending methods, generics with constraints, value parameter generics, Result error handling with ? and guard binding, events, closures (proc), async/await, concurrency (parallel for, parallel:, detach + gather, atomic[T]), native vector types, arrays, spans, collections, for/while/match/if, variant membership test (is), parallel for, defer, unsafe, ref[T], own[T], dyn[Interface], format strings, heredocs, str_buffer, compile-time reflection, inline for/inline if, static_assert, and explicit casts. See the sections below for full details on each feature.
Source Files & Modules
Milk Tea source files use the .mt extension. A file is either an ordinary source file or an external file for raw ABI bindings. There are no wildcard imports — every module must be imported by name.
Ordinary Files
The normal source form. No module header; module identity is inferred from the file path relative to package.source_root. import statements appear only at the top.
import std.math as math
import std.raylib as rl
function main() -> int:
return 0
External Files
Dedicated raw ABI surface, usually for std.c.* bindings and bindgen output. Start with external and accept include, link, and compiler_flag directives. After directives, only raw ABI declarations are allowed. The mtc bindgen command generates external files from C headers.
external
include "raylib.h"
link "raylib"
compiler_flag "-DPLATFORM_DESKTOP"
struct Color:
r: ubyte
g: ubyte
b: ubyte
a: ubyte
external function InitWindow(width: int, height: int, title: cstr) -> void
Module Resolution
Module lookup resolves a.b.c to a/b/c.mt. For an active target platform P, the compiler prefers a/b/c.P.mt and falls back to a/b/c.mt. Platform-specific files still map to module name. Valid platform suffixes: linux, windows, wasm. There is no #if or #ifdef — use when, inline if, or platform-specific file variants instead.
import std.str
import std.vec
import mylib.utils
Lexical Rules
Indentation
:starts a block- Indentation must be spaces only (tabs are rejected)
- Indentation must be a multiple of 4 spaces
- Indentation increases by one level at a time
- Newlines end statements except inside
()and[], or when the previous line ends with a binary operator
## Preferred: wrap in delimiters.
let total = (
subtotal
+ tax
- discount
)
## Also accepted: operator-led continuation.
let total = subtotal +
tax -
discount
Naming Conventions
| Category | Style | Example |
|---|---|---|
| Modules, variables, functions | snake_case | player_count, load_level |
| Types (struct, enum, variant, etc.) | PascalCase | Vec2, LoadError, EventKind |
| C binding modules | Preserve C names exactly | SDL_Window, c.InitWindow |
Comments
| Form | Description |
|---|---|
# text | Line comment |
## text | Documentation comment; attaches to the next declaration (no blank line) |
Literals
| Kind | Examples | Type |
|---|---|---|
| Integer | 42, 0xff, 0b1010, 1_000_000 | int (default) |
| Float | 3.14, 1.2e-3, 1.0f, 1.0d | float (default) |
| Character | 'a', '\n', '\t', '\0', '\x41' | ubyte |
| Boolean | true, false | bool |
| String | "hello" | str |
| C String | c"hello" | cstr |
| Format String | f"count=#{n}" | str |
| Heredoc String | <<-TAG ... TAG | str |
| Heredoc C String | c<<-TAG ... TAG | cstr |
| Heredoc Format | f<<-TAG ... TAG | str |
| Null | null, null[ptr[char]] | T? |
Digits may be grouped with _ separators: 1_000_000, 0xFF_FF, 0b1010_0101. Integer literals accept type suffixes: 42u (uint), 0xFFub (ubyte), 100z (ptr_uint), 7i (int), -1l (long). Float literals accept f (float) and d (double) suffixes: 1.0f, 1.0d.
Operators & Punctuation
| Category | Tokens |
|---|---|
| Delimiters | ( ) [ ] |
| Separators / Access | : , . |
| Type markers | -> ? |
| Arithmetic | + - * / % |
| Bitwise | ~ & | ^ << >> |
| Comparison | == != < <= > >= |
| Assignment | = += -= *= /= %= &= |= ^= <<= >>= |
| Variadic | ... |
| Word operators | and or not |
| Pattern test | is — variant arm membership test, desugars to match |
| Parameter modes | in out inout (reserved for foreign function) |
Variables & Guards
Local Declarations
let width: int = 1280 ## immutable
var score: int = 0 ## mutable
let dt = rl.get_frame_time() ## type inferred (float)
var player_count = 0 ## type inferred (int)
## Zero-initialized (explicit type required, must be zero-initializable):
var scratch: array[ubyte, 256]
Constants
const GRAVITY: float = 9.81
## Block-bodied const (evaluated at compile time)
const NEXT_POW2 -> int:
var n: int = 1
while n < 1024:
n = n * 2
return n
Destructuring
## Tuple 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) = p
Guard Binding
The initializer must be T?, Option[T], or Result[T, E]. The else block must terminate control flow.
let window = maybe_window else:
return 1
let image = load_image(path) else:
return 1
let config = load_config() else as error:
return error
let _ = initialize_runtime() else:
return 1
Postfix Result/Option Propagation (?)
expr? unwraps Option[T] or Result[T, E] on success; on failure (None or failure(error)), returns early from the enclosing function or proc. Only allowed inside function and proc bodies.
let parsed = parse(input)?
let lowered = lower(parsed)?
return Result[Output, Error].success(value = lowered)
Data Declarations
Type Alias
type Seconds = float
type Callback = fn(level: int, message: cstr) -> void
Struct
struct Vec2:
x: float
y: float
## With attributes
@[packed]
struct Header:
tag: ubyte
@[align(16)]
struct Mat4:
data: array[float, 16]
## Nested structs (qualified as Parent.Nested)
struct Rectangle:
x: float
y: float
struct Edge:
start: float
end: float
top_edge: Edge
left_edge: Edge
Union
union Number:
i: int
f: float
Enum
Backing type defaults to int; values auto-increment from 0 or the previous explicit value. Both are optional.
## Explicit type and values (classic form)
enum State: ubyte
idle = 0
running = 1
## Backing type and values both optional
enum Color:
red ## 0
green ## 1
blue ## 2
## Auto-increment from explicit value
enum Status:
idle = 10 ## 10
running ## 11
stopped ## 12
Flags
flags Mask: uint
a = 1 << 0
b = 1 << 1
both = Mask.a | Mask.b ## composite alias
Variant (Tagged Union)
variant Token:
ident(text: str)
number(value: int)
eof
## Constructors
let t1 = Token.ident(text = "hello") ## payload arm
let t2 = Token.eof ## no-payload arm
## Generic variants
variant Option[T]:
some(value: T)
none
variant Result[T, E]:
success(value: T)
failure(error: E)
Option[T] and Result[T, E] are auto-imported — no import statement is needed. They are available in every Milk Tea source file. Their extending methods (is_some, unwrap, is_success, map_error, etc.) are always accessible.
Opaque
For C handles whose layout is unknown. Opaque types may implement interfaces, enabling constrained generics and dyn dispatch over C handles.
opaque SDL_Window implements Closable
opaque ma_engine
Attributes
User-defined attributes target specific declaration kinds: attribute[field], attribute[callable], attribute[const], attribute[event], attribute[enum], attribute[flags], attribute[union], attribute[variant], attribute[struct]. Multiple targets may be combined: attribute[const, event]. Built-in attributes: packed, align(bytes), deprecated(message).
attribute[field] rename(name: str)
attribute[struct, event] trace(name: str)
@[trace(name = "player")]
struct Player:
@[rename(name = "hp")]
health: int
static_assert
static_assert(condition, message) checks a compile-time boolean condition. If false, compilation fails with the given message. Valid at module level and inside function bodies, inline for loops, and const function bodies.
static_assert(size_of(int) == 4, "expected 32-bit int")
static_assert(MAX_ENTITIES > 0, "need at least one entity")
Interfaces & Methods
Interfaces
Explicit nominal method-set contracts for static polymorphism. No inheritance, no hidden dispatch.
public interface Damageable:
editable function take_damage(amount: int) -> void
function is_alive() -> bool
## Generic interface
interface Mapper[T]:
function map(x: T) -> T
## Conformance on nominal type
struct NPC implements Damageable:
hp: int
name: str
Methods (extending)
Three kinds: function (value receiver), editable function (editable receiver), static function (no receiver).
extending Counter:
function read() -> int:
return this.value
editable function bump() -> void:
this.value += 1
static function zero() -> Counter:
return Counter(value = 0)
Dynamic Dispatch (dyn[I])
Runtime interface values are fat pointers (data + vtable). Construct with adapt[I](value: ref[T]). For generic interfaces, the type parameter must be fully specified: dyn[Mapper[int]] is valid; dyn[Mapper] is rejected.
function render_all(drawables: span[dyn[Drawable]]):
for d in drawables:
d.draw()
let d: dyn[Drawable] = adapt[Drawable](ref_of(entity))
Type System
Primitive Types
| Type | Description |
|---|---|
bool | Boolean |
byte short int long | Signed integers |
ubyte ushort uint ulong | Unsigned integers |
char | Single-byte character (ABI-facing). Not a general arithmetic integer type — cast to an integer explicitly for arithmetic. |
ptr_int ptr_uint | Pointer-sized integers |
float double | Floating-point |
void | No value |
str | UTF-8 string view (borrowed) |
cstr | NUL-terminated C string |
vec2 vec3 vec4 | Float vectors with .x .y .z .w |
ivec2 ivec3 ivec4 | Integer vectors |
mat3 mat4 | Column-major matrices |
quat | Quaternion (layout-compatible with vec4) |
Primitive type names (int, float, str, bool, etc.) are reserved and cannot be reused for variable bindings, parameters, locals, import aliases, or type parameters.
Native Type Construction
let v = vec3(x = 1.0, y = 2.0, z = 3.0)
let m = 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)
Type Constructors
| Constructor | Description |
|---|---|
ptr[T] | Raw writable pointer |
const_ptr[T] | Raw read-only pointer |
own[T] | Owning heap pointer — auto-dereferences like ref but storable, returnable, and nullable. Created via heap.must_alloc[T](count). Compiles to T*. |
ref[T] | Safe non-null writable alias |
span[T] | Pointer + length view ({ data, len }) |
array[T, N] | Fixed-size array |
str_buffer[N] | Fixed-capacity mutable UTF-8 text |
Task[T] | Async task handle |
Option[T] | Optional value variant |
Result[T, E] | Success or failure variant |
fn(params...) -> R | Function pointer type |
proc(params...) -> R | Closure type (value captures) |
SoA[T, N] | Structure-of-Arrays: fields become separate arrays |
dyn[Interface] | Runtime interface value (fat pointer) |
atomic[T] | Atomic value for lock-free concurrent access |
(T, U) | Tuple type |
Nullability
T? 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 and null reuses the null pointer as 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 null to express absence; use zero[T] for generic value-initialization. zero[ptr[T]] is rejected when the expected type is already a nullable pointer-like type — write null instead.
At an FFI boundary, only pointer-like T? is allowed: external / foreign function parameters and returns reject a non-pointer value nullable such as int? — use ptr[T]? or pass an explicit struct.
let window: ptr[Window]? = null
let name: cstr? = null
let port: int? = null ## value-type optional, stored inline
## Typed null for ambiguous contexts (target must be pointer-like)
let handle = null[ptr[char]]
Memory Model
Milk Tea follows value semantics by default. Scalars, structs, and fixed arrays copy by value. There is no hidden heap boxing, no garbage collector, and no hidden reference counting. Heap allocation is always explicit and allocator-driven:
| Module | Use Case |
|---|---|
std.mem.heap | General-purpose allocation |
std.mem.arena | Frame, level, and scratch lifetimes (bulk free) |
std.mem.pool | Fixed-size object pools |
std.mem.stack | Explicit temporary allocators |
std.mem.tracking | Debug allocator: leak, bad-free, and double-free detection |
std.mem.endian | Byte-swap and network byte-order helpers (hton_ushort, ntoh_uint, etc.) |
T to a ref[T] parameter borrows it implicitly — you don't need ref_of() at every call site. Member access and method calls auto-dereference ref[T] receivers, so handle.field and handle.method() work without read(handle).
Generics
Generic structs, variants, functions, methods, interfaces, and foreign functions are supported. Constraints use implements.
## Generic struct
struct Slice[T]:
data: ptr[T]
len: ptr_uint
## Generic function with constraint
function damage_one[T implements Damageable](target: ref[T], amount: int) -> void:
if target.is_alive():
target.take_damage(amount)
## Multiple constraints
function update_and_draw[T implements Updatable and Drawable](value: ref[T]):
value.update()
value.draw()
## Value parameter
function int_with_bits[N: int]() -> type:
if N == 64:
return long
else:
return int
## Specialization
let big = int_with_bits[64]()
let s64 = Slice[ubyte](data = ptr, len = 32)
Functions
There is no overloading. One function name maps to exactly one callable symbol. Parameters must be typed; the return type defaults to void if omitted.
Ordinary Functions
function add(a: int, b: int) -> int:
return a + b
## Return type defaults to void
function say_hello(name: str):
io.print_line(f"Hello, #{name}!")
const function
Evaluable at compile time. Generates both a compile-time constant-folding path and a normal runtime function. Recursive calls between const functions are supported. Use const function for reusable compile-time logic; use a block-bodied const X -> T: ... for one-shot computed constants.
const function square(x: int) -> int:
return x * x
const RESULT: int = square(5) ## folded to 25 at compile time
external function
No body. Raw C ABI declarations. Cannot be generic or async.
external function printf(format: cstr, ...) -> int
foreign function
Projects a C function into Milk Tea types with boundary marshalling. in, out, and inout are declared on the parameter; callers pass ordinary expressions or lvalues. The = c.Symbol(...) mapping supports a fan-out form when a surface parameter must be split into multiple raw C arguments.
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
consuming parameters: After the call, the passed variable is bound to null (the foreign function takes ownership). Consuming foreign calls must appear as top-level expression statements, and the foreign function must return void.
Procs (Closures)
let multiplier = 2
let doubler = proc(x: int) -> int:
return x * multiplier
## Expression-bodied
let tripler = proc(x: int) -> int: x * 3
Statements
if / else if / else
Condition must be bool. No truthy/falsy coercion.
if ready:
start_game()
else if wants_menu:
open_menu()
else:
show_intro()
## Inline form: single statement on same line
if ready: return 1 else: return 0
if x > 10: return "big" else if x > 5: return "med" else: return "small"
while
while running:
tick()
for (Single)
Supports ranges, arrays, spans, and custom iterables.
for i in 0..count:
update_enemy(i)
for item in items:
process(item)
for (Parallel)
Arrays and spans only. Lengths must match.
for entity, position, velocity in entities, positions, velocities:
update(entity, position, velocity)
pass
pass is an explicit no-op statement for intentionally empty block bodies. It produces no code in the output.
if debug_mode:
pass
else:
do_work()
Range Index Assignment
Assign a tuple to a contiguous slice using an exclusive range with literal bounds. The tuple width must match the slice width exactly.
var buf: array[float, 4]
buf[0..3] = (1.0, 2.0, 3.0) # assigns buf[0], buf[1], buf[2]
Custom Iterables
Types can participate in for loops by exposing a non-editable zero-argument iter() method. The returned iterator must expose either:
next() -> ptr[T]?— returns a nullable pointer to each element; iteration stops onnull, ornext() -> booltogether withcurrent() -> T—next()advances and returns whether another element exists;current()returns the current element.
Match Patterns
Statement Form
## Enum match (must be exhaustive unless _ present)
match kind:
EventKind.quit:
return 0
_:
return 1
## Integer match (_ required)
match key_code:
65:
fire()
27:
quit()
_:
return
## Variant match with payload binding
match token:
Token.ident(text):
use_name(text)
Token.number as n:
use_value(n.value)
Token.eof:
return
Expression Form
let label = match code:
1: "one"
2: "two"
_: "other"
## str match (_ required)
let cmd_label = match command:
"lex": "lexer"
"parse": "parser"
_: "unknown"
Variant Membership Test (is)
expr is Variant.Arm returns bool. Desugars to a match expression at parse time — no new semantics. Use for boolean membership checks; use match for payload destructuring.
## No-payload arm
if kind is TokenKind.eof:
break
## Payload arm (any payload matches)
if kind is TokenKind.identifier:
process_ident()
## With logical operators
if kind is TokenKind.eof or kind is TokenKind.indent:
break
## In a let binding
let ok = kind is TokenKind.op_plus
Variants also support == and != for direct value comparison (discriminant + payload fields). The compiler generates a per-type comparison helper function in the emitted C.
let a = TokenKind.eof
let b = TokenKind.eof
if a == b:
io.print_line("same arm")
let id1 = TokenKind.ident(name = "hello")
let id2 = TokenKind.ident(name = "hello")
if id1 == id2:
io.print_line("same payload")
if id1 != TokenKind.eof:
io.print_line("different arm")
Struct Patterns (Variants)
Inline destructuring with guards, bindings, and field discards inside variant arm patterns.
match entity:
Entity.player(hp > 0, position):
render(position)
Entity.player:
remove()
Entity.enemy:
return
## _ discards a field without binding it
match record:
Record.fields(_, _, _, label):
use_label(label)
Defer & Unsafe
defer
Registers cleanup code at scope exit. Lowers to cleanup labels in C.
function main() -> int:
rl.init_window(800, 600, "Game")
defer rl.close_window()
while not rl.window_should_close():
rl.begin_drawing()
defer rl.end_drawing()
render_frame()
return 0
unsafe
Required for: pointer indexing, raw dereference, pointer arithmetic, pointer casts, reinterpret[...]. Calling a foreign function that declares str as cstr or out parameters is ordinary safe code — the foreign boundary handles marshalling. unsafe is for raw std.c.* bindings, pointer reinterpretation, and manual memory walking.
## Expression form
let p = unsafe: pixels + offset
## Block form
unsafe:
let pixel = read(ptr[uint]<-p)
pointer[0] = 1
pointer[1] = 2
Compile-Time Control Flow
when
Evaluates discriminant at compile time; only the chosen branch is type-checked and emitted.
when TARGET_OS:
TargetOs.linux:
return open_linux(path)
TargetOs.windows:
return open_windows(path)
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")
inline if
Only the chosen branch is emitted; dead branch may reference nonexistent symbols. Supports else and else if branches with the same dead-branch elimination. A compile-time type comparison is a valid condition — T == int (a bare type parameter) or field.type == float (a reflected field type) — which enables type-based dispatch in generic bodies.
const DEBUG_RENDER: bool = false
function draw() -> void:
inline if DEBUG_RENDER:
debug_overlay()
else:
normal_draw()
inline match
Not required to be exhaustive; only the chosen arm emits code.
inline match TARGET_BACKEND:
Backend.gl:
gl_draw(item)
Backend.vulkan:
vk_draw(item)
inline while
The condition must be a compile-time constant. The loop unrolls to a fixed number of iterations, capped at 10,000. A non-terminating inline while (condition that never becomes false) is a compile error.
inline while n < 1024:
n = n * 2
emit (Compile-Time Code Generation)
emit generates declarations at compile time. It is only valid inside const function or inline bodies. Currently supports emitting function, struct, and const declarations. Not to be confused with event.emit() which fires runtime events.
const function generate_helpers() -> void:
emit function meaning_of_life() -> int:
return 42
emit function greet() -> str:
return "hello"
Expressions & Operators
Primary Expressions
## Identifiers, literals, parenthesized expressions
let x = (a + b) * c
## Tuple literals
let pair = (42, "hello") ## positional
let point = (x = 10, y = 20) ## named
## Type queries (compile-time)
let s = size_of(int)
let a = align_of(Mat4)
let o = offset_of(Vec2, y)
## If-expressions
let max = if a > b: a else: b
## Match-expressions
let label = match code:
1: "one"
2: "two"
_: "other"
## Proc expressions
let add_one = proc(x: int) -> int: x + 1
Postfix Expressions
## Member access, indexing, calls
value.field
arr[i]
func(arg)
## Partial field update (returns copy)
let moved = v.with(x = 10.0)
## Specialization (bare or module-qualified names only)
let spec = name[T]
let sized = name[32]
Operator Precedence (low to high)
| Precedence | Operators |
|---|---|
| 1 (lowest) | or |
| 2 | and |
| 3 | | |
| 4 | ^ |
| 5 | & |
| 6 | == != |
| 7 | < <= > >= |
| 8 | << >> |
| 9 | + - |
| 10 (highest) | * / % |
Native Type Operators
| Type | Supported Operators |
|---|---|
vecN / ivecN | + - * (component-wise same-type); * / (scalar); unary - |
matN | + - (same-type); * / (scalar); unary - |
quat | + - * (component-wise same-type); unary - |
Built-in Callables
Special functions recognized by the compiler that lower to specific backend operations.
| Callable | Description |
|---|---|
fatal(message) | Terminate with message |
ref_of(x) | Writable safe reference to an addressable lvalue |
const_ptr_of(x) | Read-only raw pointer to an addressable lvalue |
ptr_of(x) | Writable raw pointer from a mutable addressable lvalue |
read(r) | Read through a ref[T] or ptr[T] (requires unsafe for raw) |
T<-value | Explicit numeric cast |
reinterpret[T](value) | Bit reinterpretation (requires unsafe) |
zero[T] | Zero-initialized value |
default[T] | Semantic default via T.default() |
hash[T](value) | Canonical hash (lowers to T.hash(...)) |
equal[T](left, right) | Canonical equality (lowers to T.equal(...)) |
order[T](left, right) | Canonical ordering: returns negative when left < right, 0 when equal, positive when left > right |
array[T, N](...) | Array literal constructor |
span[T](data, len) | Span construction |
get(coll, index) | Recoverable bounds-checked access, returns ptr[T]? |
adapt[I](value) | Construct dyn[I] runtime interface value |
size_of(T) | Compile-time size in bytes |
align_of(T) | Compile-time alignment |
offset_of(T, field) | Compile-time field offset |
Compile-Time Reflection
Reflection builtins return handle values representing type structure. field_handle exposes .name (str) and .type (the field's type). member_handle exposes .name (str) and, for enum members with explicit values, .value (an integer). attribute_handle provides access to attribute arguments via attribute_arg[T].
| Built-in | Returns | Description |
|---|---|---|
fields_of(T) | array[field_handle, N] | All fields of struct T in declaration order |
members_of(E) | array[member_handle, N] | All members of enum or variant E |
attributes_of(T) | array[attribute_handle, N] | All attributes on T |
attributes_of(T, name) | array[attribute_handle, N] | Attributes of T matching name |
field_of(T, name) | field_handle | Named field of T |
callable_of(T, name) | callable_handle | Named callable of T |
attribute_of(T, name) | attribute_handle | Named attribute on T |
has_attribute(T, name) | bool | Whether T has the named attribute |
attribute_arg[T](handle) | T | Typed argument of an attribute handle |
inline for field in fields_of(Point):
let sz = size_of(field.type)
let off = offset_of(Point, field)
static_assert(sz > 0, "field has non-zero size")
A field_handle's .type is also usable directly in type position inside a compile-time context — const_ptr[field.type], equal[field.type](...), Vec[field.type] — resolving to the field's concrete type. Because the inline for body is type-checked once per element, per-field dispatch (including recursion into nested-struct field types) is validated at check time. This powers reflective generics such as std.hash's equal_struct / hash_struct / order_struct, and std.fmt.format_value[T], which renders any struct as { field = value, ... }.
## Per-field equality via field.type used in type position (content-correct)
function field_equal[T](a: const_ptr[T], b: const_ptr[T]) -> bool:
inline for field in fields_of(T):
let off = offset_of(T, field)
unsafe:
let pa = const_ptr[field.type]<-(ptr[ubyte]<-a + off)
let pb = const_ptr[field.type]<-(ptr[ubyte]<-b + off)
if not equal[field.type](pa, pb):
return false
return true
Strings & Text
Text Types
| Type | Ownership | Use Case |
|---|---|---|
str | Borrowed | Read-only UTF-8 view (literals, format strings, slicing) |
cstr | Borrowed | NUL-terminated C ABI string (c"hello") |
str_buffer[N] | Owned (stack) | Fixed-capacity mutable UTF-8 builder |
std.string.String | Owned (heap) | Growable owned text via fmt.format(f"...") |
Format Strings
let text = f"count=#{count} ok=#{ready}"
## Precision for floats
let info = f"value=#{pi:.4}"
## Hex format for ints
let hex = f"address=#{ptr_value:x}"
## Owned text (escape stack lifetime)
import std.fmt
let owned = fmt.format(f"count=#{count}") ## -> string.String
Interpolated expressions must be str, cstr, bool, a numeric primitive, an integer-backed enum or flags type, or a type implementing format_len() and append_format() (custom formatting hooks). The compiler lowers fmt.format(f"..."), str_buffer.append_format(f"..."), and string.String.append_format(f"...") directly to the formatted output without an intermediate allocation.
Format Specifiers
| Specifier | Example | Applies To |
|---|---|---|
:.N | f"pi=#{3.14159:.2}" → "pi=3.14" | float, double |
:x / :X | f"addr=#{255:x}" → "addr=ff" | Integers, enums, flags |
:o / :O | f"perm=#{8:o}" → "perm=10" | Integers |
:b / :B | f"mask=#{5:b}" → "mask=101" | Integers |
Custom Formatting Hooks
Types can participate in format strings by implementing two associated functions:
extending MyType:
function format_len() -> ptr_uint:
return ... # byte length of formatted output
function append_format(output: ref[string.String]) -> void:
... # write formatted bytes into output
The compiler calls append_format directly when the sink is a string.String (via fmt.format). For f"..." literals and str_buffer sinks, the formatter must write exactly format_len() bytes — fewer or more raises a runtime error.
Heredocs
Nonblank content lines are dedented by their shared leading spaces. The trailing newline before the terminator tag is preserved.
## str heredoc
const help: str = <<-HELP
Usage: tool [options]
Options:
--help Show this message
HELP
## cstr heredoc
const shader: cstr = c<<-GLSL
#version 330
void main() {
gl_FragColor = vec4(1.0);
}
GLSL
## Format heredoc
let msg = f<<-MSG
Hello, #{name}!
Score: #{score}
MSG
str_buffer[M] Methods
| Method | Description |
|---|---|
.clear() | Clear contents |
.assign(str) | Replace contents (traps if exceeds capacity) |
.append(str) | Extend contents (traps if exceeds capacity) |
.assign_format(str) | Replace via format string |
.append_format(str) | Extend via format string |
.len() | Current text length |
.capacity() | Max writable bytes (excludes trailing NUL) |
.as_str() | Borrow as str |
.as_cstr() | Borrow as cstr |
Adjacent String Literals
Ordinary "..." and c"..." literals may continue across following indented lines when each continued line starts with the same literal kind. The segments concatenate exactly with no implicit separator:
let text = "hello "
"from multiple "
"indented lines" # produces "hello from multiple indented lines"
Safety Rules
| Rule | Description |
|---|---|
Conditions must be bool | No truthy/falsy coercion from integers or pointers |
| Explicit casts for mixed signed/unsigned | Mixed signed/unsigned arithmetic requires T<-value |
| Enum/flags don't auto-coerce | Outside external-call boundaries, no implicit conversion to backing integers |
| Safe indexing is bounds-checked | arr[i] calls fatal on OOB; use get(arr, i) for recoverable access |
Pointer indexing requires unsafe | Raw pointer dereference, arithmetic, casts, reinterpret all need unsafe |
% requires integer operands | |
| Bitwise ops require matching types | Integer or flags types |
| Shift ops require integer operands | |
| No implicit struct equality | Structs cannot be compared with ==/!=. Use equal[T](a, b). Variants do support == and != with generated per-type comparison. |
No str/cstr concatenation | Use format strings or std.str helpers |
| Compile-time constant fit | Exact compile-time numeric constants (literals, const values) fit an explicit numeric target without a manual cast when representable exactly |
| Integer-to-float at typed boundaries | A primitive integer expression may flow into an expected float type for explicit typed locals, assignments, returns, function arguments, or field initializers. Integer arithmetic stays integer arithmetic until that final boundary cast. |
Async
async function lifts its return type to Task[T]. await is only allowed inside async functions.
async function child() -> int:
return 41
async function parent() -> int:
let v = await child()
return v + 1
## async main is compiler-bootstrapped
async function main() -> int:
let result = await do_work()
return result
Task Helpers
Import std.async for runtime control:
import std.async as aio
let task = some_async_work()
aio.wait(task)
let r = aio.result(task)
Limitations
await is supported inside if expressions and bodies, while bodies and conditions, for bodies and iterables, match discriminants and arms, let ... else: initializers and else bodies, unsafe blocks, short-circuit and/or expressions, and assignment targets. defer is supported in async functions, including cleanup bodies that await. async main pre-lift return type must be int or void. ? propagation inside async functions completes the task early on failure.
Concurrency
Milk Tea has first-class compiler support for multithreading using real OS threads (libuv). All concurrency is structured: spawned work completes before the parent scope continues. No hidden thread pool, no GC interaction, no fire-and-forget.
Parallel For (data-parallel)
Dispatches loop iterations across CPU cores. Each iteration writes to its own index range — the bread and butter of data-oriented game programming.
parallel for i in 0..entity_count:
positions[i] += velocities[i] * dt
Rules: range iteration only (0..N). No break, continue, return, defer, or nested parallel for in the body. ref[T] captures rejected.
Parallel Blocks (structured fork-join)
Run heterogeneous work concurrently. Each statement runs concurrently on OS threads (one on the calling thread, the rest on worker threads). The parent blocks until all complete.
parallel:
load_textures(path)
load_sounds(path)
Rules: at least two statements. Single-writer-or-multiple-readers enforced: if one statement writes a variable, no other may access it.
Atomic Types
atomic[T] provides lock-free concurrent access to integer values. T must be a primitive integer or bool.
var counter: atomic[int]
counter.store(0)
let prev = counter.add(1) # returns previous value
let value = counter.load()
counter.sub(1)
let old = counter.exchange(42)
| Method | Signature | Description |
|---|---|---|
load() | -> T | Atomic read |
store(value) | -> void | Atomic write (editable receiver) |
add(value) | -> T | Fetch-and-add, returns previous (editable) |
sub(value) | -> T | Fetch-and-sub, returns previous (editable) |
exchange(value) | -> T | Atomic swap, returns previous (editable) |
All operations use sequential consistency. Lowers to C11 _Atomic T with __atomic_* builtins.
Detach & Collect
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: detach returns a Handle — must be bound with let or var. Currently supports global function calls with no captured local variables. gather joins handles in order. ref[T] captures rejected.
Compile-Time Safety
ref[T]captures rejected at all thread boundaries- Write conflicts detected between
parallel:statements (single-writer-or-multiple-readers) atomic[T]is always sendable across thread boundaries
std.thread (OS threads), std.sync (Mutex, Condition, Semaphore, AtomicUint), and std.jobs (thread pool). The built-in parallel for, parallel:, and atomic[T] are compiler-integrated and provide compile-time safety checks that library threading cannot.
Events
Built-in typed publisher/subscriber with fixed-capacity listener storage. Zero heap allocation during dispatch — emit snapshots active listeners into a stack-allocated array, guaranteeing predictable latency critical for real-time, audio, and embedded contexts.
Declaration
public event closed[4]
public event resized[8](ResizeEvent)
## As struct members
struct Window:
public event closed[4]
public event resized[8](ResizeEvent)
Operations
## Subscribe
let sub = window.closed.subscribe(on_close)?
## One-shot
let once = window.resized.subscribe_once(on_resize)?
## Unsubscribe
window.closed.unsubscribe(sub)
## Emit (only from declaring module)
closed.emit()
resized.emit(ResizeEvent(width = 800, height = 600))
## Async wait for next emission
let payload = await event.wait()
Event API Reference
| Expression | Description |
|---|---|
event.subscribe(fn) -> Result[Subscription, EventError] | Register listener; fails with EventError.full when at capacity |
event.subscribe(state, fn) | Stateful subscription — state pointer passed as first arg to listener |
event.subscribe_once(fn) | One-shot listener; auto-unsubscribes after first emission |
event.unsubscribe(sub) -> bool | Returns true if active and removed, false if already stale |
event.emit(payload?) | Fire event (only callable from the declaring module) |
event.wait() -> Task[Result[T, EventError]] | Async await for next emission |
Subscription is an opaque handle returned by subscribe. EventError is a built-in enum with single member full = 0. Event methods do not support named arguments.
Full Example
struct ResizeEvent:
width: int
height: int
struct Window:
public event closed[4]
public event resized[8](ResizeEvent)
function on_close() -> void:
io.print_line("closed")
function attach(window: ref[Window]) -> Result[void, EventError]:
let closed_sub = window.closed.subscribe(on_close)?
let resized_sub = window.resized.subscribe(on_resize)?
defer window.closed.unsubscribe(closed_sub)
defer window.resized.unsubscribe(resized_sub)
return Result[void, EventError].success()
Standard Library
Collections
| Module | Type | Iteration | Description |
|---|---|---|---|
std.vec | Vec[T] | Mutable ptr[T]? | Contiguous growable array |
std.deque | Deque[T] | Mutable ptr[T]? | Growable ring buffer |
std.map | Map[K, V] | Keys read-only; values mutable; entries current() | Hash table (hash/equal) |
std.set | Set[T] | Read-only ptr[T]? | Hash set |
std.ordered_map | OrderedMap[K,V] | Keys read-only; values mutable; entries current() | AVL tree (order) |
std.ordered_set | OrderedSet[T] | Read-only ptr[T]? | AVL sorted unique set |
std.linked_map | LinkedMap[K,V] | Keys read-only; values mutable; entries current() | Insertion-ordered hash map |
std.linked_set | LinkedSet[T] | Read-only ptr[T]? | Insertion-ordered hash set |
std.binary_heap | BinaryHeap[T] | Read-only ptr[T]? | Max-heap (order) |
std.priority_queue | PriorityQueue[T] | Read-only ptr[T]? | Task-oriented facade over heap |
std.counter | Counter[T] | Keys read-only; entries current() | Insertion-ordered frequency table |
std.multiset | MultiSet[T] | Values read-only; entries current() | Bag with per-element counts |
std.queue | Queue[T] | Mutable ptr[T]? | FIFO facade over Deque |
std.stack | Stack[T] | Mutable ptr[T]? | LIFO facade over Deque |
Core Library Modules
| Module | Description |
|---|---|
std.str | Borrowed string helpers: validation, equality, slicing, search |
std.string.String | Growable owned UTF-8 text |
std.fmt | Formatting engine (only canonical formatter) |
std.math | Math: sqrt, sin, cos, abs, pow |
std.linear_algebra | Vector/matrix/quaternion extensions: dot, cross, normalized, lerp, transpose |
std.hash | Canonical hash/equal/order for primitives and structs |
std.mem.heap | General heap allocator |
std.mem.arena | Frame/level/scratch arena allocator |
std.mem.pool | Fixed-size object pool |
std.mem.stack | Explicit temporary allocator |
std.async | Async runtime: sleep, work, completed, result, wait, run |
std.option | Option[T] — optional value (prelude type; methods always available without import). Methods: is_some, is_none, unwrap, expect, unwrap_or, unwrap_or_else |
std.result | Result[T, E] — fallible computation (prelude type; methods always available without import). Methods: is_success, is_failure, unwrap, unwrap_error, unwrap_or, unwrap_or_else, ok, error, map_error |
System & I/O
| Module | Description |
|---|---|
std.time | Time primitives |
std.fs | File system operations |
std.path | Path manipulation |
std.process | Process management |
std.cli | Command line interface |
std.stdio | Standard I/O: print_line, print_format, file_open, file_read_line |
std.terminal | Terminal control |
std.json std.toml | Serialization |
std.uri | URI parsing |
std.http std.tls std.net | Networking |
std.gzip std.tar | Compression |
std.sync std.thread std.jobs | Concurrency |
std.fsm std.goap std.behavior_tree | AI / State machines |
std.cell | Shared mutable cell allocation |
CLI Reference
The mtc CLI is the primary tool for checking, building, and running Milk Tea programs.
Essential Commands
| Command | Description |
|---|---|
mtc check <path> | Type-check + lint; reports all diagnostics sorted by line |
mtc run <path> | Build and execute |
mtc build <path> | Build only (emit C, compile, link) |
mtc lex <file.mt> | Print lexer token stream |
mtc parse <path> | Print parsed AST |
mtc lower <path> | Print lowered IR |
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, --select, --ignore) |
mtc test <path> | Discover and run @[test] functions |
mtc new <name> | Scaffold a new package (package.toml + src/main.mt) |
mtc run-module <module> | Run a pre-built module by name |
mtc completions <shell> | Print a bash/zsh/fish completion script |
Package Commands
| Command | Description |
|---|---|
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 |
Toolchain
| Command | Description |
|---|---|
mtc toolchain bootstrap | Bootstrap the native toolchain |
mtc toolchain doctor | Diagnose toolchain setup |
mtc toolchain tools | List available native tools |
mtc cache status | Show build cache stats |
mtc docs [--open] [--port PORT] | Start a local HTTP server serving the language reference |
Build Flags
Build and run commands support: --profile, --platform, --cc, --keep-c, --locked, --frozen, and -I include paths.
Global Options
These work with any command (before or after the subcommand, up to a -- separator):
| Option | Description |
|---|---|
-h, --help | Show help; mtc help <command> shows command-specific help |
-V, --version | Print the compiler version |
-q, --quiet | Suppress informational output |
-v, --verbose | Print per-file progress |
--color auto|always|never | Control diagnostic colorization (default: auto) |
Linting
Milk Tea ships with a built-in linter providing rules across correctness, style, performance, and convention. Run with mtc lint.
CLI Usage
mtc lint path/to/file.mt # report all diagnostics
mtc lint path/to/file.mt --fix # auto-apply safe fixes
mtc lint path/to/file.mt --select unused-import # only selected rules
mtc lint path/to/file.mt --ignore line-too-long # skip selected rules
Inline Suppression
Suppress a rule for the next line with # lint: ignore <rule-code>:
# lint: ignore unused-import
import std.math # intentionally available for downstream
Configuration
Place a .mt-lint.yml file in your package root to customize rule severity:
rules:
prefer-let-else: warn
unused-import: error
line-too-long: off
Rule Reference
| Category | Code | Description |
|---|---|---|
| Correctness | constant-condition | Always-true or always-false conditions |
| Correctness | dead-assignment | Variable assigned but never read |
| Correctness | unreachable-code | Code after a terminating statement |
| Correctness | redundant-null-check | Null check on already-proven non-null |
| Correctness | loop-single-iteration | Loop with known single iteration |
| Style | prefer-let-else | Var followed by null check; suggest let...else |
| Style | prefer-var-else | Same as above for mutable bindings |
| Style | redundant-cast | Explicit cast where implicit widening already works |
| Style | redundant-return | Unnecessary return at end of void function |
| Style | trailing-list-comma | Multiline lists should end with trailing comma |
| Style | line-too-long | Line exceeds recommended width |
| Style | doc-tag | Documentation comment formatting |
| Convention | unused-import | Imported module never referenced |
| Convention | unused-var | Variable declared but never read |
| Convention | reserved-names | Binding shadows a primitive type or built-in name |
| Convention | event-capacity | Event capacity too large or unbounded |
Severity levels: error (fail build), warn (report only), off (disabled). Rules default to warn unless noted in the manual.
Language Limitations
This is a quick-reference summary of the current compiler's rejection surface. See the full manual for complete rules.
Function Restrictions
external functioncannot be generic, async, or take/return arrays,ref[...], orproc(...)foreign functioncannot be async, cannot takeproc(...)- Functions cannot return
ref[T]or non-owning structs - A consuming foreign function must return
void external/foreign functioncannot use a non-pointer value nullable (e.g.int?) as a parameter or return type; only pointer-likeT?may cross an FFI boundary
Storage Restrictions
ref[T]values rejected in constants, module variables, and nested storage- Bare interface names are not runtime storage types; use
dyn[I] dyn[I]for generic interfaces must be fully specified:dyn[Mapper[int]]proccannot captureref[T]values
Control Flow Restrictions
breakandcontinuemust be inside loopsreturnnot allowed insidedeferblocks?propagation not allowed insidedeferblocksmatchon enum/variant must be exhaustive unless_present; match onint/strrequires_let ... else:else block must terminate control flow
External File Restrictions
- Cannot contain
attribute,event,var,variant,interface,extending,foreign function, ordinaryfunction,async function, orstatic_assert publicis rejected (declarations are implicitly exported)
Expression Restrictions
- Conditions must be
bool; no truthy/falsy coercion - Mixed signed/unsigned arithmetic requires explicit cast; non-widening integer conversions (narrowing, signed → unsigned) need explicit
T<-value - Enum and flags values do not implicitly coerce to backing integers
+does not supportstr/cstrconcatenation==and!=not supported on struct types; useequal[T]- Bare function and method names are not usable as values (cannot be assigned or passed without calling). Use
fn(...)function pointer types orproc(...)closures for callable values.
Event Restrictions
- Event payload cannot be
ref[T] emitis only callable from within the declaring module- Events not allowed in external files