Introduction

Chasm is a statically-typed, compiled scripting language designed for games. Fast, safe, and embeddable in any C engine.

Chasm is a small, statically-typed language that compiles to C99. It is designed for game scripting, fast enough to use in tight loops, safe enough to hot-reload without crashing, and simple enough to embed in any C-based engine.

@score :: script = 0

def on_tick(dt :: float) do
  if @score > 100 do
    @score = 0
  end
end

defp add_points(n :: int) do
  @score = @score + n
end

Why Chasm?

Most scripting languages used in games are dynamic, they trade type safety and predictable performance for flexibility. Chasm goes the other way: strict types, arena-based memory, and a lifetime system that prevents whole classes of bugs at compile time.

PropertyChasm
Type systemStatic, inferred
Memory modelArena-backed, no GC
OutputC99, links into any engine
Hot reloadFirst-class, lifetime-safe
Error styleRust-inspired diagnostics with source locations

Key Features

Lifetime-safe attributes. Module-level state uses a three-tier lifetime system (frame, script, persistent). The compiler enforces that short-lived values cannot leak into long-lived storage, no use-after-free, no stale pointers.

Arena allocation. Arrays backed by arenas are reset at the start of each frame with zero per-element cost. No GC pauses mid-frame.

Compiles to C99. Chasm programs become a single .c file that you compile with any C99 compiler. The output is easy to inspect, link, and profile.

Hot reload. Edit a script and reload without restarting the engine. script state survives; frame state resets automatically.

Embeds anywhere. The runtime is a single header (chasm_rt.h). Engines written in C, C++, Zig, or Rust can host Chasm scripts.

A Taste of the Language

# Module attribute, lives for the life of the script
@high_score :: persistent = 0

defstruct Player do
  x     :: float = 0.0
  y     :: float = 0.0
  speed :: float = 200.0
end

@player :: script = Player{}

def on_tick(dt :: float) do
  dx = 0.0
  if key_down(:right) do dx = @player.speed end
  if key_down(:left)  do dx = -@player.speed end
  @player = @player with { x: @player.x + dx * dt }
end

The Three-Lifetime Model

Every piece of mutable state in Chasm lives in one of three arenas. An arena is a block of memory with a pointer, allocating means advancing the pointer, freeing means resetting it. There is no garbage collector and no malloc/free.

frame  →  script  →  persistent
  ↑          ↑            ↑
shortest               longest
(one tick)          (never resets)

frame, Resets at the start of every tick. Use it for temporary scratch data that only needs to exist for one frame: candidate lists, path buffers, per-frame calculations. Allocation is essentially free.

script, Persists for the life of the script, but resets on hot-reload. This is where your game state lives: entity arrays, score, player position, anything that needs to survive across ticks but can be re-initialized when you reload.

persistent, Never resets, not even on hot-reload. Use this for assets: loaded textures, fonts, sounds. Loading them is slow; you only want to do it once.

@particles  :: frame      = array_new(Particle, 64)  # gone next tick
@enemies    :: script     = array_fixed(32, Enemy{}) # resets on reload
@player_tex :: persistent = 0                        # lives forever

The compiler enforces that a short-lived value cannot be stored in a longer-lived attribute. If you try to store a frame value in a script attribute, you get E008. To move a value up the chain, use a promotion function:

# Wrong, frame value into script attr
@name :: script = some_frame_string   # E008

# Right, copy into the script arena first
@name :: script = script_copy(some_frame_string)

This rule prevents an entire class of bugs, use-after-free, stale pointers, crashed-on-next-tick, at compile time, with no runtime cost.

Continue to Installation to get the compiler set up.