Iron Language Reference
Complete guide to the Iron programming language — a compiled, strongly-typed language designed for game development.
Overview
Iron is a compiled, performant programming language focused on game development. It compiles to C and produces native standalone binaries.
- File extension:
.iron - CLI:
iron build,iron run,iron test,iron check,iron fmt - Compiles to C, then to native binaries via clang/gcc
- Platforms: macOS (arm64, x86_64), Linux, Windows
Philosophy
- Concise: few keywords that express great meaning
- Strong types, no implicit conversions
- Manual memory management with built-in helpers (defer, ref counting)
- Brace-delimited blocks
- Null safety by default
- Legibility over magic
- No operator overloading, no hidden control flow
Primitive Types
All primitive types start with uppercase.
Int -- platform int (64-bit) Int8 Int16 Int32 Int64 UInt -- platform unsigned UInt8 UInt16 UInt32 UInt64 Float -- f64 Float32 Float64 Bool -- true / false String -- always unicode, always iterable
There is no Char type. A single character is a String of length 1.
Variables — val / var
val name = "Victor" -- immutable, type inferred var hp = 100 -- mutable, type inferred val speed: Float = 200.0 -- immutable, type explicit name = "Other" -- COMPILE ERROR: val cannot be reassigned hp = 90 -- ok
val= immutable bindingvar= mutable binding- Type is inferred by default, can be explicit with
: Type
Nullable Types
Non-nullable by default. Opt in with ?.
val name: String = "Victor" name = null -- COMPILE ERROR: String is not nullable var target: Enemy? = null -- explicitly nullable target.attack() -- COMPILE ERROR: must check first if target != null { target.attack() -- ok, compiler narrows type }
Strings
Strings are immutable reference types. Passed by reference (no copy of character data), but characters can never be modified — operations always create new strings.
val greeting = "Hello World" -- length, iteration print(len(greeting)) for c in greeting { print(c) } -- string interpolation val name = "Victor" val hp = 100 print("{name} has {hp} HP") -- multiline val text = """ multi line string """ -- common operations val upper = greeting.upper() val sub = greeting[0..5] val has = greeting.contains("World") val parts = greeting.split(" ")
Passing Semantics
Strings are passed by reference — no character data is copied. var rebinds the reference, it does not mutate the characters.
Compiler Optimizations
- Interning: identical string literals share the same memory at compile time
- Small string optimization: short strings stored inline, no heap allocation
- Zero-copy passing: only the reference struct is copied (pointer + length), never the characters
Objects
Declared with the object keyword. Construction is positional, following declaration order.
object Player { var pos: Vec2 var hp: Int val speed: Float val name: String } val p = Player(vec2(100.0, 100.0), 100, 200.0, "Victor")
Inheritance — extends
Single inheritance only. Child objects inherit all fields and methods from the parent.
object Entity { var pos: Vec2 var hp: Int } object Player extends Entity { val name: String val speed: Float val sprite: rc Texture } -- Player has: pos, hp, name, speed, sprite -- override parent methods (no keyword needed) func Entity.take_damage(amount: Int) { self.hp -= amount } func Player.take_damage(amount: Int) { val reduced = amount - self.armor super.take_damage(reduced) }
Interfaces
Interfaces define a contract of methods. Missing an interface method is a compile error.
interface Drawable { func draw() } interface Updatable { func update(dt: Float) } object Player extends Entity implements Drawable, Updatable { val name: String val speed: Float } -- interfaces as types for polymorphism func draw_all(items: [Drawable]) { for item in items { item.draw() } } -- runtime type checking if a is Player and b is Enemy { a.take_damage(b.damage) }
Enums
Simple C-style enums. Always accessed with the type prefix.
enum GameState { PAUSED, RUNNING, MENU, } var state = GameState.RUNNING if state == GameState.PAUSED { draw_text("PAUSED", 300, 250, 40, WHITE) }
Generics
Full generics using [T] syntax. Works on functions, objects, and methods. Uses monomorphization.
-- generic function func find[T](items: [T], check: func(T) -> Bool) -> T? { for item in items { if check(item) { return item } } return null } -- generic object object Pool[T] { var items: [T] var count: Int } func Pool[T].get() -> T? { if self.count > 0 { self.count -= 1 return self.items[self.count] } return null } -- usage var bullet_pool = Pool[Bullet](heap [Bullet; 256], 0) val b = bullet_pool.get()
Functions & Methods
All declared with the func keyword.
Standalone Functions
func add(a: Float, b: Float) -> Float { return a + b }
Methods
Methods use func TypeName.method_name() syntax — not nested inside the object block. self is implicit and always mutable.
func Player.new(name: String) -> Player { return Player(vec2(100.0, 100.0), 100, 200.0, name) } func Player.is_alive() -> Bool { return self.hp > 0 }
Passing Convention
Positional arguments only. Primitives are always copied. Objects and Strings are passed by reference, immutable by default. Use var to allow mutation.
-- immutable reference (default) func print_stats(player: Player) { print("{player.name}: {player.hp}") player.hp = 0 -- COMPILE ERROR: immutable } -- mutable reference func heal(var player: Player, amount: Int) { player.hp += amount -- ok, modifies original }
Multiple Return Values
func divide(a: Float, b: Float) -> Float, Err? { if b == 0.0 { return 0.0, Err("division by zero") } return a / b, null } val result, val err = divide(10.0, 0.0) val result, _ = divide(10.0, 3.0) -- discard error
Lambdas
Anonymous functions use func() { } — same keyword, just without a name. All outer variables are captured by reference.
val double = func(x: Int) -> Int { x * 2 } val on_collision = func(entity: Entity, other: Entity) { entity.take_damage(10) spawn_particles(entity.pos) } -- inferred types from context val enemy = find[Enemy](enemies, func(e) { e.hp < 50 }) -- lambdas as types in objects object Button { val label: String val on_click: func() } val btn = Button("Start", func() { start_game() })
Control Flow
Brace-delimited blocks with familiar syntax.
if player.hp <= 0 { player.die() } elif player.hp < 20 { player.warn_low_hp() } else { player.update(dt) } for i in range(len(bullets)) { bullets[i].update(dt) } for c in name { print(c) } while not window_should_close() { val dt = get_frame_time() }
Pattern Matching
match state { GameState.RUNNING { player.update(dt) } GameState.PAUSED { draw_pause_menu() } GameState.MENU { draw_main_menu() } else { log.warn("unknown state") } }
The else branch is optional. The compiler warns if the match is not exhaustive.
Memory Management
Iron gives you explicit control over memory with compiler-assisted safety. No garbage collector, no borrow checker.
Stack — Default
No keyword needed. Values live on the stack and are freed when the scope ends. Stack values can never escape their block.
val pos = vec2(10.0, 20.0) -- stack, freed at scope exit -- COMPILE ERROR: stack value cannot escape func make_grid() -> [Vec2; 16] { var grid = [Vec2; 16] return grid -- ERROR } -- correct: use heap to return func make_grid() -> [Vec2; 16] { val grid = heap [Vec2; 16] return grid -- ok }
Heap & Auto-Free
Use heap to allocate on the heap. Values that don't escape their block are automatically freed.
val enemies = heap [Enemy; 64] val boss = heap Enemy(Vec2(0.0, 0.0), 1000) func process() { val data = heap [UInt8; 4096] parse(data) -- auto-freed here, data doesn't escape }
Escaped values produce a compiler warning. Silence with leak for intentional permanent allocations, or use free for explicit cleanup.
| Situation | Result |
|---|---|
heap without free, value doesn't escape | Auto-freed, no warning |
heap without free, value escapes | Warning: possible memory leak |
heap with leak | No warning, intentional |
free on a non-heap value | Compile error |
leak on a non-heap or rc value | Compile error |
Reference Counting — rc
For shared ownership. Automatically freed when the last reference dies.
val sprite = rc load_texture("hero.png") -- ref count = 1 var other = sprite -- ref count = 2 other = null -- ref count = 1 -- when last ref dies, texture is freed automatically
Defer
Runs at scope exit (LIFO order). Used for deterministic resource cleanup.
val window = init_window(800, 600, "Game") defer close_window(window) val file = open_file("save.dat") defer close_file(file)
No pointers. There are no pointer types in the language. The compiler handles all reference-to-pointer translation when generating C code.
Concurrency
Thread pools, spawn/await, typed channels, mutexes, and parallel loops are all first-class language features.
Thread Pools
val compute = pool("compute", 4) val io = pool("io", 1) val physics = pool("physics", 2) -- pin to specific CPU cores physics.pin(2, 3)
Spawn & Await
-- spawn on default pool spawn("autosave") { save_game(state) } -- spawn on a specific pool, get a handle val handle = spawn("asset-loader", io) { return load_texture("hero.png") } val tex = await handle -- block until done -- non-blocking check if handle.done() { val tex = handle.result() }
Channels
Typed, optionally buffered.
val ch = channel[String]() -- unbuffered val ch = channel[Texture](4) -- buffered (capacity 4) spawn("loader", io) { ch.send(load_texture("hero.png")) } val tex = ch.recv() -- blocking val tex = ch.try_recv() -- non-blocking (T?) ch.close()
Mutex
val score = mutex(0) spawn("scorer") { score.lock(func(var s) { s += 10 }) } score.lock(func(val s) { print("score: {s}") })
Parallel For
Add parallel to split a loop across cores. Each iteration must be independent. An implicit barrier ensures completion.
-- parallel on default pool for i in range(len(particles)) parallel { particles[i].update(dt) } -- parallel on a specific pool for i in range(len(particles)) parallel(compute) { particles[i].update(dt) } -- COMPILE ERROR: can't mutate outer var in parallel for var total = 0 for i in range(len(enemies)) parallel { total += enemies[i].hp -- ERROR }
Modules
One file = one module. No module declaration needed. The file path is the module name.
my_game/ main.iron -- entry point player.iron -- module: player enemy.iron -- module: enemy physics/ collision.iron -- module: physics.collision rigid_body.iron -- module: physics.rigid_body
-- flat import: everything available directly import player -- aliased import: prefixed access import physics.rigid_body as rb val body = rb.create_body(pos, mass)
Visibility
Everything is public by default. Use private to restrict to the current file.
private func Player.recalc() { self.hp = clamp(self.hp, 0, 100) }
Compile-Time Evaluation
Any pure function can be evaluated at compile time using comptime at the call site. The result is baked into the binary with zero runtime cost.
func build_sin_table() -> [Float; 360] { var table = [Float; 360] for i in range(360) { table[i] = sin(Float(i) * 3.14159 / 180.0) } return table } -- comptime at call site val SIN_TABLE = comptime build_sin_table() -- embed files at compile time val SHADER_SOURCE = comptime read_file("shaders/main.glsl") -- same function works at runtime too val dynamic_table = build_sin_table()
Restrictions
Can use: math, loops, conditionals, file reads (embed assets), string manipulation.
Cannot use: heap, free, rc, runtime APIs (raylib, OS, network).
Game-Dev Blocks
Managed blocks that auto-wrap begin/end pairs. The draw block automatically calls BeginDrawing()/EndDrawing().
draw { clear(DARKGRAY) player.draw() for i in range(bullet_count) { draw_circle(bullets[i].pos, 4.0, RED) } draw_text("{player.name}: {bullet_count} bullets", 10, 10, 20, WHITE) }
Standard Library
Built-in — No Import Needed
-- printing print("hello") println("hello") -- type conversions val f = Float(42) val i = Int(3.14) val s = String(100) -- common functions len(array) range(end) range(start, end) abs(x) min(a, b) max(a, b) clamp(val, min, max) assert(condition)
Collections
-- List: dynamic array var enemies = List[Enemy]() enemies.add(enemy) enemies.remove(0) val e = enemies.get(0) -- Map: key-value store var scores = Map[String, Int]() scores.set("victor", 100) val s = scores.get("victor") -- Int? -- Set: unique values var tags = Set[String]() tags.add("enemy") val has = tags.has("enemy")
math Module
import math math.PI math.TAU math.E math.sin(x) math.cos(x) math.tan(x) math.asin(x) math.acos(x) math.atan2(y, x) math.floor(x) math.ceil(x) math.round(x) math.sqrt(x) math.pow(b, e) math.lerp(a, b, t) math.random() -- 0.0..1.0 math.random_int(min, max) math.random_float(min, max)
io Module
import io val data, val err = io.read_file("save.dat") val err = io.write_file("save.dat", data) val exists = io.file_exists("save.dat") val files = io.list_files("assets/") io.create_dir("saves/") io.delete_file("temp.dat")
time Module
import time val now = time.now() -- seconds since start val ms = time.now_ms() -- milliseconds time.sleep(0.5) var timer = time.Timer(2.0) timer.update(dt) if timer.done() { spawn_wave() timer.reset() }
log Module
import log log.info("game started") log.warn("low memory") log.error("failed to load texture") log.debug("player pos: {player.pos}") log.set_level(log.WARN)
Comments
-- single line commentKeywords Summary
| Keyword | Purpose |
|---|---|
val | Immutable binding |
var | Mutable binding |
func | Function / method declaration |
object | Data structure declaration |
enum | Enumeration declaration |
interface | Interface declaration |
import | Module import |
if / elif / else | Conditional branching |
for / while | Loops |
match | Pattern matching on enums |
return | Return from function |
heap | Heap allocation |
free | Heap deallocation |
leak | Intentional permanent allocation |
defer | Execute at scope exit |
rc | Reference-counted wrapper |
private | File-scoped visibility |
extends | Single inheritance |
implements | Interface implementation |
super | Call parent method |
is | Runtime type check |
spawn | Launch a thread |
await | Wait for thread result |
parallel | Parallel for loop modifier |
pool | Create a thread pool |
comptime | Compile-time evaluation |
self | Implicit method receiver |
not / and / or | Logical operators |
true / false / null | Literal values |
Full Example
A complete game with player movement, bullets, and rendering.
import raylib import math as m object Player { var pos: Vec2 var hp: Int val speed: Float val name: String val sprite: rc Texture } func Player.new(name: String) -> Player { return Player(vec2(100.0, 100.0), 100, 200.0, name, rc load_texture("hero.png")) } func Player.update(dt: Float) { if is_key_down(.RIGHT) { self.pos.x += self.speed * dt } if is_key_down(.LEFT) { self.pos.x -= self.speed * dt } if is_key_down(.UP) { self.pos.y -= self.speed * dt } if is_key_down(.DOWN) { self.pos.y += self.speed * dt } } func Player.draw() { draw_texture(self.sprite, self.pos) } func Player.is_alive() -> Bool { return self.hp > 0 } func main() { val window = init_window(800, 600, "My Game") defer close_window(window) var player = Player.new("Victor") val bullets = heap [Bullet; 256] var bullet_count = 0 while not window_should_close() { val dt = get_frame_time() player.update(dt) if is_key_pressed(.SPACE) { bullets[bullet_count] = player.shoot() bullet_count += 1 } for i in range(bullet_count) { bullets[i].update(dt) } draw { clear(DARKGRAY) player.draw() for i in range(bullet_count) { if bullets[i].alive { draw_circle(bullets[i].pos, 4.0, RED) } } draw_text("{player.name}: {bullet_count} bullets", 10, 10, 20, WHITE) } } }