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
endGet going
Install the Chasm compiler and set up your editor.
First steps
Write and run your first Chasm program in minutes.
Source
Browse the compiler source, report issues, and contribute.
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.
| Property | Chasm |
|---|---|
| Type system | Static, inferred |
| Memory model | Arena-backed, no GC |
| Output | C99, links into any engine |
| Hot reload | First-class, lifetime-safe |
| Error style | Rust-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 }
endThe 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 foreverThe 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.