Tutorial
Step-by-step guide to learning the Swamp programming language
This tutorial teaches Swamp from the ground up. Each section builds on the previous one.
1. Introduction
Swamp is a systems programming language designed for games and real-time servers.
Key principles:
- No garbage collection — all memory is reserved at compile time
- Deterministic — same input always produces same output
- Trivially copyable state — game state can be snapshotted with
memcpy - Fast iteration — hot reloading without losing state
// Your first Swamp program
print("Hello, Swamp!")
2. Variables & Basic Types
Defining Variables
Use := to define a variable. The type is inferred automatically.
player_name := "Hero"
health := 100
speed := 5.5
is_alive := true
Immutability by Default
Variables are immutable by default. Use mut to allow reassignment.
score := 0 // immutable - cannot change
mut health := 100 // mutable - can reassign
health = 90 // OK
Basic Types
| Type | Description | Example |
|---|---|---|
Int | Whole numbers | 42 |
Float | Decimal values | 3.14 |
Bool | True or false | true |
String | Text | "hello" |
Char | Single character | 'A' |
U8, U16, U32 | Unsigned integers | 255 |
level := 5 // Int
gravity := 9.8 // Float
game_over := false // Bool
title := "Quest" // String
letter := 'A' // Char
byte_value: U8 = 255 // Unsigned 8-bit
String Interpolation
Use single quotes with {} to embed values.
name := "Hero"
score := 100
message := 'Player {name} scored {score} points!'
String Slicing
Access substrings with ranges.
text := "Hello"
text[1..3] // "el" (exclusive)
text[1..=3] // "ell" (inclusive)
mut greeting := "Hello"
greeting[0..2] = "Ye" // "Yello"
Idiom: Avoid strings in game logic. Strings are slow (comparisons, memory) and lose type information. Use enums, IDs, or numbers instead. Strings belong only at the edges: logging, debugging, and displaying text to players.
// Avoid
if action == "DRINK_POTION" { ... }
// Prefer
enum Action { Idle, DrinkPotion, Attack }
if action == Action::DrinkPotion { ... }
Tuples
Group values without defining a struct.
position := (10, 20)
fn get_bounds() -> (Int, Int) {
(800, 600)
}
width, height := get_bounds()
3. Functions
Defining Functions
fn greet(name: String) {
print('Hello, {name}!')
}
Returning Values
The last expression is the return value. There is no return keyword.
fn add(a: Int, b: Int) -> Int {
a + b
}
fn max(a: Int, b: Int) -> Int {
if a > b { a } else { b }
}
Mutable Parameters
Mark parameters with mut to modify them.
fn apply_damage(mut player: Player, damage: Int) {
player.health -= damage
}
Named Arguments
Call functions with arguments in any order by using names.
fn spawn_enemy(health: Int, damage: Int, speed: Float) {
// ...
}
// Positional (must match order)
spawn_enemy(100, 25, 3.5)
// Named (any order)
spawn_enemy(damage: 25, speed: 3.5, health: 100)
// Mix positional and named
spawn_enemy(100, speed: 3.5, damage: 25)
Idiom: Keep functions to four parameters or fewer. If you need more, group related values into a struct. This improves readability and avoids argument-order mistakes.
// Avoid: too many parameters
fn draw_sprite(x: Int, y: Int, rotation: Float, scale: Float, opacity: Float, layer: Int) { ... }
// Prefer: group into a struct
struct DrawParams {
x: Int
y: Int
rotation: Float
scale: Float
opacity: Float
layer: Int
}
fn draw_sprite(params: DrawParams) { ... }
Omitting Arguments with ..
Use .. to fill remaining arguments with default values.
fn create_entity(health: Int, damage: Int, speed: Float, armor: Int) {
// ...
}
// Only specify what you need
create_entity(health: 50, ..) // damage: 0, speed: 0.0, armor: 0
create_entity(100, 25, ..) // speed: 0.0, armor: 0
create_entity(damage: 10, armor: 5, ..) // health: 0, speed: 0.0
Out Parameters
When a function returns an aggregate type (struct, array, etc.), the compiler actually passes the destination as a hidden first parameter. This is called “structured return” (sret).
// This function appears to return a Player...
fn create_player(name: String) -> Player {
Player { name: name, health: 100, position: { x: 0.0, y: 0.0 } }
}
// ...but internally works like:
// fn create_player(out: Player, name: String) { ... }
The out keyword makes this explicit, giving you access to the destination memory:
fn create_player(out: Player, name: String) {
out.name = name
out.health = 100
out.position = Position { x: 0.0, y: 0.0 }
}
// Called like a function that returns Player
player := create_player("Hero")
This is useful when you need to initialize fields individually or when working with the destination directly is more natural than constructing a complete value to return.
4. Control Flow
If Expressions
Every if is an expression that yields a value.
damage := if is_critical {
base_damage * 2
} else {
base_damage
}
Idiom: Prefer single assignment over mutation. Use if/match as expressions to assign once rather than declaring mut and reassigning.
// Prefer: single assignment
direction := if input == Left { -1 } else { 1 }
// Avoid: mutation
mut direction := 0
if input == Left {
direction = -1
} else {
direction = 1
}
While Loops
mut countdown := 3
while countdown > 0 {
print('T minus {countdown}')
countdown -= 1
}
For Loops
// Iterate over a collection
for enemy in enemies {
enemy.update()
}
// With mutation
for mut enemy in enemies {
enemy.health -= 10
}
// Exclusive range: 0, 1, 2
for i in 0..3 {
print(i)
}
// Inclusive range: 0, 1, 2, 3
for i in 0..=3 {
print(i)
}
// Key-value iteration (maps)
for id, mut enemy in enemies_map {
print('Updating enemy {id}')
enemy.update()
}
Break and Continue
for item in inventory {
if item == Item::Key {
break // exit loop
}
}
for enemy in enemies {
if !enemy.is_active {
continue // skip to next iteration
}
enemy.update()
}
Idiom: Place continue statements early in the loop body to filter out invalid cases upfront. This keeps the main logic unindented and easier to read.
// Prefer: filter early
for enemy in enemies {
if !enemy.is_active { continue }
if enemy.health <= 0 { continue }
// Main logic stays flat
enemy.update()
enemy.check_collisions()
}
// Avoid: deeply nested
for enemy in enemies {
if enemy.is_active {
if enemy.health > 0 {
enemy.update()
enemy.check_collisions()
}
}
}
Guard Expressions
Evaluate conditions in sequence, return first match.
reward :=
| score >= 1000 -> "Legendary Chest"
| score >= 500 -> "Gold Chest"
| score >= 100 -> "Silver Chest"
| _ -> "Bronze Chest"
The _ wildcard handles all remaining cases.
Idiom: Prefer guards over if-else chains for value selection. Guards read top-to-bottom, stay flat, and make the default case explicit. Also order branches by frequency: put the common (hot) case first, rare cases later, and the default last.
// Prefer: guards
state :=
| health > 50 -> Normal // most common
| health > 10 -> Wounded
| health > 0 -> Critical
| _ -> Dead // default last
// Avoid: nested if-else
state := if health > 50 {
Normal
} else if health > 10 {
Wounded
} else if health > 0 {
Critical
} else {
Dead
}
AND Block Expression
Chain boolean checks with short-circuit evaluation using &>.
can_attack := &> {
player.is_alive()
player.has_weapon()
player.stamina > 10
target.in_range(player)
}
// Equivalent to:
can_attack := player.is_alive() && player.has_weapon() && player.stamina > 10 && target.in_range(player)
Evaluation stops at the first false.
5. Structs
Defining Structs
struct Player {
name: String
health: Int
position: Position
}
struct Position {
x: Float
y: Float
}
Creating Instances
player := Player {
name: "Hero"
health: 100
position: Position { x: 0.0, y: 0.0 }
}
Accessing Fields
current_health := player.health
player_x := player.position.x
Modifying Fields
mut player := Player { name: "Hero", health: 100, position: { x: 0.0, y: 0.0 } }
player.health -= 10
player.position.x = 5.0
Default Values with ..
Use .. to fill unspecified fields with defaults.
enemy := Enemy {
damage: 50
.. // health: 0, name: "", etc.
}
Idiom: Fill only what matters, zero the rest. The .. operator sets unspecified fields to zero (or their default() if defined). This works well with the “Zero Is Initialization” principle: design your types so that zero values are always safe and represent “nothing” or “inactive”.
// Zero is safe: id=0 means "no player"
struct Player {
id: Int // 0 = no player
health: Int
score: Int
}
player := Player { id: 42, .. } // health=0, score=0
if player.id != 0 {
// player exists
}
Embedded Fields
Use embed to pass the outer struct where the embedded type is expected.
struct Position {
x: Float
y: Float
}
struct Entity {
embed position: Position
health: Int
}
fn print_position(pos: Position) {
print('({pos.x}, {pos.y})')
}
entity := Entity { position: { x: 10.0, y: 20.0 }, health: 100 }
// Entity can be passed where Position is expected
print_position(entity) // automatically uses entity.position
Member Functions
Use impl to attach functions to a struct.
impl Player {
fn take_damage(mut self, amount: Int) {
self.health -= amount
if self.health < 0 {
self.health = 0
}
}
fn is_alive(self) -> Bool {
self.health > 0
}
}
// Usage
player.take_damage(25)
if player.is_alive() {
print("Still standing!")
}
Implicit Receiver
Inside member functions, use . as shorthand for self..
impl Player {
fn reset(mut self) {
.health = 100 // same as: self.health = 100
.position.x = 0.0 // same as: self.position.x = 0.0
.position.y = 0.0
}
}
Associated Functions
Functions without self are called with ::.
impl Player {
fn new(name: String) -> Player {
Player {
name: name
health: 100
position: Position { x: 0.0, y: 0.0 }
}
}
}
player := Player::new("Hero")
Idiom: Keep behavior with the type it belongs to. Use impl blocks instead of free functions when the function conceptually belongs to a type. This improves discoverability (code completion shows all methods) and keeps documentation in one place.
6. Enums & Pattern Matching
Defining Enums
enum GameState {
MainMenu
Playing
Paused
GameOver
}
Enums with Data
enum Item {
Gold // no data
Potion Int // single value (healing amount)
Weapon(Int, Float) // tuple (damage, range)
Armor { // struct variant
defense: Int
weight: Float
}
}
Match Expressions
match game_state {
MainMenu -> show_menu(),
Playing -> update_game(),
Paused -> show_pause_screen(),
GameOver -> show_results(),
}
Destructuring in Match
match item {
Gold -> {
player.money += 100
},
Potion amount -> {
player.health += amount
},
Weapon damage, range -> {
player.equip_weapon(damage, range)
},
Armor { defense, weight } -> {
if player.can_carry(weight) {
player.defense += defense
}
},
}
Pattern Guards with &&
Add conditions to patterns.
match attack {
Melee damage && has_power_up -> apply_damage(damage * 2),
Melee damage -> apply_damage(damage),
Ranged damage, range && range > distance -> apply_damage(damage),
_ -> (),
}
Literal Patterns
Match against specific values.
match player.health {
100 -> show_status("Full Health"),
1..10 -> show_status("Critical!"),
_ -> show_health_bar(),
}
match position {
0, 0 -> spawn_at_origin(),
x, 0 -> spawn_at_ground(x),
0, y -> spawn_at_wall(y),
x, y -> spawn_at(x, y),
}
7. Optional Types
Declaring Optionals
Add ? after a type to make it optional.
target: Entity? = none
equipped_weapon: Weapon? = none
Default Value with ??
health := saved_health ?? 100 // use 100 if none
spawn := checkpoint ?? Position { x: 0.0, y: 0.0 }
The when Keyword
when target {
// target is unwrapped and available here
target.take_damage(10)
}
when weapon = find_weapon() {
player.equip(weapon)
} else {
print("No weapon found")
}
8. Collections
Swamp collections have fixed capacity known at compile time. No runtime allocation.
Vec (Dynamic Array)
// Declare with capacity
enemies: Vec<Enemy; 64> = []
// Initialize with values
scores := [100, 95, 90, 85, 80]
// Access by index
first := scores[0]
// Add items
mut items: Vec<Item; 32> = []
items.add(Item::Gold)
items.add(Item::Potion(50))
// Iterate
for score in scores {
print(score)
}
// Iterate with mutation
for mut enemy in enemies {
enemy.update()
}
Map (Key-Value)
// Declare with capacity
spawn_points: Map<Level, Position; 16> = [:]
// Initialize with values
settings := [
Setting::Volume: 80
Setting::Brightness: 100
]
// Access
volume := settings[Setting::Volume]
// Assign
mut config: Map<String, Int; 8> = [:]
config["max_players"] = 4
Fixed-Size Arrays
// Exactly 4 elements, always
directions: [Direction; 4] = [North, East, South, West]
9. Advanced Collections
Stack (LIFO)
mut undo_stack: Stack<Action; 32> = []
undo_stack.push(action)
last_action := undo_stack.pop()
Queue (FIFO)
mut event_queue: Queue<Event; 64> = []
event_queue.enqueue(event)
next_event := event_queue.dequeue()
Transformers
Use lambdas with collection methods.
// Filter
alive_enemies := enemies.filter(|e| e.health > 0)
// Find
boss := enemies.find(|e| e.is_boss)
// Map
names := players.map(|p| p.name)
// Any / All
has_key := inventory.any(|item| item == Item::Key)
all_ready := players.all(|p| p.ready)
10. Borrowing & Scoped Bindings
Borrow Binding
Create an alias to an existing place with = and &.
a = &game.player.position
print('x: {a.x}, y: {a.y}')
mut b = &game.player.stats
b.score += 100
The alias is valid only within its scope.
The with Keyword
Create a temporary binding to avoid repetition.
// Instead of game.players[0].position.x repeatedly:
with player = &game.players[0] {
print('x: {player.position.x}')
print('y: {player.position.y}')
}
// Mutable binding
with mut player = &game.players[0] {
player.health -= 10
player.position.x += 1.0
}
The only Keyword
Restrict scope to specific variables.
only player, delta_time {
player.position.x += player.velocity.x * delta_time
player.position.y += player.velocity.y * delta_time
}
11. Modules & Imports
File Structure
my_game/
├── main.swamp
├── player.swamp
├── enemies/
│ ├── goblin.swamp
│ └── dragon.swamp
└── utils/
└── math.swamp
Importing Modules
// Import entire module
mod player
// Import from nested path
mod enemies::goblin
// Import specific items
mod utils::math::{ clamp, lerp }
External Packages
use some_package
use another_package::specific_module
use third_package::module::{ TypeA, TypeB }
12. Documentation & Comments
Line Comments
// Regular comment for implementation notes
player.health -= damage // inline comment
Documentation Comments
Use /// to document the next declaration.
/// Represents a player in the game world.
///
/// Players have health, position, and an inventory.
struct Player {
/// Current health points (0-100)
health: Int
/// World position
position: Position
}
/// Apply damage to an entity and handle death.
fn apply_damage(mut entity: Entity, amount: Int) {
entity.health -= amount
}
Module Documentation
Use //! at the top of a file to document the module.
//! # Combat System
//!
//! Handles all combat-related logic including damage calculation,
//! status effects, and death handling.
13. Game-Specific Features
Resource IDs
Reference assets with compile-time checking.
player_sprite: Res<Image> = @gfx/player.png
explosion_sound: Res<Audio> = @audio/explosion.wav
// The compiler verifies these files exist
Constants
const MAX_PLAYERS = 4
const GRAVITY = 9.8
const GAME_TITLE = "My Awesome Game"
Anchor
Create named allocations for the game engine to access.
anchor game_state = GameState::new()
anchor renderer = RenderState { .. }
The bits Type
Pack multiple small values into a single integer.
bits EntityFlags {
is_active: 1
team_id: 3 // 0-7
is_visible: 1
layer: 4 // 0-15
}
mut flags := EntityFlags { is_active: 1, team_id: 2, .. }
flags.is_visible = 1
Summary
You’ve learned the core features of Swamp:
| Section | Topics |
|---|---|
| 1-2 | Variables, types, tuples, type aliases |
| 3 | Functions, named arguments, .., out parameters |
| 4 | Control flow, guards, &> blocks |
| 5 | Structs, embedded fields, impl, implicit receiver |
| 6 | Enums, pattern matching, guards |
| 7 | Optional types, when |
| 8-9 | Collections, transformers |
| 10 | Borrowing, with, only |
| 11 | Modules and imports |
| 12 | Documentation comments |
| 13 | Resource IDs, constants, anchor, bits |
Next Steps
- Read the Language Specification for complete details
- Explore the Core Reference for built-in functions
- Read the Idioms for best practices and “The Swamp Way” of writing code