EnglishFrançais

2 · Language tour

A read-through of every language feature with runnable snippets. If you're impatient, skim this chapter once and refer back when needed.

Conventions

Variables and primitives

let x = 42                  // immutable binding (recommended)
var y = 3.14                // mutable binding
let n: int = 7              // explicit type annotation
let s: string = "hello"
let b: bool = true
let d: double = 1.5

Primitive types: int (i64), float / double, bool, string (C char*), void. Array types use T[] (e.g. string[]). Nullable types use T? (e.g. User?).

Integer literals accept three bases (v0.8.46+):

let dec = 255       // decimal
let hex = 0xFF      // hexadecimal — also 0X..., digits 0-9 a-f A-F
let bin = 0b1111    // binary       — also 0B..., digits 0/1

All three produce the same int value (255). Hex literals are passed through to C verbatim; binary literals are lowered to decimal at lex time so the C output stays portable (0b... is a GCC extension).

Operators

// Arithmetic
let a = 1 + 2 - 3 * 4 / 5 % 6
// Comparison
let cmp = a == 0 || a != 0 && a < 10
// Bitwise
let bits = (a & 0xff) | (a >> 4) ^ ~a << 1
// Compound assigns
var n = 0
n += 1
n -= 1
n *= 2
n /= 2
n %= 5
n &= 0xff
n |= 0x10
n ^= 0xa
n <<= 2
n >>= 1
// Range (used in for-in and match)
for i in 0..10 { /* … */ }
// Pipeline (left-to-right composition; sugar over a.f().g())
// e.g. x |> double |> Console.WriteLine    (when each accepts the previous result)

Precedence is the standard C/Java set; use parentheses when in doubt. Unary !, -, ~ exist; ! requires a bool operand; ~ is the bitwise complement.

Strings

let plain = "hello"
let escaped = "tab\there\nnewline"
let hex = "\x1b[31mred\x1b[0m"          // \xHH = one byte
let unicode = "héllo 中 €"      // \uHHHH = UTF-8 codepoint
let multi = """
line 1
line 2 with "quotes" and \n untouched
"""
let interp = "x={n} obj.f={obj.field} call={Math.Sqrt(16.0)}"

Interpolation supports simple variables, obj.field (where obj is a known local), this.field, and method/static calls (e.g. {Math.Sqrt(x)}, {String.Length(s)}). For deeper expressions like {Math.Sqrt(obj.field)} use a let-binding first:

let v = obj.field
"sqrt = {Math.Sqrt(v)}"

Classes

public class User {
    // Fields with explicit types
    public Name: string
    public Age:  int
    private Score: int

    // Constructor (method named after the class)
    public User(string name, int age) {
        this.Name  = name
        this.Age   = age
        this.Score = 0
    }

    // Methods
    public string Greet() {
        return "Hello, " + this.Name + "!"
    }

    public void AddScore(int points) {
        this.Score = this.Score + points
    }
}

Use:

let u = new User("Alice", 30)
Console.WriteLine(u.Greet())
u.AddScore(10)

Static methods

public class Calc {
    public static int Add(int a, int b) { return a + b }
    public static int Mul(int a, int b) { return a * b }
}

let n = Calc.Add(2, 3)

Inheritance

public class Animal {
    public Name: string
    public Animal(string n) { this.Name = n }
    public string Speak() { return "..." }
}

public class Cat extends Animal {
    public Cat(string n) {
        // No `super()` yet — assign fields directly when needed.
        this.Name = n
    }
    public string Speak() { return "meow" }
}

Data classes / records

data class Point(float X, float Y)
record Line(Point A, Point B)

Both expand into a class with public fields + a public constructor in field declaration order. Useful for plain value carriers.

Enums

Simple enum:

public enum Direction {
    North
    East
    South
    West
}

let d = Direction.North
match d {
    Direction.North => Console.WriteLine("⬆"),
    Direction.East  => Console.WriteLine("➡"),
    Direction.South => Console.WriteLine("⬇"),
    Direction.West  => Console.WriteLine("⬅")
}

Algebraic enum (tagged union):

public enum Shape {
    Circle(int)
    Rect(int, int)
    Triangle(int, int, int)
}

let s = Shape.Rect(4, 3)
match s {
    Circle(r)        => Console.WriteLine("circle r={String.FromInt(r)}"),
    Rect(w, h)       => Console.WriteLine("{String.FromInt(w * h)}"),
    Triangle(a,b,c)  => Console.WriteLine("triangle")
}

Variant payloads in the declaration are types only, not named parameters. Names are introduced in the match arm via the binder pattern: Circle(r) => ….

Interfaces

public interface Shape {
    int Area()
    string Name()
}

public class Square implements Shape {
    public Side: int
    public Square(int s) { this.Side = s }
    public int Area() { return this.Side * this.Side }
    public string Name() { return "square" }
}

Since v0.3.5, interfaces support generic params and the TypeChecker enforces the contract:

public interface IComparable<T> {
    Compare(T other) -> int
}

public class IntBox implements IComparable<int> {
    public Value: int
    public IntBox(int v) { this.Value = v }
    public int Compare(int other) { return this.Value - other }
}

The check substitutes T → int at the implements site, then verifies that IntBox has a Compare(int) -> int method. A mismatch produces a precise diagnostic naming the offending method/parameter and its expected vs got type.

Generics are still stripped at the C level (T → void*); the new check is a static contract layered on top of the existing duck-typed dispatch — there's no vtable yet.

Control flow

if (x > 0) {
    Console.WriteLine("positive")
} else if (x < 0) {
    Console.WriteLine("negative")
} else {
    Console.WriteLine("zero")
}

while (n > 0) {
    n = n - 1
}

for i in 0..10 { /* exclusive end: 0..9 */ }
for c in characters { /* iterate a List<T> */ }

break
continue

Block scope ({ … }, v0.8.35+)

A bare { … } at statement position introduces its own scope. Useful to give long methods visually-separated sections where each section can reuse local names without colliding at the C level:

public static void Run() {
    {
        let r0: int = 1
        let r1: int = 2
        // ... section A uses r0, r1 ...
    }
    {
        let r0: int = 10   // OK — fresh scope, no shadowing warning
        let r1: int = 20
        // ... section B reuses the same names ...
    }
}

Each block emits a matching C { … } wrapper, so let inside one block is invisible to the next. Mirrors C / C++ / Java scoping; required only when you want a fresh slot for the same name. if / while / for / match arm bodies already get their own scopes automatically.

Guard clauses

public static int Clamp(int x, int lo, int hi) {
    guard x > lo else { return lo }
    guard x < hi else { return hi }
    return x
}

Reads top-to-bottom: if the condition is false, run the else block (typically a return / throw / break / continue). Equivalent to if (!(cond)) { exit }.

Pattern matching

match n {
    0           => Console.WriteLine("zero"),
    x if x < 0  => Console.WriteLine("negative"),         // arm guard
    1..9        => Console.WriteLine("small"),            // range
    10..99      => Console.WriteLine("medium"),
    _           => Console.WriteLine("else")              // wildcard
}

Arm bodies are statements, not expressions. To compute a value, use early-return inside arms or assign in each branch:

public string Classify(int n) {
    if (n == 0) { return "zero" }
    if (n < 0)  { return "negative" }
    return "positive"
}

For algebraic enums, patterns destructure the payload:

match shape {
    Circle(r)  => useRadius(r),
    Rect(w, h) => useDims(w, h)
}

Try / catch / throw

try {
    risky()
} catch (e) {
    Console.WriteError("caught")
} finally {
    cleanup()
}

The implementation is setjmp/longjmp-based at the C level — no stack unwinding cost when no throw fires.

Tuples

public class Math2 {
    public static (int, int) DivMod(int a, int b) {
        return (a / b, a % b)
    }
}

let (q, r) = Math2.DivMod(17, 5)   // q = 3, r = 2

Lambdas

// Single-param, expression body
let double = x => x * 2

// Multi-param (v0.3.5)
let add  = (x, y) => x + y
let pick = (a, b, c) => a + b + c

// Block body (v0.3.5) — let-bindings + explicit return
let plus3 = x => {
    let doubled = x * 2
    return doubled + 3
}

// Closures capture enclosing locals by value (v0.3.4)
let n = 100
let shift = x => x + n           // captures n

Lambdas compile to a top-level C function plus an AmalgameClosure { fn, env } value; captured locals are snapshotted into a heap-allocated env at the creation site, not by textual substitution. The runtime exposes Closure_call1 / _call2 / _call3 for arities 1–3.

Higher-order List<T> methods accept a lambda directly (since v0.3.6 — see 04-stdlib.md for the full list):

let nums = new List<int>()
nums.Add(1) ; nums.Add(2) ; nums.Add(3) ; nums.Add(4)
let big   = nums.Filter(x => x > 2)              // [3, 4]
let times = nums.Map(x => x * 10)                // [10, 20, 30, 40]
let total = nums.Reduce(0, (acc, x) => acc + x)  // 10

First-class functions (Closure type, v0.8.30+)

Use the Closure type to declare function-valued class fields, method parameters, and collection elements. Lambdas literal- or named-bound to a Closure slot are usable like any other value, and a stored closure is invoked with the same field(args) syntax as a method call — the compiler dispatches through AmalgameClosure_callN under the hood.

public class Route {
    public Path:    string
    public Handler: Closure                  // ← first-class fn field

    public Route(string path, Closure handler) {
        this.Path    = path
        this.Handler = handler
    }

    public int Run(int x) {
        return this.Handler(x)                // ← invoke stored closure
    }
}

let r = new Route("/double", x => x * 2)
r.Run(21)                                    // → 42

// Closures in a collection.
let routes = new List<Route>()
routes.Add(new Route("/inc", x => x + 1))
let r0: Route = routes.Get(0)
r0.Run(99)                                   // → 100

This is the foundation of callback APIs (handler registries, event emitters, middleware chains, etc.).

Arities 1, 2 and 3 are supported. Closures of arity 0 or 4+ require a future cgen extension.

Typed closures (Closure<A, R>, v0.8.35+)

Annotate the closure shape to let the compiler emit typed casts on each argument and the return value. The last comma-separated parameter is R (the return type); the rest are argument types. Same convention as Kotlin's (A) -> R, just spelled inside <…>.

// Arity 1: scalar in, scalar out.
let f1: Closure<int, int> = x => x * 2
let r1: int = f1(21)                          // 42

// Arity 2: two scalars in, one out.
let f2: Closure<int, int, int> = (a, b) => a + b
let r2: int = f2(10, 20)                      // 30

// Pointer argument + pointer return.
let f3: Closure<User, string> = u => u.Name
let r3: string = f3(new User("alice", 30))    // "alice"

// Typed closure as a class field — call result is pointer-typed.
public class Server {
    public Handler: Closure<Conn, Conn>
    public Server(h: Closure<Conn, Conn>) { this.Handler = h }
    public int Dispatch(c: Conn) {
        let r: Conn = this.Handler(c)         // ← typed cast, no warning
        return r.Id
    }
}

The runtime ABI is unchanged — Closure<…> still lowers to AmalgameClosure*. The annotation is purely a compile-time hint that informs:

User code that previously needed -Wno-int-conversion to silence "pointer from integer" noise around closure boxing can drop those flags once the relevant Closure fields/params/locals carry their typed shape.

Direct lambda args (v0.8.36+). Lambda-param inference also fires when the lambda is passed directly to a ctor or method whose parameter is declared Closure<A, R> — the resolver walks the target signature and pushes A into the lambda's first PARAM. All three forms below produce the same typed unpack with no -Wint-conversion noise:

// Direct ctor arg — c inferred as WebContext from Route's signature.
new Route(c => Program.handle(c))

// Method arg — same inference path.
reg.Register("name", c => Program.handle(c))

// Bind to a typed local — c inferred from the local annotation.
let h: Closure<WebContext, HttpResponse> = c => Program.handle(c)
new Route(h)

// Or type the lambda param explicitly — overrides inference.
new Route((c: WebContext) => Program.handle(c))

List literals and comprehensions

Square-bracket syntax builds a List<T> inline. Comma-separated expressions are literals; the for x in iter form is a comprehension that maps and optionally filters:

// Empty list — element type comes from the target slot.
let empty: List<int> = []

// Literal with explicit elements.
let names: List<string> = ["alpha", "beta", "gamma"]

// Mixed with computed values and trailing comma.
let n = 7
let nums: List<int> = [
    n,
    n + 1,
    n * 2,
]

// Function argument.
TakeList(["x", "y", "z"])

// List comprehension — map a range.
let squares: List<int> = [i * i for i in 0..10]
// → [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

// Comprehension with filter.
let evens: List<int> = [i for i in 0..20 if i % 2 == 0]

// Iterate an existing list — `x` is bound element-by-element.
let upper: List<string> = [String_ToUpper(name) for name in names]

Both forms lower to the same shape — a GCC compound-statement expression that allocates a fresh AmalgameList* and pushes each boxed element. Literals are equivalent to the longhand new List<T>(); list.Add(...) series — use whichever reads better at the call site.

The comprehension supports two iterable shapes:

Same-name nesting ([ [j for j in 0..i] for i in 0..3 ]) isn't supported yet — pick distinct loop variables when nesting.

Spread operator ...src (v0.8.36+). Inside a list literal, each ...src item splices src's elements at its position:

let a: List<int> = [1, 2, 3]
let b: List<int> = [4, 5]
let c: List<int> = [...a, ...b, 99]
// → [1, 2, 3, 4, 5, 99]  (Count() == 6)

// Mix spreads with literal elements at any position.
let names = ["alice", "bob"]
let all   = ["zero", ...names, "tail"]
// → ["zero", "alice", "bob", "tail"]

// Spread the result of a method call — source is evaluated once
// (bound to a synthetic `__sp_<i>` local in the generated C).
let doubled = [...nums.Map(x => x * 2), 999]

Variadic parameters ...name: ElemType (v0.8.61+). A method's trailing parameter may be variadic; inside the body it's a List<ElemType>. Callers pass zero or more args in that position, or splice an existing list with ...expr:

public static int Sum(...nums: int) {
    var total: int = 0
    for n in nums { total = total + n }     // nums is a List<int>
    return total
}

public static int SumFrom(int base, ...nums: int) { ... }   // fixed + variadic tail

Sum()                  // 0   — zero args → empty list
Sum(1, 2, 3, 4)        // 10  — inline args
let xs = new List<int>()
Sum(...xs)             // splice a list into the variadic slot

Constructors can be variadic too (Bag(...nums: int), v0.8.63+).

Async / await async fn + await (v0.8.70). An async fn returns a future; await unwraps it. The sugar lowers onto the amalgame-async runtime (so a program using it depends on that package). Full coverage — fibers, channels, the scheduler — is in 13-async.md:

public async fn Compute(int n): int { return n * 2 }
// let f = w.Compute(21);  let r: int = await f   // r == 42

Nested generics List<List<T>> (v0.8.36+). Chained .Get(i).Get(j) peels one generic layer per hop, so each level gets a typed cast instead of falling through to void*:

let rows: List<List<string>> = …
let s: string = rows.Get(0).Get(0)   // proper code_string cast

let cube: List<List<List<int>>> = …
let v: int = cube.Get(0).Get(0).Get(0)  // unbox via intptr_t

Works up to arbitrary depth, both for List<…> and the value- half of Map<K, …>. The raw element type is tracked at every let annotation + every typed method return.

Higher-order methods on List<Class> (v0.8.35+). Both xs.Filter(x => x > 0) and xs.Map(x => x.Name) over a List<User> now work end-to-end. The resolver patches the lambda's first param from the receiver's element type, the cgen emits a typed unpack (User* x = (User*)__arg0;) and the consumer-side for u in filterResult lowers with the right element cast (no manual as User* needed). Untyped lists (new List<int>() with no annotation) still fall back to void* and require explicit casts.

Null safety

T? declares a nullable. ?. short-circuits to null when the receiver is null:

let user: User? = null
let name = user?.Name              // null
let len  = user?.GetNameLength()   // null
if (name == null) {
    Console.WriteLine("anonymous")
}

?? is null-coalescing (returns the right operand when the left is null):

let display = user?.Name ?? "anonymous"

Decorators

public class Math2 {
    @inline
    public static int Square(int x) { return x * x }

    @deprecated
    public static int Old(int x) { return x }
}
Decorator Effect on emitted C
@inline adds inline to the function definition
@deprecated adds __attribute__((deprecated)) to the prototype

Other decorators are accepted by the parser but currently no-ops.

Named arguments

let p = new Person(name: "Bastien", age: 31)
Math2.Clamp(x: 50, lo: 0, hi: 10)

Names are documentation-only at the call site for now: values are passed in source order regardless of the names. Tracked in ROADMAP_COMPLET.md.

Library mode

Files without a Program.Main compile as libraries automatically. Use --lib to force library mode even when a Program.Main exists.

./amc --lib mylib.am -o mylib
gcc -Iruntime -c mylib.c -o mylib.o
# Link mylib.o into a host program from C, Amalgame, etc.

Reading diagnostics

Errors come with a source snippet and a caret, like rustc:

error[typechecker]: Cannot assign 'string' to 'n' of type 'int'
  --> /tmp/test.am:19:13
   |
19 |         let n: int = p.Name
   |             ^

error[resolver]: Unknown symbol 'someThing'
  --> /tmp/test.am:4:9
  |
4 |         someThing.x()
  |         ^

The two passes are:

Both errors are non-fatal individually; the compiler still emits a .c file when possible (so you see all errors at once), but exits non-zero when any error was reported.

What's not in the language yet

See ROADMAP_COMPLET.md for the full backlog.