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 |
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 | Returns true if both operands are true (short-circuits if first is false) |
\|\| |
OR | Returns true if either operand is true (short-circuits if first is true) |
The && and \|\| operators use short-circuit evaluation: if the result can
be determined from the first operand alone, the second operand is not
evaluated; a call on the right-hand side does not run.
fn checkAccess(isAdmin: bool, isOwner: bool) bool {
return isAdmin || isOwner;
}
fn validate(hasEmail: bool, hasPassword: bool) bool {
return hasEmail && 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) |
Slicing & Comparison
base[start..end] produces a sub-slice. The base may be a slice, a fixed array, or a
many-item pointer; bounds are the caller’s responsibility, like indexing. == and !=
compare two slices by length and contents (float element types are rejected; NaN makes
byte equality a lie; compare those with a loop).
const s: str = "hello world";
const hello: []u8 = s[0..5];
const world: []u8 = s[6..11];
if (hello == "hello") { /* taken */ }
if (hello != world) { /* taken */ }
var arr: [6]u16 = [1, 2, 3, 4, 5, 6];
const mid: []u16 = arr[2..5]; // [3, 4, 5]
std.string layers call-style helpers on top: eq, startsWith, endsWith, find,
contains, slice, trim/trimLeft/trimRight, concat, fromInt, parseInt.
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.
With a literal end bound the index is a u64; with a variable bound it takes that
variable’s type.
Without a :, for walks a slice or array element-wise. The loop var binds each
element in place, like s[i] as an lvalue, so assigning through it writes to the
underlying storage.
fn sumBytes(s: []u8) u32 {
var total: u32 = 0;
for b in s {
total = total + (b as u32);
}
return total;
}
fn firstFourSquares() [4]u16 {
var arr: [4]u16 = [1, 2, 3, 4];
for v in arr {
v = v * v;
}
return arr; // [1, 4, 9, 16]
}
loop { … } runs forever (sugar for while (true)); break exits the innermost
loop and continue jumps to its next iteration.
fn firstPowerAbove(n: u64) u64 {
var p: u64 = 1;
loop {
p = p * 2;
if (p > n) { break; }
}
return p;
}
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.
Function Pointers
fn(params) Ret is a first-class type: store one in a local or a struct field
and call it through the binding. someFn as u64 takes a function’s raw
address (for FFI hand-off and @callC).
fn add(a: i32, b: i32) i32 { return a + b; }
fn mul(a: i32, b: i32) i32 { return a * b; }
const Op = struct { name: str, apply: fn(i32, i32) i32 };
fn run() i32 {
var f: fn(i32, i32) i32 = add;
const r: i32 = f(2, 3); // 5
const op: Op = Op { name: "mul", apply: mul };
return r + op.apply(2, 3); // 5 + 6
}
There are no closures: a callback that needs state takes an explicit context
argument (see std/box and jam-objc’s block support for the pattern).
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
TypeName { field: value, … }: literals always name their type.
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 = Vec3 { x: 0.0, y: 100.0, z: 50.0 };
var px: Pixel = 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 = Outer {
inner: Pixel { 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 = 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 = 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 | 2 | 3 { return 20; } // `|`-joined patterns
4..=9 { return 30; } // inclusive range
_ { return 255; }
}
return 99;
}
Range bounds are full 64-bit and may be negative (-100..=-1); an inverted
range is a compile error. When the scrutinee’s enum type is unambiguous the
variant may be written bare: Some(x) instead of Option(i32).Some(x).
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.
A match expression must be exhaustive: cover every variant of the enum or
add a _ arm; anything else is a compile error (a fallthrough would leave the
result undefined). Statement-position matches keep switch-style fallthrough.
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) = Box(i32) { value: 17 };
var p: Pair(i32, u8) = 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 = 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.
Result(T, E) ships the same way (import("std").result). An instantiation can be
bound to a module-scope alias and used like any named type:
const { Result } = import("std").result;
const ParseResult = Result(u64, str);
fn parse(s: []u8) ParseResult {
if (s.len == 0) {
return ParseResult.Err("empty input");
}
return ParseResult.Ok(s.len);
}
Compile-Time Execution
comp marks bindings and parameters the compiler evaluates during
compilation. A comp const/comp var folds to a constant; comp if selects
a branch at compile time and the dead arm is never lowered; a comp
parameter specializes the function per call-site value (each distinct value
gets its own clone).
comp const WORDS: u32 = 4;
fn scale(comp k: u32, x: u32) u32 { // one clone per distinct k
return x * k;
}
fn size() u32 {
comp if (WORDS > 2) {
return scale(8, WORDS);
} else {
return scale(2, WORDS);
}
}
cfn is the second compile-time form. A top-level cfn runs its body at
compile time and emits code into the caller (std.fmt.print is one; the
format string is parsed during compilation). A cfn method is an
ordinary runtime function that opts into compiler-synthesized call sites:
cfn drop registers with automatic drop, cfn clone with .clone(),
cfn at/setAt/len with v[i] indexing (on plain bindings and field
chains alike: h.buf[i] dispatches the same way), and a cfn method may
declare a variadic pack (see extern / FFI).
Testing
tfn declares a test. jam test <file-or-dir> compiles every tfn,
synthesizes a main that runs them, and reports each by name.
import("test") exports assert(actual, expected).
const { assert } = import("test");
fn double(x: u64) u64 { return x * 2; }
tfn doubling() {
assert(double(21) as i32, 42);
}
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); @sizeOf(void) is 0 |
@alignOf(T) |
Alignment of T in bytes (u64) |
@os() |
Target OS name as a str ("macos", "linux", …) |
@isDarwin() @isLinux() @isWindows() @isUnix() |
Target-OS predicates; fold to a bool literal and dead arms drop |
@isX86_64() @isAarch64() |
Target-arch predicates (honour -C target); same folding |
@dropInPlace(ptr) |
Run the pointee type’s drop glue at ptr |
@callC(R, addr, args...) |
Indirect C call through a raw u64 address; the fixed C signature is synthesized from R and the args’ static types (see extern / FFI) |
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.
Calling raw function addresses: @callC
When the callee is only known as an address (dlsym results,
objc_msgSend-style dispatchers), @callC(R, addr, args...) performs an
indirect call with the C calling convention. R is the return type (void
is accepted here), addr is a u64, and each argument’s static type
becomes the corresponding parameter type of the synthesized signature, with no
coercion happens at the boundary, so spell scalar types explicitly
(0 as i64, x as u64).
pub extern fn fabs(x: f64) f64;
fn callThroughAddress() f64 {
const addr: u64 = fabs as u64;
return @callC(f64, addr, -2.5); // fn(f64) f64, by address
}
By-value aggregates are allowed when they are homogeneous float aggregates
of at most 4 same-width floats ({f64,f64,f64,f64}, FP registers) or at
most two full 64-bit words ({u64,u64}, GP registers); other shapes are
rejected at compile time.
Variadic cfn: forwarding argument packs
A cfn method may declare one trailing argument pack, args: .... The
method is instantiated per call-site shape (each pack argument’s static
type becomes a real parameter), and the pack’s only use is forwarding:
args... at the end of a call-argument list, most usefully into @callC
or an extern C-variadic like printf:
const Caller = struct {
addr: u64,
cfn callThrough(self: Caller, R: type, args: ...) R {
return @callC(R, self.addr, args...);
}
};
const n: i32 = c.callThrough(i32, -7 as i32); // fn(i32) i32
const d: f64 = c.callThrough(f64, 1.5, 2.5); // fn(f64, f64) f64
The pack cannot be read, indexed, or stored, only spread. Packs are not
available on plain fn or on top-level (comptime) cfns.
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, declared at the signature only.
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);
}
Call sites are sigil-free for every mode: the signature’s mode declaration does all the work, and the argument is always a plain expression:
var p: Point = Point { x: 3.0, y: 4.0 };
scale(p, 2.0); // mut access, no sigil
distance(p, otherPoint); // read-only, no sigil
The default mode is enforced: writing into a default-mode parameter (the binding
itself, a field, an array or slice element, or a for-each element view over it)
is a compile error. Writes through a pointer-typed value (p.*, self.ptr[i])
are governed by the pointer’s own mut/const qualifier, not the parameter’s mode.
fn broken(arr: [4]u16) {
arr[0] = 99; // error: parameters are read-only borrows by default
}
The borrow follows the value through calls: a returned slice, whether bare, carried
in a struct’s fields (Pair { data: v.asSlice(), n: 0 }), or in an enum payload
(Option([]u8).Some(v.asSlice())), is tracked back to the arguments it derives
from, so views of a read-only parameter reject writes no matter how many bindings,
calls, or match arms they pass through, including locals assembled inside the
callee and then returned (var p = ...; p.data = v.asSlice(); return p;). Scalar
payloads and fields are plain copies and stay freely mutable. A view-carrying struct still owns its
inline fields: p.n = 5 and whole rebinds stay legal; only writes reaching the
borrowed elements are rejected. Passing read-only storage as a mut
argument or calling a mut self method on it, directly (v.push(1)) or through a
field chain (h.buf.push(1)), is rejected the same way; the fix is always to take
the parameter as mut in the signature.
There is no reference type in jam, so & is not a borrow operator: it is address-of,
producing a *mut T / *const T pointer value, and is only meaningful where the
parameter’s type is a pointer (FFI, sink out-params). Writing &x for a mode
parameter is a compile error.
After a move call the binding is dead in the caller; reading it is a compile error.
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.
A move parameter is owned by the callee: it drops when the function exits, unless
the body moves it onward: into a container slot, a struct-literal field, another
move call, or a return. Binding a bare drop-bearing value to a new name
(var owned = c;) or storing it (arr[i] = c;) is likewise a move, never a copy.
Assigning over a live drop-bearing value (c = newCounter();, h.field = x;,
v[i] = x;) drops the previous occupant before the store, so overwriting never
leaks.
Moving a drop-bearing value out of a let/mut parameter is rejected; those are
borrowed, not owned; declare the parameter move to take ownership.
Matching a drop-bearing enum by value consumes the scrutinee: arm bindings take
ownership of the payload (they drop at the arm’s end unless moved onward), arms
that don’t bind drop the residual payload on entry, and a temporary scrutinee
(match (v.pop())) is owned by the match itself. Matching a let/mut
parameter’s enum is rejected (borrowed values can’t be consumed); clone the
scrutinee or take it by move.
When both values are genuinely needed, clone explicitly: x.clone() is a deep
copy. Plain data clones for free (it is the value), structs without their own
drop clone field-wise, arrays clone element-wise, and a type that owns
resources (has cfn drop) must define cfn clone(self: Self) Self to say how
its resource duplicates: the compiler can clone structure, never resources.
Container clones are conditional: Vec(T).clone() exists exactly when T is
cloneable. There are no implicit clones.
Drops stay static: jam never inserts runtime drop flags. Two restrictions on
drop-bearing bindings keep that possible: a move must be unconditional relative to the
binding’s declaration (moving inside an if arm, loop body, or match arm that does
not also contain the declaration is rejected with “move it on all control-flow paths
or none”), and a moved binding cannot be re-assigned; bind a new name instead. Where
Rust would insert a runtime drop flag for a conditionally-moved value, jam rejects the
program, the same static resolution Swift’s noncopyable types use.