Language Reference

Jam Reference

Jam is a statically typed systems language with mutable value semantics, generics over types, tagged unions, and an LLVM backend. The compiler infers types where it can; everything across a function boundary is fully explicit.

Variables & Constants

Jam has two binding forms inside functions: var for mutable storage and const for single-assignment values. Every binding must be initialized at its declaration, Jam has no undefined placeholder.

fn example() {
    var counter: i32 = 0;
    counter = counter + 1;

    const limit: i32 = 100;
    // limit = 101;  // error: cannot reassign const binding
}

At module scope, const declarations bind a name to a compile-time value. They are inlined at every use site, referring to one costs the same as a literal. The type may be inferred when an initializer alone determines it.

const FLAG_Z: u8 = 0x80;
const PAGE:   u32 = 4096;
const SHIFT       = 4;            // type inferred

fn isZSet(f: u8) bool {
    return (f & FLAG_Z) != 0;     // FLAG_Z inlined here
}

Module-scope const is also how top-level types, imports, and function pointers are named, Jam funnels every top-level declaration through the same form.


Integer Types

An integer is a number without a fractional component. Jam provides both signed (i) and unsigned (u) integers of various sizes.

Length Signed Unsigned Range (Signed) Range (Unsigned)
8-bit i8 u8 -128 to 127 0 to 255
16-bit i16 u16 -32,768 to 32,767 0 to 65,535
32-bit i32 u32 -2,147,483,648 to 2,147,483,647 0 to 4,294,967,295
64-bit i64 u64 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 0 to 18,446,744,073,709,551,615
Arch isize usize Depends on computer architecture (32 or 64-bit)  
fn main() u8 {
    const smallNum: u8  = 255;
    const bigNum:   u64 = 18446744073709551615;

    const temperature: i32 = -40;
    const altitude:    i64 = -11034;

    var counter: i8 = -128;
    return 0;
}

Numeric literals can use underscores for readability (0x1234_5678, 1_000_000) and the standard 0x, 0o, 0b prefixes.

Choosing Integer Types

Use unsigned types (u8, u16, etc.) for values that are never negative, indices, counts, byte sizes. Use signed types (i8, i16, etc.) when you need to represent negative values.


Boolean Type

The bool type represents a value that can be either true or false. Booleans are one byte in size.

const isSweet: bool = true;
const isSour:  bool = false;

if (isSweet) {
    std.fmt.println("Delicious!");
}

Logical Operators

Jam provides three logical operators for working with boolean values:

Operator Name Description
! NOT Negates a boolean value
and AND Returns true if both operands are true (short-circuits if first is false)
or OR Returns true if either operand is true (short-circuits if first is true)

The and and or operators use short-circuit evaluation: if the result can be determined from the first operand alone, the second operand is not evaluated.

fn checkAccess(isAdmin: bool, isOwner: bool) bool {
    return isAdmin or isOwner;
}

fn validate(hasEmail: bool, hasPassword: bool) bool {
    return hasEmail and hasPassword;
}

Strings

Strings in Jam are represented as slices of bytes ([]u8). The str type is an alias for []u8. Element mutability follows the binding: const s: []u8 = "..." is read-only; var s: []u8 = "..." permits writes.

const greeting: str   = "Hello, World!";
const message:  []u8  = "Same as str";

// Strings are UTF-8.
const chinese: str = "世界";
const emoji:   str = "🌍";

Memory Layout

A slice is a two-word value: a pointer to the data and a length.

Field Type Description
ptr *const[] u8 Many-item pointer to the first byte
len u64 Number of bytes (excludes any trailing null in literals)

Escape Sequences

Escape Description
\n Newline
\r Carriage return
\t Tab
\\ Backslash
\" Double quote
\' Single quote
\0 Null byte
\xHH Hex byte (2 hex digits)
\u{HHHHHH} Unicode codepoint (1-6 hex digits, encoded as UTF-8)
const newline: str = "Line1\nLine2";
const quote:   str = "She said \"Hello\"";
const hello:   str = "\x48\x65\x6C\x6C\x6F";   // "Hello"
const earth:   str = "\u{1F30D}";              // 🌍

Pointers & Slices

Jam distinguishes three reference families. Pointer types take a required const or mut qualifier marking whether the pointee may be written. Slices and fixed arrays carry no qualifier, their element mutability follows the binding.

Type Description
*const T Single-item pointer, read-only pointee
*mut T Single-item pointer, writable pointee
*const[] T Many-item pointer (indexable), read-only pointee
*mut[] T Many-item pointer (indexable), writable pointee
[]T Slice (pointer + length); element mutability follows the binding
[N]T Fixed-size array of N elements; element mutability follows the binding

The & prefix takes the address of a binding or array element; p.* dereferences a pointer. Indexing through a many-item pointer is p[i].

fn pointerExample() u8 {
    var x: u8 = 42;
    var p: *mut u8 = &x;
    p.* = 100;
    return x;                       // 100
}

fn indexThroughMany() u8 {
    var arr: [4]u8 = [10, 20, 30, 40];
    var p: *mut[] u8 = &arr[0];
    p[2] = 99;
    return arr[2];                  // 99
}

No bare *T

The mutability qualifier is part of the pointer type, there is no shorthand *T. Writing *const u8 vs *mut u8 documents intent at every signature.


Arrays

Fixed-size arrays have a length known at compile time. Array literals come in three forms: comma-separated, fill-with-count, and empty (which produces a zero-length slice).

var a: [4]u8  = [10, 20, 30, 40];     // [a, b, c, d]
var b: [16]u8 = [0; 16];              // [expr; N], fill 16 slots with 0
const empty: []u8 = [];               // empty slice

// Index with []. Out-of-bounds is undefined at runtime;
// bounds checks are not yet inserted.
var x: u8 = a[2];                     // 30
a[0] = 99;

A fixed array implicitly coerces to a slice when bound to a []T location, producing a two-word {ptr, len} view over the same storage.


Control Flow

Jam has if/else, while, for, and return. Conditions require parentheses.

fn classify(n: i32) i32 {
    if (n < 0) {
        return -1;
    } else if (n == 0) {
        return 0;
    } else {
        return 1;
    }
}

fn sumTo(n: u32) u32 {
    var total: u32 = 0;
    var i: u32 = 0;
    while (i < n) {
        total = total + i;
        i = i + 1;
    }
    return total;
}

fn fillIndices() [16]u8 {
    var arr: [16]u8 = [0; 16];
    for i in 0:16 {
        arr[i] = i as u8;
    }
    return arr;
}

The for loop iterates a half-open integer range, for i in 0:N binds i = 0, 1, …, N-1.


Functions

Functions are declared with fn. Parameters are strictly typed; the return type goes directly after the parameter list. Jam code uses camelCase by convention for functions and variables.

// Two i32 inputs, returns i32.
fn mixIngredients(sugar: i32, fruit: i32) i32 {
    return sugar + fruit;
}

// No return value, omit the return type.
fn greet() {
    std.fmt.println("Hello!");
}

Functions can be prefixed with pub (visible outside the defining module) and extern (declared but defined elsewhere, typically libc).

pub fn add(a: i32, b: i32) i32 { return a + b; }

pub extern fn malloc(size: u64) *mut[] u8;
pub extern fn free(ptr: *mut[] u8);

Void Functions

A function with no declared return type returns nothing. Don't write void, just omit the return-type slot.


Type Casts

The as operator performs explicit conversions. Integer ↔ integer casts truncate or extend; integer → float converts numerically; an enum tag can be extracted to its underlying integer type.

const big: u32 = 0x1234_5678;
const low: u8  = big as u8;            // truncate → 0x78

const small: u8  = 200;
const wide:  u32 = small as u32;       // zero-extend → 200

const negative: i32 = 250;
const narrow:   i8  = negative as i8;  // wrap → -6

Casts are explicit at every narrowing or sign-changing step. There are no implicit numeric conversions.


Structs

A struct groups fields under a single name. Top-level structs are declared via const Name = struct { … };. Field names are field: Type; instances are built with { field: value, … }.

const Vec3   = struct { x: f32, y: f32, z: f32 };
const Pixel  = struct { r: u8, g: u8, b: u8 };
const Player = struct { hp: u32, level: u8, alive: bool };

fn main() {
    const v: Vec3 = { x: 0, y: 100, z: 50 };
    var px:  Pixel = { r: 10, g: 20, b: 30 };
    px.r = 100;

    // Nested literals work the same way.
    const Outer = struct { inner: Pixel, c: u8 };
    const x: Outer = {
        inner: { r: 1, g: 2, b: 3 },
        c: 4,
    };
}

Methods

Functions declared inside a struct body are methods. The first parameter is conventionally self; the parameter mode chooses borrow semantics.

const Counter = struct {
    value: u32,
    sink:  *mut u32,

    fn drop(self: mut Counter) {
        var p: *mut u32 = self.sink;
        p.* = p.* + 1;
    }
};

fn observe() u32 {
    var hits: u32 = 0;
    var c: Counter = { value: 5, sink: &hits };
    return c.value;
    // c.drop() fires automatically here, see Drop, below.
}

A method can also be invoked by-name on the type: Counter.drop(&c) calls it explicitly while c is still in scope (and the automatic drop will still fire at scope exit, calling drop manually is currently a footgun).


Enums

Enums describe a closed set of named variants. The simplest form is payload-less, each variant is a u8 discriminant.

const Color = enum { Red, Green, Blue };

fn classifyColor(c: Color) u8 {
    match (c) {
        Color.Red   { return 100; }
        Color.Green { return 200; }
        Color.Blue  { return 50; }
        _           { return 0; }
    }
    return 99;
}

Discriminant values are assigned in declaration order starting at 0. You can also pin them explicitly:

const Phase = enum { Idle = 0, Running = 5, Stopping = 9 };

fn phaseAsByte(p: Phase) u8 {
    return p as u8;
}

Variants with Payloads (Tagged Unions)

Variants can carry positional fields. Pattern matching destructures them by name.

const Op = enum {
    Nop,
    LdRR(u8, u8),
    Imm(u8),
    Wide(u16),
};

fn srcReg(op: Op) u8 {
    match (op) {
        Op.Nop          { return 0xFF; }
        Op.LdRR(d, s)   { return s; }
        Op.Imm(v)       { return v; }
        Op.Wide(w)      { return 0xEE; }
        _               { return 0xCC; }
    }
    return 0;
}

Payload-carrying enums are how Option(T) and Result(T, E) are built, see Generics.


Unions

A union is untagged, every field shares the same storage. Use it for type punning (read float bits as u32, etc.) and for matching C-side union { … } types at FFI boundaries.

const FloatBits = union {
    i: u32,
    f: f32,
};

fn floatBits(x: f32) u32 {
    var b: FloatBits = { f: x };
    return b.i;
}

Unions size to their largest field; alignment is the max of all fields’. Unlike enums, there is no discriminant, the compiler trusts the program to know which field is live.


Pattern Matching

The match expression dispatches on a scrutinee. Patterns include integer literals, enum variants (with or without payloads), and _ as a wildcard.

fn dispatch(x: u8) u8 {
    match (x) {
        0           { return 10; }
        1 or 2 or 3 { return 20; }       // `or`-joined patterns
        4..=9       { return 30; }       // inclusive range
        _           { return 255; }
    }
    return 99;
}

A match can also be used as an expression, each arm produces a value, and the result is the value of the matched arm.

fn unwrap(o: Option(i32), fallback: i32) i32 {
    return match (o) {
        Option(i32).Some(x) { x }
        Option(i32).None    { fallback }
    };
}

Arms that bind payload fields introduce those names into the arm’s scope.


Generics

A generic function takes one or more type parameters declared T: type and returns a type. Calling it with concrete type arguments at compile time produces a concrete type; each distinct argument list yields a fresh struct (or enum).

fn Box(T: type) type {
    return struct {
        value: T,
    };
}

fn Pair(A: type, B: type) type {
    return struct {
        first:  A,
        second: B,
    };
}

fn main() {
    var b: Box(i32)        = { value: 17 };
    var p: Pair(i32, u8)   = { first: 7, second: 35 };
}

A type alias const Name = Generic(arg); registers Name as a synonym, subsequent uses resolve to the same instantiated struct.

const BoxI32 = Box(i32);
var b: BoxI32 = { value: 17 };

Generic Methods

Methods on generic types are cloned per instantiation. Each instantiation gets its own LLVM symbol with substituted parameter and return types.

fn Holder(T: type) type {
    return struct {
        value: T,
        fn unwrap(self: move Self) T {
            return self.value;
        }
    };
}

Self inside a struct body refers to the enclosing struct type, for a generic, that’s the specific instantiation in play.

Generic Enums

The same mechanism produces tagged unions parameterized by type. The standard library’s Option(T) is built this way:

pub fn Option(T: type) type {
    return enum {
        Some(T),
        None,
    };
}

Construct variants by qualifying with the instantiated type: Option(i32).Some(42), Option(i32).None.


Modules & Imports

import("name") returns a module value, a compile-time record of the symbols another file exports. Bind it through const either whole or destructured.

// Whole-module binding.
const std = import("std");

fn show() {
    std.fmt.println("Hello!");
}

// Destructured binding, pulls specific names into the current scope.
// Re-exports under `std` can be reached via chained access on the
// import expression — pick the names you need without binding the
// whole module.
const { Vec }    = import("std").collections;
const { Option } = import("std").option;
const { assert } = import("test");

The path is resolved relative to the file (./collections.jam or std/string.jam) or to a built-in name (std, test). Modules can re-export by binding to a local pub name.


Comptime Intrinsics

Intrinsics are compiler builtins prefixed with @. They run at compile time and their results are substituted as constants before LLVM sees the code.

Intrinsic Description
@sizeOf(T) Size of T in bytes (u64)
@alignOf(T) Alignment of T in bytes (u64)
fn bytesFor(n: u64) u64 {
    return n * @sizeOf(u32);          // n * 4, fully constant-folded
}

fn pointerSize() u64 {
    return @sizeOf(*const u8);        // 8 on a 64-bit target
}

fn sliceSize() u64 {
    return @sizeOf([]u8);             // 16, {ptr, len}
}

Intrinsics compose freely with runtime arithmetic. They produce a u64 and require an explicit cast to narrow to other widths.


extern / FFI

extern declarations link to symbols defined in C (or any system providing the C ABI). The compiler does not generate a body; it just emits a call. Most stdlib allocators use this.

pub extern fn malloc(size: u64)  *mut[] u8;
pub extern fn free(ptr: *mut[] u8);
pub extern fn realloc(ptr: *mut[] u8, size: u64) *mut[] u8;

fn allocOne() *mut[] u32 {
    var raw: *mut[] u8 = malloc(@sizeOf(u32));
    return raw as *mut[] u32;
}

Param types must match the C ABI. Slice arguments are passed as a {ptr, len} pair, and struct arguments larger than two words use sret-style return ABI, these match the platform’s C compiler.


Mutable Value Semantics

Jam is a mutable value semantics language. Every binding owns its value; passing a value to a function does not silently share storage with the caller. Functions opt into a specific borrowing or transfer behavior via a per-parameter mode keyword.

The result is the same memory safety as Rust, no use-after-free, no double-free, no data races, no reads of uninitialized memory, with no lifetime annotations.

The design is inspired by Hylo’s parameter-mode system and Rust’s drop semantics. The compiler enforces three rules: definite initialization (every binding is written before it is read), exclusivity (no overlapping mutable borrows), and linear drops (every owned value gets exactly one destructor call).

Parameter Modes

Every function parameter is declared in one of three modes. The default mode is read-only; the other two are introduced by an explicit keyword between the colon and the type.

Mode Keyword Pointee Mutability Caller After Call
Read-only borrow (default) read-only unchanged
Exclusive read-write mut read-write unchanged
Consume ownership move read-write becomes uninitialized
// Default read-only borrow, caller's value is unchanged.
fn distance(a: Point, b: Point) f64 {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

// Exclusive read-write, caller passes `&binding`.
fn scale(p: mut Point, factor: f64) {
    p.x = p.x * factor;
    p.y = p.y * factor;
}

// Consume, caller's binding is uninitialized after the call.
fn storeIn(buf: move []u8, db: mut Database) {
    db.append(buf);
}

At the call site, a mut parameter is passed with & to make the borrow explicit:

var p: Point = { x: 3.0, y: 4.0 };
scale(&p, 2.0);                  // mut borrow, explicit &
distance(p, otherPoint);          // read-only, no sigil

move parameters take a plain expression, the binding is dead in the caller after the call.

Exclusivity

At any call boundary, at most one argument may be a mut borrow of a given binding, and a mut borrow cannot coexist with any other borrow of the same binding. The compiler rejects programs that violate this rule, the same “law of exclusivity” Rust’s borrow checker enforces, applied locally at each call rather than across whole-program lifetimes.

fn modify(x: mut u32, y: u32) u32 {
    x = x + y;
    return x;
}

fn caller() u32 {
    var n: u32 = 5;
    return modify(&n, n);   // error: conflicting borrows of `n`
}

Drop

A struct may define a drop method that runs automatically when an owned instance goes out of scope. The drop method takes self: mut Self and runs exactly once per owned value, even on early returns, in match arms, and through nested control flow. There is no manual defer ceremony.

const File = struct {
    fd: i32,

    fn drop(self: mut File) {
        close(self.fd);
    }
};

fn readFile(path: []u8) i32 {
    var f: File = openFile(path);
    return f.fd;
    // f.drop() runs here automatically
}

A value moved into another function (via the move mode) becomes uninitialized in the caller, so the drop fires at the new owner’s scope exit, never twice.