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.

Philosophy

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

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

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.

SituationResult
heap without free, value doesn't escapeAuto-freed, no warning
heap without free, value escapesWarning: possible memory leak
heap with leakNo warning, intentional
free on a non-heap valueCompile error
leak on a non-heap or rc valueCompile 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 comment

Keywords Summary

KeywordPurpose
valImmutable binding
varMutable binding
funcFunction / method declaration
objectData structure declaration
enumEnumeration declaration
interfaceInterface declaration
importModule import
if / elif / elseConditional branching
for / whileLoops
matchPattern matching on enums
returnReturn from function
heapHeap allocation
freeHeap deallocation
leakIntentional permanent allocation
deferExecute at scope exit
rcReference-counted wrapper
privateFile-scoped visibility
extendsSingle inheritance
implementsInterface implementation
superCall parent method
isRuntime type check
spawnLaunch a thread
awaitWait for thread result
parallelParallel for loop modifier
poolCreate a thread pool
comptimeCompile-time evaluation
selfImplicit method receiver
not / and / orLogical operators
true / false / nullLiteral 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)
    }
  }
}