🍵 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 runsOrdinary values have no hidden allocation or dispatch. Runtime interface dispatch and closure environments are explicit language features with their own costs.
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 source to stdout (--write rewrites in place, --check verifies)
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. Ordinary value operations do not introduce hidden heap traffic or dispatch; runtime interface values and closure environments remain explicit language features. 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.str
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; dyn provides explicit 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

## ── inline if with a compile-time type comparison ───────────────────
function size_label[T]() -> str:
    inline if T == int:
        return "32-bit"
    inline if T == float:
        return "float"
    return "other"

## ── 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)

## ── Foreign FFI: raw external + foreign projection ──────────────────
external function atoi(input: cstr) -> int

## foreign function projects a raw ABI call into ordinary Milk Tea types
foreign function parse_int_foreign(input: str as cstr) -> int = atoi

## ── 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

## ── when: compile-time conditional (only the chosen branch is emitted) ──
enum Backend: ubyte
    gl     = 1
    vulkan = 2
const TARGET_BACKEND: Backend = Backend.gl

function backend_label() -> str:
    when TARGET_BACKEND:
        Backend.gl:
            return "OpenGL"
        Backend.vulkan:
            return "Vulkan"

## ── 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}")

    ## Lockstep for over arrays/spans (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

    ## is: variant arm membership test
    if action is Action.attack:
        io.print_line("action is an attack")

    ## match expression
    let label = match player.level:
        1: "beginner"
        2: "novice"
        _: "advanced"

    ## Struct equality: field-wise ==/!=
    let stats_a = Entity.Stats(attack = 10, defense = 5, speed = 1.0)
    let stats_b = Entity.Stats(attack = 10, defense = 5, speed = 1.0)
    if stats_a == stats_b:
        io.print_line("stats equal")

    ## 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")

    ## Foreign FFI, when, and inline-if type dispatch
    let parsed_int = parse_int_foreign("42")
    io.print_line(f"FFI parsed: #{parsed_int}")
    io.print_line(f"Backend: #{backend_label()}")
    io.print_line(f"int label: #{size_label[int]()}")

    ## ── 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, foreign functions (external / foreign function), 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, when, inline for/inline if (including type comparison), struct ==/!=, 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. Circular module imports are supported through forward-declaration bindings and two-pass checking.

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

## The continuation operator set is closed:
## + - * / % | & ^ << >> == != < <= > >= and or is ..
## Member-access chains exceeding one line wrap in () with leading dots:
let total = (
    values.iter()
        .filter(pred)
        .fold(0, add)
)

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.

Struct & Variant Equality

Structs and variants support == and != with field-wise comparison. A struct compares equal when every field compares equal; a variant compares equal when the same arm is active and its payload fields are equal. Every field must be equality-comparable (nested structs, arrays, nullable values, str, enums, flags, pointers, ...). Recursive variants embed cyclic fields as pointers and compare those by pointer identity. Untagged union fields and non-comparable types (proc, span, simd, SoA, Task, dyn) are rejected; use equal[T](...) there.

struct Vec2:
    x: float
    y: float

variant Shape:
    circle(radius: float)
    square(side: float)

function same_point(a: Vec2, b: Vec2) -> bool:
    return a == b

function same_shape(a: Shape, b: Shape) -> bool:
    return a == b

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 or hidden dispatch; runtime polymorphism is explicit through dyn[I].

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)

Method kinds: function (value receiver), editable function (editable receiver), static function (no receiver), plus compile-time-evaluable const function and static const function. editable const function, async const function, and const on interface methods are rejected.

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). s[i] reads the byte at i as ubyte (bounds-checked); s[start..stop] returns a borrowed view using byte offsets and traps off UTF-8 boundaries. The + operator concatenates two str values into a per-thread scratch buffer (no allocation); the result is valid while the scratch budget lasts.
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
simd[T, N]SIMD vector: N numeric lanes (128/256-bit), GCC vector extensions
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. Ordinary values do not carry hidden heap boxes or reference counts; captured proc values may allocate ref-counted closure environments as part of their lifecycle. User-owned heap allocation remains 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 also applies to methods: const function (value receiver) and static const function (no receiver) fold when called from a compile-time context. editable const function, async const function, and const on interface methods are rejected.

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 (Lockstep)

This is the lockstep multi-iterable form, distinct from parallel for. 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

Range-index reads borrow a sub-view with no copy and no allocation. The result is a borrowed str for string receivers and a borrowed span[T] for arrays and spans. Bounds may be any integer types. The range is start-inclusive and end-exclusive. start > stop or stop > len traps at runtime; string slices additionally require UTF-8 code-unit boundaries at both bounds.

let text: str = "hello world"
let head = text[0..5]              # str view: "hello"
var values: array[int, 4] = (10, 20, 30, 40)
let middle = values[1..3]          # span[int] view over 20, 30

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. String views are immutable and cannot be assigned through.

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

Block-bodied const and const function

Block-bodied const initializers and const function bodies are evaluated at compile time, and the result is folded into the emitted C. Supported surface: literals (character literals fold to their byte value), other const values, arithmetic, str concatenation, if/while/for with break and continue, match expressions, index and range access, struct member access, let/var with destructuring and else: / else as error: guards, assignment (including compound), numeric prefix casts, calls to other const functions and const methods, and the reflection builtins.

## str concatenation folds to a literal
const GREET: str = "hello" + " " + "world"

## match expressions fold
const LABEL -> str:
    return match 2:
        1: "one"
        2: "two"
        _: "other"

## const methods fold too
struct Rect:
    w: int
    h: int
    const function area() -> int:
        return this.w * this.h

const R: Rect = Rect(w = 10, h = 20)
const AREA: int = R.area()   ## folded to 200 at compile time

when

Evaluates discriminant at compile time; only the chosen branch is type-checked and emitted. when may also appear at module level to conditionally include declarations, imports, or type definitions.

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)

## Range index: str returns str view; arrays and spans return span[T] view
let head = text[0..5]
let middle = values[1..3]

## 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
3not (unary prefix)
4is
5|
6^
7&
8== !=
9< <= > >=
10<< >>
11+ -
12 (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
assert(condition, message?)Runtime check; aborts when the bool condition is false
expect(condition, message?)Always-on test assertion (same as assert)
expect_eq(actual, expected, message?)Compares with the language == (primitives, str, structs, variants, arrays); aborts when the values differ
expect_ne(actual, expected, message?)Compares with !=; aborts when the values are equal
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, x[start..stop]); a stored or returned dynamic format string carries its own heap buffer
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 with explicit release
import std.fmt
let owned = fmt.format(f"count=#{count}")  ## -> string.String (call .release())

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] and a[start..stop] call fatal on OOB; string slices additionally require UTF-8 boundaries; use get(arr, i) for recoverable single-element 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
Aggregate equality requires comparable fieldsStructs and variants support ==/!= when every field is equality-comparable. Untagged union fields and non-comparable types (proc, span, simd, SoA, Task, dyn) are rejected — use equal[T](a, b) there. Recursive variants compare cyclic fields by pointer identity.
No cstr concatenation+ concatenates str; cstr and mixed str/cstr are rejected
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 (sleep, work, completed, result, wait, run, plus explicit-runtime variants such as wait_on, work_on, and with_runtime):

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). parallel for and parallel: are structured barriers; detach continues asynchronously until an explicit gather. There is no GC interaction or implicit fire-and-forget cleanup.

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
std.bitsetBitsetBit-level set with popcount
std.spatialSpatialGrid[T]Uniform spatial hash grid (returns Vec)
std.ring_bufferRingBuffer[T]Mutable ptr[T]?Fixed-capacity circular buffer
std.sparse_setSparseSet[T]Mutable ptr[T]?O(1) insert/remove; dense iteration
std.lru_cacheLruCache[K,V]iter()linked_map.Entries[K, V]Capacity-bounded LRU eviction map

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, plus explicit-runtime variants (wait_on, work_on, with_runtime)
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.boxExplicit single-value heap storage for shared mutable state

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 source to stdout (--write rewrites in place, --check verifies)
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>Resolve a module by name, build it, and run it
mtc std list [--json]List hand-written standard library modules grouped by category (--json for machine-readable output)
mtc std show <module>Print a standard library module's source (dotted or slashed names; platform variants resolve to the active platform)
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 cache purgeRemove the entire build cache
mtc docs [--open] [--port PORT]Start a local HTTP server serving the language reference

Tooling

CommandDescription
mtc bindgen <header.h>Generate an external binding module from a C header
mtc snapshot <path>Render a highlighted HTML snapshot of a source file
mtc lspStart the Language Server Protocol server (editor IntelliSense)
mtc dapStart the Debug Adapter Protocol server (debugging)

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 all rules on a line with # lint: ignore, or specific rules with # lint: ignore(rule1, rule2):

var count = 0  # lint: ignore
var total = 0  # lint: ignore(prefer-let, dead-assignment)

Configuration

Create a default config with mtc lint --init, or place a .mt-lint.yml file in the project root (or any ancestor directory):

max_line_length: 120
select:
    - line-too-long
    - prefer-let
    - missing-return
ignore:
  - shadow
  - useless-expression

max_line_length defaults to 120 when omitted. When both select and ignore are present, select takes precedence.

Rule Reference

CodeSeverityAuto-fixDescription
borrow-and-mutatewarningLocal is borrowed with ref_of/ptr_of and also mutated in the same scope
constant-conditionwarningBranch or loop condition is provably always true or false
dead-assignmentwarningAssigned value is overwritten before any read
duplicate-if-conditionwarningif/else if branch repeats a previous condition and is unreachable
directional-ffi-arghintLegacy ptr_of/ref_of/out call-site wrapper is redundant for directional FFI parameters
doc-taghint## doc comment tag (@param, @return, @throws, @see) is invalid or inconsistent
event-capacitywarningEvent capacity may copy too many listeners to stack on emit
line-too-longwarningSource line exceeds configured maximum length
loop-single-iterationwarningLoop body always exits on the first iteration
missing-returnerrorNon-void function lacks a guaranteed return on all paths
noop-compound-assignmenthintCompound assignment uses an identity value and has no effect
owning-release-doublewarningOwning binding may be released more than once
owning-release-leakwarningOwning binding is never released
platform-api-driftwarningPublic API differs across platform-specific variants of the same module
prefer-lethintyesvar binding is never mutated; use let
prefer-let-elsehintyesNullable guard can be rewritten as let ... else:
prefer-inline-methodshintyesMethods on a struct can be written inline inside the struct declaration
prefer-is-varianthintA match that maps one variant arm to a boolean can be expr is Arm
prefer-own-ptrhintPointer binding used only inside unsafe could be own[T] for auto-deref
prefer-or-patternhintAdjacent match arms with identical bodies can merge with |
prefer-struct-withhintField copies from another value can use .with(...)
prefer-tryhintA match that only propagates the failure branch can use expr?
prefer-var-elsehintyesNullable guard can be rewritten as var ... else:
redundant-bool-comparehintyesComparing a boolean expression to true/false is redundant
redundant-casthintyesExplicit cast where implicit widening already works
redundant-elsewarningyeselse block is unnecessary because all prior branches return
redundant-ignored-match-bindinghintyesIgnored as _ match binding is redundant
redundant-null-checkhintNull check on a value already known non-null by flow analysis
redundant-returnhintyesFinal bare return in a void function is unnecessary
redundant-type-annotationhintyesType annotation is redundant; the type is inferred from the initializer
redundant-unsafehintyesunsafe block contains no unsafe operations and can be removed
reserved-primitive-namewarningyesBinding uses a reserved built-in type name in its active namespace
self-assignmentwarningVariable is assigned to itself
self-comparisonwarningValue is compared to itself, making the condition constant
shadowwarningLocal binding shadows an outer binding with the same name
trailing-list-commahintyesTrailing comma in call argument list is redundant
unreachable-codewarningCode after a guaranteed terminator cannot execute
unused-importwarningImport alias is never referenced (intentionally not auto-fixable: removals can drop extension methods or canonical hooks)
unused-localwarningLocal binding is never referenced
unused-paramwarningParameter is never referenced
useless-expressionwarningExpression statement has no side effects and its result is unused

Severity levels: error (fail build), warning, and hint.

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

Interface Restrictions