🍵 Milk Tea

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
📦 Install: gem install mt-lang 💻 VS Code Extension
⚡ Statically Typed 🌎 Compiles to C 🔒 Safe by Default 🔧 Data-Oriented ☘ No GC 🔗 First-Class C FFI

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

PrincipleDescription
What you see is what runsNo hidden allocation, dispatch, or heap traffic. Every cost is spelled in source.
C is the ABI ground truthStructs, unions, pointers, and calling conventions map directly without lossy translation.
Safe by default, unsafe by choicePointer operations require unsafe. Indexing is bounds-checked. Conversions are explicit.
Data-oriented firstPlain structs, arrays, spans, pools, and arenas matter more than elaborate object systems.
One job, one canonical surfaceNo 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

CommandDescription
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;
}
Method lowering: 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
Tip: This example covers imports, constants, 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

## Preferred: wrap in delimiters.
let total = (
    subtotal
    + tax
    - discount
)

## Also accepted: operator-led continuation.
let total = subtotal +
    tax -
    discount

Naming Conventions

CategoryStyleExample
Modules, variables, functionssnake_caseplayer_count, load_level
Types (struct, enum, variant, etc.)PascalCaseVec2, LoadError, EventKind
C binding modulesPreserve C names exactlySDL_Window, c.InitWindow

Comments

FormDescription
# textLine comment
## textDocumentation comment; attaches to the next declaration (no blank line)

Literals

KindExamplesType
Integer42, 0xff, 0b1010, 1_000_000int (default)
Float3.14, 1.2e-3, 1.0f, 1.0dfloat (default)
Character'a', '\n', '\t', '\0', '\x41'ubyte
Booleantrue, falsebool
String"hello"str
C Stringc"hello"cstr
Format Stringf"count=#{n}"str
Heredoc String<<-TAG ... TAGstr
Heredoc C Stringc<<-TAG ... TAGcstr
Heredoc Formatf<<-TAG ... TAGstr
Nullnull, 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

CategoryTokens
Delimiters( ) [ ]
Separators / Access: , .
Type markers-> ?
Arithmetic+ - * / %
Bitwise~ & | ^ << >>
Comparison== != < <= > >=
Assignment= += -= *= /= %= &= |= ^= <<= >>=
Variadic...
Word operatorsand or not
Pattern testis — variant arm membership test, desugars to match
Parameter modesin 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)
Prelude types. 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

TypeDescription
boolBoolean
byte short int longSigned integers
ubyte ushort uint ulongUnsigned integers
charSingle-byte character (ABI-facing). Not a general arithmetic integer type — cast to an integer explicitly for arithmetic.
ptr_int ptr_uintPointer-sized integers
float doubleFloating-point
voidNo value
strUTF-8 string view (borrowed)
cstrNUL-terminated C string
vec2 vec3 vec4Float vectors with .x .y .z .w
ivec2 ivec3 ivec4Integer vectors
mat3 mat4Column-major matrices
quatQuaternion (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

ConstructorDescription
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...) -> RFunction pointer type
proc(params...) -> RClosure 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:

ModuleUse Case
std.mem.heapGeneral-purpose allocation
std.mem.arenaFrame, level, and scratch lifetimes (bulk free)
std.mem.poolFixed-size object pools
std.mem.stackExplicit temporary allocators
std.mem.trackingDebug allocator: leak, bad-free, and double-free detection
std.mem.endianByte-swap and network byte-order helpers (hton_ushort, ntoh_uint, etc.)
ref[T] convenience: Passing a mutable addressable 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:

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)

PrecedenceOperators
1 (lowest)or
2and
3|
4^
5&
6== !=
7< <= > >=
8<< >>
9+ -
10 (highest)* / %

Native Type Operators

TypeSupported 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.

CallableDescription
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<-valueExplicit 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-inReturnsDescription
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_handleNamed field of T
callable_of(T, name)callable_handleNamed callable of T
attribute_of(T, name)attribute_handleNamed attribute on T
has_attribute(T, name)boolWhether T has the named attribute
attribute_arg[T](handle)TTyped 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

TypeOwnershipUse Case
strBorrowedRead-only UTF-8 view (literals, format strings, slicing)
cstrBorrowedNUL-terminated C ABI string (c"hello")
str_buffer[N]Owned (stack)Fixed-capacity mutable UTF-8 builder
std.string.StringOwned (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

SpecifierExampleApplies To
:.Nf"pi=#{3.14159:.2}""pi=3.14"float, double
:x / :Xf"addr=#{255:x}""addr=ff"Integers, enums, flags
:o / :Of"perm=#{8:o}""perm=10"Integers
:b / :Bf"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

MethodDescription
.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

RuleDescription
Conditions must be boolNo truthy/falsy coercion from integers or pointers
Explicit casts for mixed signed/unsignedMixed signed/unsigned arithmetic requires T<-value
Enum/flags don't auto-coerceOutside external-call boundaries, no implicit conversion to backing integers
Safe indexing is bounds-checkedarr[i] calls fatal on OOB; use get(arr, i) for recoverable access
Pointer indexing requires unsafeRaw pointer dereference, arithmetic, casts, reinterpret all need unsafe
% requires integer operands
Bitwise ops require matching typesInteger or flags types
Shift ops require integer operands
No implicit struct equalityStructs cannot be compared with ==/!=. Use equal[T](a, b). Variants do support == and != with generated per-type comparison.
No str/cstr concatenationUse format strings or std.str helpers
Compile-time constant fitExact compile-time numeric constants (literals, const values) fit an explicit numeric target without a manual cast when representable exactly
Integer-to-float at typed boundariesA 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)
MethodSignatureDescription
load()-> TAtomic read
store(value)-> voidAtomic write (editable receiver)
add(value)-> TFetch-and-add, returns previous (editable)
sub(value)-> TFetch-and-sub, returns previous (editable)
exchange(value)-> TAtomic 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

Tip: For library-level threading, see 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

ExpressionDescription
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) -> boolReturns 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

ModuleTypeIterationDescription
std.vecVec[T]Mutable ptr[T]?Contiguous growable array
std.dequeDeque[T]Mutable ptr[T]?Growable ring buffer
std.mapMap[K, V]Keys read-only; values mutable; entries current()Hash table (hash/equal)
std.setSet[T]Read-only ptr[T]?Hash set
std.ordered_mapOrderedMap[K,V]Keys read-only; values mutable; entries current()AVL tree (order)
std.ordered_setOrderedSet[T]Read-only ptr[T]?AVL sorted unique set
std.linked_mapLinkedMap[K,V]Keys read-only; values mutable; entries current()Insertion-ordered hash map
std.linked_setLinkedSet[T]Read-only ptr[T]?Insertion-ordered hash set
std.binary_heapBinaryHeap[T]Read-only ptr[T]?Max-heap (order)
std.priority_queuePriorityQueue[T]Read-only ptr[T]?Task-oriented facade over heap
std.counterCounter[T]Keys read-only; entries current()Insertion-ordered frequency table
std.multisetMultiSet[T]Values read-only; entries current()Bag with per-element counts
std.queueQueue[T]Mutable ptr[T]?FIFO facade over Deque
std.stackStack[T]Mutable ptr[T]?LIFO facade over Deque

Core Library Modules

ModuleDescription
std.strBorrowed string helpers: validation, equality, slicing, search
std.string.StringGrowable owned UTF-8 text
std.fmtFormatting engine (only canonical formatter)
std.mathMath: sqrt, sin, cos, abs, pow
std.linear_algebraVector/matrix/quaternion extensions: dot, cross, normalized, lerp, transpose
std.hashCanonical hash/equal/order for primitives and structs
std.mem.heapGeneral heap allocator
std.mem.arenaFrame/level/scratch arena allocator
std.mem.poolFixed-size object pool
std.mem.stackExplicit temporary allocator
std.asyncAsync runtime: sleep, work, completed, result, wait, run
std.optionOption[T] — optional value (prelude type; methods always available without import). Methods: is_some, is_none, unwrap, expect, unwrap_or, unwrap_or_else
std.resultResult[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

ModuleDescription
std.timeTime primitives
std.fsFile system operations
std.pathPath manipulation
std.processProcess management
std.cliCommand line interface
std.stdioStandard I/O: print_line, print_format, file_open, file_read_line
std.terminalTerminal control
std.json std.tomlSerialization
std.uriURI parsing
std.http std.tls std.netNetworking
std.gzip std.tarCompression
std.sync std.thread std.jobsConcurrency
std.fsm std.goap std.behavior_treeAI / State machines
std.cellShared mutable cell allocation

CLI Reference

The mtc CLI is the primary tool for checking, building, and running Milk Tea programs.

Essential Commands

CommandDescription
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

CommandDescription
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

CommandDescription
mtc toolchain bootstrapBootstrap the native toolchain
mtc toolchain doctorDiagnose toolchain setup
mtc toolchain toolsList available native tools
mtc cache statusShow 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):

OptionDescription
-h, --helpShow help; mtc help <command> shows command-specific help
-V, --versionPrint the compiler version
-q, --quietSuppress informational output
-v, --verbosePrint per-file progress
--color auto|always|neverControl 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

CategoryCodeDescription
Correctnessconstant-conditionAlways-true or always-false conditions
Correctnessdead-assignmentVariable assigned but never read
Correctnessunreachable-codeCode after a terminating statement
Correctnessredundant-null-checkNull check on already-proven non-null
Correctnessloop-single-iterationLoop with known single iteration
Styleprefer-let-elseVar followed by null check; suggest let...else
Styleprefer-var-elseSame as above for mutable bindings
Styleredundant-castExplicit cast where implicit widening already works
Styleredundant-returnUnnecessary return at end of void function
Styletrailing-list-commaMultiline lists should end with trailing comma
Styleline-too-longLine exceeds recommended width
Styledoc-tagDocumentation comment formatting
Conventionunused-importImported module never referenced
Conventionunused-varVariable declared but never read
Conventionreserved-namesBinding shadows a primitive type or built-in name
Conventionevent-capacityEvent 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

Storage Restrictions

Control Flow Restrictions

External File Restrictions

Expression Restrictions

Event Restrictions