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

TypeDescriptionExample
IntWhole numbers42
FloatDecimal values3.14
BoolTrue or falsetrue
StringText"hello"
CharSingle character'A'
U8, U16, U32Unsigned integers255
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:

SectionTopics
1-2Variables, types, tuples, type aliases
3Functions, named arguments, .., out parameters
4Control flow, guards, &> blocks
5Structs, embedded fields, impl, implicit receiver
6Enums, pattern matching, guards
7Optional types, when
8-9Collections, transformers
10Borrowing, with, only
11Modules and imports
12Documentation comments
13Resource IDs, constants, anchor, bits

Next Steps