Language Specification

Detailed specification of the Swamp programming language

Comments

Comments help you explain your code. Swamp has three types: regular line comments (//) for quick notes, block comments (/* */) for longer explanations, and documentation comments (///) that automatically generate readable documentation for your code. Documentation comments are special because they help others understand how to use your code and appear in tooltips in your editor.

// Regular line comments - for implementation notes
player_health := 100      // Start health value

/* Block comments - for longer explanations
   that span multiple lines and can contain
   lists, examples, etc. **Markdown** will be supported in the future. */

/// Documentation comments - generate external documentation
/// These comments should explain the purpose and usage
/// of the following item (struct, function, field, etc.)
struct Player {
    /// The player's current position in the world
    position: Point,

    /// Current health points (0-100)
    /// When this reaches 0, the game is over
    health: Int,

    /// Movement speed in units per second
    /// Affected by equipment and status effects
    speed: Float,
}

Variables

Variable Definition

Defining a variable in Swamp is simple:

player_name := "Hero"

You don’t need to declare type, it is implied. Use snake_case1 for variable names.

When you create a variable, it’s immutable by default. This means once you set its value, it can’t be changed. If you need to change it later, mark it as mutable with mut as you declare it.

player_name := "Hero"    // Immutable - cannot be changed
mut health := 100        // Mutable - can be reassigned
health := 101

Variable Reassignment

Only mutable variables can be given new values. This helps prevent accidental changes and makes your code easier to understand.

// Mutable variables can be reassigned
mut score := 0
score = score + 100
score = score * 2

Variable Scope and Lifetime

Variables only exist within their scope (the block of code where they’re defined). There are no global variables, so any variables that you need to access within a function must be given to it as a parameter. However, a variable can be accessed inside a nested scope (such as within an if expression or for loop). When the scope ends, the variable is automatically cleaned up.

{
    power_up := spawn_powerup()    // Power-up exists in this scope
    if player.collides_with(power_up) {
        mut bonus := 100
        bonus *= 2  // Local multiplication
        player.score += bonus
        {
            {
                mut new bonus cool thing := power_up
            }
        }
    }
    // bonus is not accessible here
}   // power_up is automatically cleaned up here

Mutability and Reference Rules

Swamp uses a unique approach to managing mutability that prioritizes safety and clarity:

  • Everything is immutable (read-only) by default. This immutability-first approach prevents accidental modifications and makes your code easier to reason about.

  • Mutability (write access) is explicitly declared with mut. When you need a variable that can change, you must declare this intention clearly.

  • References are requested with &. The & operator is used when you need to temporarily give a reference to a function or a specific scope. This is called “borrowing”. The most common case is when you want to pass something to a function parameter that is mut.

  • References can only be used in specific contexts and cannot be stored. This prevents memory safety issues.

Swamp encourages an “immutability-first” approach to software design. We recommend keeping variables immutable by default and only introducing mutability when needed.

Variable Type Annotation

Swamp supports type inference by automatically deducing a variable’s type from its assigned value. However, there are times when the variable’s type is not obvious — especially when initializing a variable with a optional type value like none, an empty vector [], or an empty map [:]. In those cases, you can explicitly specify the variable’s type using type annotation.

a : Int? = none

numbers : [Int] = []

settings : [Int:Float] = [:]

Functions

Function Definition

fn my_function(parameter: Type) {
}

Functions are declared with fn followed by the function name (in snake_case1) and parameters in parentheses. Each parameter also needs a declared Type (upper CamelCase2).

Functions that Return Values

fn add(a: Int, b: Int) -> Int {
    a+b
}

If the function will return a value, the parameters are followed by a ->and a Type declaration for the return value. By default, the function will return the last expression of its definition.

Parameter Mutability

Functions can choose whether they want to modify their parameters by using mut.This helps make it clear which functions will change the values passed to them and which will just read them.

// Mutable parameter example
fn apply_damage(mut target: Entity, damage: Int) {
    target.health -= damage
    if target.health <= 0 {
        target.state = EntityState::Downed
    }
}

// Immutable parameter example
fn calculate_squared_distance(player: Point, target: Point) -> Float {
    dx := target.x - player.x
    dy := target.y - player.y
    (dx * dx + dy * dy)
}

Types of Functions

Swamp has three types of functions that help you organize your code:

Member Functions

These operate on an instance of a type, accessed using a dot notation. They can modify the instance if marked with mut. 3

impl Player {
    /// Reduces player health and handles incapacitation
    fn take_damage(mut self, amount: Int) {
        self.health -= amount
        if self.health <= 0 {
            self.state = State::Incapacitated
        }
    }

    /// Calculates distance to target
    fn squared_distance_to(self, target: Point) -> Float {
        dx := target.x - self.position.x
        dy := target.y - self.position.y
        dx * dx + dy * dy
    }
}

// Usage:
player.take_damage(10)
distance := player.distance_to(enemy.position)

Associated Function Calls

These belong to the type itself, not instances. They’re called using double colon notation (::) and are often used for instantiation or utility functions.

impl Weapon {
    // Factory method
    fn create_sword() -> Weapon {
        Weapon {
            damage: 10,
            range: 2.0,
            weapon_type: WeaponType::Sword
        }
    }
}

sword := Weapon::create_sword()

Standalone Functions

In Swamp, standalone functions (functions not associated with any type) are rarely used because it’s usually better to organize functions as part of a relevant type. However, they can be useful for certain utility operations or when interfacing with system-level features. Or you want to have a short name to call it with.

/// Logs a debug message to the console
fn log(message: String) {
    // ... write to console/file
}

Named function arguments

fn my_function(health: Int, damage: Int, modifier: Int) {...}

// don't need to remember the order
my_function(modifier: 10, damage: 5, health: 10)

// if not using the field names, need to be correct order
my_function(10, 5, 10)

Suggested by @catnipped

Basic Types

Swamp provides fundamental types for storing different kinds of data: Integers (Int) for whole numbers, Floating-point numbers (Float) (technically Fixed Point numbers) for decimal values, Booleans (Bool) for true/false conditions, and Strings (String) for text, and Codepoint (Char) for characters. There is also an U8 (8-bit unsigned), but is rarely used in “logic” code.

Integers

health := 100

Integer Operations

  • Add +
  • Subtract -
  • Multiply *
  • Divide / (only for constant divisors, use .div() for non-constants — a lot slower)
  • Modulo % (only for constant divisors, use .mod() for non-constants — a lot slower)

Integer Comparisons

  • Equal ==
  • Not Equal !=
  • Less Than <
  • Less or Equal to <=
  • More Than >
  • More or Equal to >=

Floats

Floats are always written with at least one decimal, to keep them apart from Ints.

speed := 5.5

Float Operations

  • Add +
  • Subtract -
  • Multiply *
  • Divide / (only for constant divisors, use .div() for non-constants — a lot slower)
  • Modulo % (only for constant divisors, use .mod() for non-constants — a lot slower)

Float Comparisons

  • Equal ==
  • Not Equal !=
  • Less Than <
  • Less or Equal to <=
  • More Than >
  • More or Equal to >=

Booleans

is_jumping := true

A Boolean can only have two different values, true or false.

Boolean Operations

  • And &&
  • Or ||
  • Not !

Strings

player_name := "Hero"

Strings are written in quotation marks "". If you need to use quotation marks within the string, you can use backslashes like this \".

dialog := "Guard: \"Stop right there!\""

String Access

player_name := "Hero"
player_name[1..3] // returns "er"
player_name[1..=3] // returns "ero"

String Assignment

mut player_name := "Hero"
player_name[0..2] = "Ze"
player_name[0..=1] = "Ze"

Escape Sequences

  • \n - newline
  • \t - tab
  • \\ - the character \
  • \' - the character '
  • \" - the character "
  • \xHH - inserts an octet in string. (e.g. '\xF0\x9F\x90\x8A' = 🐊)
  • \u(HHH) - inserts unicode character as utf8. (e.g. '\u(1F40A)' = 🐊)

String Operations & Member Functions

  • .len() Returns length (in characters).

String Interpolation

String interpolation lets you embed values and expressions directly in your text using curly brackets {} in a single quotation mark declaration.

// Basic interpolation
name := "Hero"
message := 'Welcome, {name}!'

Anything within the curly brackets will be handled like regular code: you can include simple variables, complex expressions, and even format them with special modifiers for precise control over how they appear.

// Expression interpolation
status := 'HP: {health * 100 / max_health}'

Composite Types

These are more complex types that let you group data together in different ways.

Structs

Structs let you create your own data types by grouping related values together.

Struct Definition

To define a struct, simply write struct followed by the name of your Struct in upper CamelCase (as it will become a Type) and some curly brackets {}. Inside the brackets, you list each field the Struct will contain (and their Types).

struct Player {
    position: Point,
    health: Int,
    mana: Int,
    speed: Float,
}

Struct Instantiation

Once a Struct is defined, you can create a an instance of it. When you do, you have to set each field of the Struct to a value.

player := Player {
    position: Point { x: 0.0, y: 0.0 },
    health: 100,
    mana: 50,
    speed: 5.0,
}
Partial Initialization with Defaults (..)

When using .. for partial initialization, Swamp follows a structured process to ensure all fields are correctly filled:

  1. Check for a Default Trait Implementation on the Struct:

    • If the struct type being instantiated has an impl Default block, Swamp:
      1. Initializes the struct using the values returned by default() function.
      2. Overwrites any fields that are explicitly set during instantiation.
    struct Player {
        name: String,
        health: Int,
        mana: Int,
        speed: Float
    }
    
    impl Default for Player {
        fn default() -> Player {
            Player {
                name: "Unknown",
                health: 100,
                mana: 50,
                speed: 5.0
            }
        }
    }
    
    player := Player {
        name: "Hero",
        mana: 75,
        ..
    }
    
    // Result: Player { name: "Hero", health: 100, mana: 75, speed: 5.0 }
    
  2. If no Default implementation is found for the struct type:

    • Swamp iterates through each field that is not explicitly set during instantiation and fills them individually by:
      • Calling Default::default() on the field type.
      • Using built-in defaults for primitive types:
        • Int0
        • Float0.0
        • Boolfalse
        • T?none
        • String"" (empty string)

    Example (No Default trait implementation):

    struct Enemy {
        health: Int,
        damage: Int,
        name: String,
        speed: Float
    }
    
    enemy := Enemy {
        damage: 200,
        ..
    }
    // Result: Enemy { health: 0, damage: 200, name: "", speed: 0.0 }
    

Struct Field Access

You can access fields like variables, using a period (struct.field).

// Read field values
current_health := player.health
can_cast := player.mana >= 20

Struct Field Assignment

Using mut, you can assign new values to fields, just like variables.

mut player := Player {
    position: Point { x: 0.0, y: 0.0 },
    health: 100,
    mana: 50,
    speed: 5.0
}

// Update fields
player.health -= 10        // Take damage
player.mana -= 20          // Use mana
player.position = Point { x: 10.0, y: 5.0 }  // Move player

Struct Implementation

Using impl you can attach member functions to struct and enum types.

impl Player {
    /// Handle taking damage and effects
    fn take_damage(mut self, amount: Int) {
        self.health -= amount
        if self.health <= 0 {
            self.health = 0
            self.state = State::Incapacitated
        }
    }
}

impl can also be used to attach functions used for associated function calls (no self).

impl Player {
    /// Create a new player with default values
    fn new() -> Player {
        Player {
            position: Point { x: 0.0, y: 0.0 },
            health: 100,
            mana: 50,
            speed: 5.0
        }
    }
}

Tuples

Tuples are similar to structs, but they are not constructed as you use them, and do not have Type names or field names. To use a Tuple, write one or more values inside regular parentheses ().

player_position := (2,1)
fn get_position() -> (Int, Int) {
    (10, 20)
}

x, y := get_position()

Enums

Enums let you define a Type that can be one of several variants. Each variant can optionally carry different types of data. They’re great for representing things that can be in different states or categories.

Enum Definition

To define an Enum, write enum followed by its name (in uppercase CamelCase as it will become a Type) and a pair of curly brackets {}. Inside the brackets, you list each variant the Enum can be.

enum Item {
    // Simple variants (no data)
    Gold,
    Key,

    // Tuple variants with data
    Weapon(Int, Float),    // damage, range
    Potion Int,           // Single data. healing amount

    // Struct variants with named fields
    Armor {
        defense: Int,
        weight: Float,
        durability: Int
    },
}

Enum Instantiation

item := Item::Armor { defense: 3, weight: 3.8, durability: 99 }

Enum Pattern Matching

You can pattern match an Enum and output different outcomes depending on what Type an Enum.

match item {
    // Simple variant matching
    Gold -> {
        player.money += 100
    },

    Key -> open_nearest_door(),

    // Tuple variant destructuring
    Weapon _, range -> {           // Ignore damage
        set_attack_range(range)
    },

    Potion amount -> {
        player.health += amount
    },

    // Struct variant destructuring
    // The `{` and `}` are there to show that these are field names
    // and must match exactly
    Armor { defense, weight } -> {
        if player.strength >= weight {
            equip_armor(defense)
        } else {
            show_message("Too heavy!")
        }
    }
}

Optional Types

Optional types handle values that might or might not exist. They are represented by adding ? after any type. When an Optional has no value, it contains the literal value none.

Type Declaration

target: Entity?            // Current target

The ? suffix indicates that these variables might not have a value.

Default Value operator ??

You use ?? to provide a default value. If the value is none, then the value to the right of ?? is used, otherwise the unwrapped value.

// Using ?? to provide default values
x := spawn_point ?? (0.0, 0.0)                 // Default to 0.0 if no spawn point

Type alias

To Review:

catnipped please review this

Adds a name to a type. You can not define an alias for another alias, named struct struct or enum type.

type My2dPosition = (Int, Int)

Bits

bits defines a packed value type stored inside a single unsigned integer. Each field occupies a fixed number of bits instead of a full 32 bit integer.

Motivation

Normally:

Bool  // at least 1 byte
Int   // 4 bytes

So a structure like:

struct Data {
    is_attacking: Bool,
    is_flying: Bool,
    small_id: Int,
}

// may use 6–12 bytes depending on alignment.

With bits, the same data can fit inside 1 byte.

The compiler packs the fields and accesses them using bit masks and shifts.

It is also useful for mapping directly to hardware registers (e.g., Game Boy Advance sprite control) or network protocols.

Syntax

bits Something {
    is_attacking: 1,
    small_id: 4, // can store values 0–15
    is_flying: 1,
}

// will be represented as an U8 (byte)

Storage Size

If no storage size is specified, the compiler selects the smallest unsigned integer that fits all bits:

total bitstype
1–8U8
9–16U16
17–32U32

Explicit size

You may force a storage type:

bits Something : U16 {
    is_attacking: 1,
    small_id: 4,
    is_flying: 1,
}

Now it always occupies 16 bits, even though only 6 are used.

Memory Layout

bit index: 7 6 5 4 3 2 1 0
           - - F I I I I A
maskfield
00000001is_attacking
00011110small_id
00100000is_flying

Creating bits

mut a := Something { small_id: 3 }
// desugared to: a = 0b00000110

Writing to a bit field

a.is_flying = 1
// desugared to: a = a | 0b00100000

Reading from a bit field

found_id: Int = a.small_id

// desugared to: found_id = (a & 0b00011110) >> 1

bitwise OR

For each bit it does an OR. if any of the bits is set, the result is 1, otherwise 0:

00000110
00010000
--------
00010110 // is_flying and small_id == 3
if a.is_flying {  // will be desugared to: if (a & 0b00100000) != 0 {

}
found_id: Int = a.small_id // (a & 0b00000110) >> 1
found_id = a.small_id.int() // (a & 0b00000110) >> 1

Collection Types

Array Types

Array types store elements sequentially in memory. While they look similar and all support iteration, the operations have different semantic meanings for each type. For example, adding to a Stack means “push onto top” while adding to a Vec means “append to end” and adding to a Queue means “enqueue at back”.

Generic Array Type: [T] represents any sequential array type. Use it in function parameters when your function works with any kind of array-like collection. For example, a function that sums numbers can accept [Int] and work with a Vec, Array, or Stack — you don’t need to write separate functions for each type.

CollectionOrderUse cases
Fixed Size Array [T;N]Fixed len. len() always returns N. You have other ways to know if an element is used, or that all are always used.
VecYou need to retain a specific order. add to tail is fast, remove is usually slow.
Stack✅ (LIFO)
Queue✅ (FIFO)
BagWhen order is irrelevant. Fast to add, erase, and iterate. Uses swap-remove4 for erase.
Grid✅ (spatial)Fixed len. 2D grid, all elements exists.
PoolWhen order is irrelevant, but you want to have an ID to reference an element. Fast to add, erase, and iterate.

Lookup Types

CollectionOrderUse cases
MapLookup a value from a key. Relatively slow to add and remove — depending on the key size. Slower to iterate, since it has “gaps”.

Swap Remove

A neat trick where you take the last element in the collection and overwrites the element that should be removed. In this case only one copy is needed.

  • Swap remove. At worst a single copy is done:
// Before:
| a | b | c | d | e | f | g | h | i |
// Operation: 'i' overwrites 'b', len -= 1 (to 'remove' the old i)
// After:
| a | i | c | d | e | f | g | h |
  • Vec remove. you have to copy all elements after the one that is removed:
// Before:
| a | b | c | d | e | f | g | h | i |
// Operation: 'c' through 'i' are copied to the left. len -= 1.
// After:
| a | c | d | e | f | g | h | i |

Collections under consideration

Unimplemented:

Collections are not decided on yet

CollectionSequential OrderDescription
SparseHas ID to reference elements. removing leaves gap in sequence. slower to iterate
Setkey: K (key-only). Only to check if a Key exists or not.
Arena❌ (append-only)handle or offset. Can only clear all elements, not individual elements.
RingBuffer✅ (cyclic)index: u16
BitSetbit index: u16

Fixed-Capacity Collections

Swamp takes a compile-time approach to memory management. Collections have a fixed maximum capacity known at compile time — no runtime growth, no surprises.

This design provides:

  • Zero runtime allocation cost — all memory is preallocated.

  • Predictable memory usage — the compiler knows exactly how much you need.

  • No GC or pauses — no runtime allocations, no stutters.

  • No leaks or fragmentation — static allocation prevents heap issues.

  • Clearer constraints — forces upfront memory budgeting.

  • Better cache locality — contiguous memory improves performance.

  • Trivial serialization — data can be saved or restored as-is.

  • Simpler debugging & tooling — fixed layouts make inspection easy.

  • Networking-ready — structs can be sent directly as binary chunks.

When you create a collection, you specify its capacity using angle brackets with a semicolon <Type; N>:

// Create a Vec that can hold up to 64 integers
enemies: Vec<Int; 64> = []

Vec

A Vec is an ordered lists of items of the same type. You can create them, access their elements by position (starting at 0), and modify them if they’re mutable.

Vec Type Declaration

fn my_function (my_list: [Int]) {}

When declaring a list as a parameter, add square brackets [] surrounding the Type that the list will take.

Vec Member Functions

  • Add item to end of list (must have same Type) .add(item)
  • Remove the item and index .remove(index)

Vec Instantiation

// Initialize positions
spawn_points := [ Point { x: 0, y: 0 }, Point { x: 10, y: 10 }, Point { x: -10, y: 5 } ]

Vec Access

waypoints := [ Point { x: 0, y: 0 }, Point { x: 10, y: 10 } ]
next_pos := waypoints[1]

Vec Assignment

mut high_scores := [ 100, 95, 90, 85, 80 ]
high_scores[0] = 105

Maps

Maps are collections that store pairs of values, where you use one value (the key) to look up another (the value). The key can be a primitive that can be automatically converted to an Int: (Simple Enums, Int, Byte, Char)

Map Declaration

fn my_function(my_map: [Key: Value]) {...}

A map looks similar to a list, but has two types within the square brackets []. The first type is used as the lookup key.

Map Instantiation


enum SpawnPoint {
    StartingLevel,
    SecondLevel,
    SecretArea,
}

// Spawn points for different level names
spawn_points := [
    SpawnPoint::StartingLevel : Point { x: 0, y: 0 },
    SpawnPoint::SecondLevel : Point { x: 100, y: 50 },
    SpawnPoint::SecretArea : Point { x: -50, y: 75 },
]

Remember that each following Key/Value pair must have the same types as the last.

An empty Map is specified as:

empty_map := [:]

Map Access

spawn_points := [
    SpawnPoint::StartingLevel : Point { x: 0, y: 0 },
    SpawnPoint::SecretArea : Point { x: 100, y: 50 },
]

start_pos := spawn_points[StartingLevel]     // Get starting position

Map Assignment

mut spawn_points := [ SpawnPoint::StartingLevel: Point { x: 0, y: 0 } ]

// Update spawn point
spawn_points[SpawnPoint::StartingLevel] := Point { x: 10, y: 10 }

Control Flow

Control flow determines how your program runs. Swamp provides several ways to control the flow of your game.

If Expressions

In Swamp, every block is an expression that returns a value. This means you can use them on the right side of assignments. When an if doesn’t have an else block, the missing path automatically returns Unit () (representing “nothing”).

If Expression

// Both paths return Int
damage := if is_critical_hit {
    base_damage * 2    // Returns Int
} else {
    base_damage       // Returns Int
}

If Expression with Implicit Unit

// Type mismatch example
value := if has_powerup {
    100              // Returns Int
}                   // Implicit else returns ()
// 'value' type is unclear: Int or ()

While Loops

While loops keep running their code block as long as a condition is true.

mut projectile := spawn_projectile()
while projectile.is_active {
    projectile.update()
    projectile.check_collisions()
}

For Loops

// Update all entities
for mut enemy in enemies {
    enemy.update()
    enemy.check_player_distance()
}

// Update all entities
for id, mut enemy in map_of_enemies {
    println("Updating enemy {id}")
    enemy.update()
    enemy.check_player_distance()
}

New syntax

// Update all entities
enemies.for( |mut enemy| {
    enemy.update()
    enemy.check_player_distance()
} )

// Update all entities
map_of_enemies.for( |id, mut enemy| {
    println("Updating enemy {id}")
    enemy.update()
    enemy.check_player_distance()
} )

// Update all entities, one line
map_of_enemies.for( |mut enemy| enemy.update() )

Further Language Constructs

When - Optional Binding

// Using when to bind and check optionals
when equipped_weapon {
    // equipped_weapon is now bound and available in this scope
    equipped_weapon.attack()
}

// Can be used with else
when target = find_nearest_enemy() {
    target.take_damage(10)
} else {
    player.search_area()
}

Constants

Constants are fixed values that remain unchanged throughout the execution of your program. Unlike variables, which can be mutable or immutable, constants are inherently immutable and are intended for values that should not be altered once set. They are ideal for defining configuration parameters, fixed values, or any data that should remain consistent across different parts of your code.

Constant Definition

Use the const keyword followed by the constant name (in SCREAMING_SNAKE_CASE5) and assign it to an expression. Constants do not require explicit type annotations, as their types are inferred from the assigned values.

const MAX_HEALTH = 100
const PI = 3.1415
const WELCOME_MESSAGE = "Welcome to Swamp!"

constants can contain more complex expressions including function calls:

const DOUBLE_PI = 2.0 * PI
const HALF_MAX_HEALTH = MAX_HEALTH / 2
const STATS = StatsStruct::calculate_stats(42)

Resource IDs

Resource IDs are a way to reference external files like images, sounds, shaders, etc. Think of them as compile-time checked file paths that ensure your assets exist both at compile time, and just before the program runs.

What are Resource IDs?

In other programming languages, you reference game assets using strings like "explosion.wav" or "player.png". The problem with that approach is if you misspell the filename or delete/rename the file, you won’t know until you run the game and it crashes. Resource IDs solve this by checking at compile time that your files exist.

// Traditional string-based approach (bad)
// If "explosion.wav" doesn't exist, you won't know until runtime
audio := load_sound("audio/explosion.wav")

// Resource ID approach (compile-time verified)
// The compiler verifies that the file exists when you build
audio : Res<Audio> = @audio/explosion

Why Use Resource IDs?

  • Catch typos early: If you write @audio/explosoin instead of @audio/explosion, the compiler will tell you immediately.

  • Never ship broken references: Your game won’t compile if asset files are missing, preventing bugs where images or sounds don’t load.

  • Better performance: Resource IDs are converted to a small integer number (u32) at compile time, making lookups extremely fast.

  • Refactoring safety: If you reorganize your asset folders, the compiler will show you both missing files, and files that are not used in your application.

Resource ID Syntax

Resource IDs always start with the @ symbol followed by a path to your asset file (without the file extension). The reason for not specifying extension is that you should be able to change the actual file type without breaking anything (e.g. from .jpg to .png).

Basic Path-Based Resource IDs
// Reference a single asset
// The compiler looks for "explosion.wav" (or similar) in assets/audio/sub_dir/
explosion_sound : Res<Audio> = @audio/sub_dir/explosion

// Reference an image
// The compiler looks for "player.png" (or similar) in assets/gfx/
player_sprite : Res<Image> = @gfx/player

The path is relative to your project’s asset directory, and you omit the file extension (.wav, .png, etc.). The compiler will find the correct file automatically.

Indexed Resource IDs

Unimplemented:

Indexed Resource IDs is not implemented yet

For collections of related assets where you have a lot of “variants” for a single resource, you can use indexed resource IDs:

// Reference a specific card by index
// The compiler expects files like cards_00.png, cards_01.png, etc.
card : Res<Image> = @gfx/cards[42]

// Loop through numbered assets
for i in 0..100 {
    card := @gfx/cards[i]
    render_card(card)
}

// Use an expression as the index
current_level := 5
level_music : Res<Audio> = @music/level[current_level]

pseudo_random_index := pseudo_random(player_position.x)
play_sound(@sfx/footsteps[pseudo_random_index])

Type Safety

Resource IDs are typed, so you can’t accidentally use an audio file where an image is expected:

player_sprite : Res<Image> = @gfx/player      // OK

// OK. @audio/footstep bound to type Res<Audio>
player_sound : Res<Audio> = @audio/footstep

// Compile error: Audio file cannot be used as Image
wrong : Res<Image> = @audio/footstep

Extension-Based Type Inference and Type Checking

Unimplemented:

Extension-based type checking for Resource IDs is not implemented yet

You can use the #[extensions()] attribute on struct definitions to provide an improved verification. Then the compiler will either check the extension/type directly the first time that specific resource id is used, or as an extra optional analyzer end step (goes through all resource ids and matches with file extensions).

// Define types with their associated file extensions
#[extensions("png", "jpeg", "jpg")]
struct Image {
    width: Int,
    height: Int,
}

#[extensions("wav")]
struct Audio {
    sample_rate: Int,
    channels: Int,
}

Borrow binding

Binds a named borrow to an identity borrow. This is for identity-stable places only, so the alias keeps pointing to the same logical value for its entire lifetime.

The binding uses = (not :=) because you are creating an alias to an existing place, not introducing a new owned value. The alias is valid only within its scope.

a = &game.some_other_thing.another

mut b = &game.some_other_thing.another

If you need a location borrow, use with.

With

The with keyword creates a temporary binding to a specific location in memory (location borrow). It is borrowed only inside the scope {}. This is useful when you want to work with a value that’s nested deep in your data structures without repeatedly typing the full path. The location is not identity safe, so the scope should normally be short.

Immutable Binding

Use with to create a shorter name for accessing deeply nested data:

// Instead of writing game.players[2] repeatedly
with player = &game.players[2] {
    print('player x is: {player.x}')
    print('player y is: {player.y}')
    print('player health is: {player.health}')
}

Mutable Binding

Add mut to modify the data at the borrowed location:

with mut player = &game.players[2] {
    player.x = 3
    player.health -= 10

    // You still have access to outside variables
    game.something_else = 3

    print('player is now: {player}')
}

Only

The only keyword creates a new restricted scope with bound variables. It’s useful for temporarily binding values or creating local references. It is almost like mini-functions. Can be useful if you have a longer function that does not make sense to split into smaller separate functions. If you only name the binding, it will create an alias variable. e.g. a=a, a=something_else, mut a=b.

Only the specified variables are available inside the expression (block).

// defaults to something=something, another=another
only something, another {
    something + another
    x + 3 // Fails, x is not a bound variable in the `with` block
}

Guard Expressions

Guard expressions in Swamp provide a concise and powerful way to evaluate multiple conditions and return a single result (or execute a block of code) based on the first matching guard. They are similar to if-else chains in other languages, but with a more pattern-like syntax. Each guard (| condition -> result) is checked in order. As soon as one guard condition is true, its associated expression is evaluated and returned. If no guard condition matches, a wildcard guard (_) can handle the remaining cases.

reward =
    | score >= 1000 -> "Treasure Chest"
    | score >= 500  -> "Gold Coins"
    | score >= 100  -> "Silver Coins"
    | _ -> "No Reward"

AND Block Expression

Syntactic sugar for chaining multiple boolean expressions with short-circuit AND semantics. The &> operator desugars to a sequence of && operations at compile time.

a := &> {
    function_that_returns_bool() // short-circuits if false

    // Easier to add comments for each step
    number_of_items > 3 // short-circuits if false

    // And the last check
    {
        print("we reached the last check")
        another_function_with_bool()
    }
}

// Desugars to:
a := function_that_returns_bool() && number_of_items > 3 && another_function_with_bool()

Each expression in the block must evaluate to Bool. Evaluation stops at the first false (short-circuit evaluation). The block syntax makes it easier to add spacing and comments between conditions, avoiding awkward inline constructions like && /* comment */ condition &&.

Implicit Receiver

The leading dot (bare dot / lonely dot) . operator provides syntactic sugar for accessing fields an member functions on an implicit receiver. It can be used when an “obvious” receiver is known from the current scope. Currently it only supports self inside member functions.

Implicit Receiver Syntax

impl Position {
    fn set_x_and_y(mut self, i: Float) {
        .x = i  // desugars to: self.x = i
        .y = i  // desugars to: self.y = i
    }
}

Compile-time Desugar

At compile time, leading-dot expressions are desugared to explicit receiver access. The compiler changes .field to receiver.field:

  • In member function bodies: self

This is purely syntactic sugar with zero runtime cost — all receiver resolution happens at compile time.

ZII arguments (rest operator)

can use rest .. operator in function arguments, both for named and normal arguments. It will automatically call default, otherwise keep it ZII.

fn my_function(health: Int, damage: Int, modifier: Int) {...}

my_function(damage: 24 ..)
// will be desugared into zero for each argument not specified:
my_function(health: 0, modifier: 0, damage: 24)


my_function(10, ..)
// will be desugared into zero for each argument not specified:
my_function(health: 10, modifier: 0, damage: 0)

Suggested by @catnipped

Out Parameters

Overview

The out keyword designates a parameter intended for initialization rather than incremental modification. The parameter type must an aggregate. The out implicitly marks it as mutable.

out is a semantic “flag”, that you are going to initialize it, but it behaves in almost all respects as a normal mut parameter.

There is a cool difference though: functions with out parameters can be called using return-value syntax, and the compiler automatically provides the structured return (sret) value.

Out Parameter Syntax

Explicit Name
fn create_vector(out result: Vec2, x: Int, y: Int) {
    result.x = x
    result.y = y
}

fn main() {
    // Call as if it returns Vec2
    vec := create_vector(10, 20)
}
Implicit Name (Shorthand)

You can omit the variable name after out — in that case the parameter will be named out.

fn create_vector(out: Vec2, x: Int, y: Int) {
    out.x = x
    out.y = y
}

fn main() {
    // Same call syntax - looks like a regular function
    vec := create_vector(10, 20)
}

How It Works

  1. Function Definition: The function takes an out parameter (must be first parameter) and returns () (Unit / nothing)

  2. Call Site: The caller can omit the out parameter and use the function as if it returns the out parameter’s type

Comparison with Regular Returns

// Regular sret function. the Vec2 will materialize into the destination
// implicitly passed in by the compiler.
// Downside is you can not get the actual sret parameter passed in behind the scenes.
fn create_vector_return(x: Int, y: Int) -> Vec2 {
    Vec2 { x: x, y: y }
}

// Out parameter, explicit sret parameter
fn create_vector_out(out: Vec2, x: Int, y: Int) {
    out.x = x
    out.y = y
}

// Both are called the same way!
v1 := create_vector_return(10, 20)
v2 := create_vector_out(10, 20)

Explicit Out Parameter Passing

You can still explicitly pass the destination if needed:

fn main() {
    mut my_vec: Vec2

    // Explicit: pass the destination yourself
    create_vector(&my_vec, 10, 20)
}

Non-Capturing Lambda

To Review:

catnipped please review this

They look very close to a closure or a lambda. But the key distinction is that they do not capture the variables or builds a state. They are basically just inserted during code generation. This makes the very performant. They can, by design, not be used as functions.

// Just with an expression
| id | id + 2

// with a block
| key, value | {
    print('key: {key}, value: {value}')
}

Pattern Matching

Pattern matching is a powerful way to handle different cases in your code. It’s like a super-powered if expression that can look inside complex types and handle multiple cases clearly.

Basic Patterns

match game_state {
    Playing -> update_game(),
    Paused -> show_pause_menu(),
    GameOver -> show_final_score(),
    _ -> show_main_menu()
}

Multiple Patterns

match item {
    // Simple variant matching
    Gold -> {
        player.money += 100
    }

    // Tuple variant destructuring
    Weapon damage, range -> {
        player.equip_weapon(damage, range)
    }

    // Struct variant destructuring
    // `{` `}` is needed since it is field name references
    Armor { defense, weight } -> {
        if player.can_carry(weight) {
            player.equip_armor(defense)
        }
    }
}

Literal Patterns

// Numeric and string literals
match player.health {
    100 -> ui.show_status("Full Health"),
    1..10 -> ui.show_status("Low Health!"),
    _ -> ui.show_health(player.health),
}

// Tuple patterns with literals and variables
match position {
    0, 0 -> player.spawn_at_origin(),
    0, y -> player.spawn_at_height(y),
    x, 0 -> player.spawn_at_width(x),
    x, y -> player.spawn_at(x, y),
}

// Struct patterns with literals
match entity {
    Player health: 100, mana: 100 -> ui.show_status("Full Power!"),
    Player health: 0 -> {
        player.die()
    }
    Enemy health: 1 -> {
        enemy.enter_rage_mode()
    }
}

// Enum patterns with data
match item {
    Gold -> {
        player.money += 100
        ui.show_pickup("Gold")
    }
    Weapon 0, _ -> ui.show_status("Broken Weapon"),
    Weapon damage, range -> player.equip_weapon(damage, range),
    Armor defense: 0 -> ui.show_status("Broken Armor"),
}

Guard Patterns

match player_state {
    Attacking damage | has_power_up ->
        apply_damage(damage * 2),
    Attacking damage ->
        apply_damage(damage),
    _ -> (),
}

Operators

Binary Operators

// Arithmetic: +, -, *, /, %
remaining_health := health - damage

// Logical: &&, ||
can_attack := in_range && has_ammo

// Comparison: ==, !=, <, <=, >, >=
if player.mana >= spell.cost {
    cast_spell(spell)
}

// Range: ..
for frame in 0..animation.frame_count {
    render_frame(frame)
}

Unary Operators

// Negation (-)
velocity.x = -velocity.x  // Reverse direction

// Logical NOT (!)
if !inventory.is_full {
    pickup_item()
}

Suffix Operators

// Optional unwrap (?)
if equipped_weapon?.can_fire {
    fire_weapon()
}

current_target := find_nearest_enemy()?

Iterable Sequences

Swamp provides several ways to work with sequences of values in your game. You can loop through ranges of numbers, collections of items, or any other sequence using for loops.

Exclusive Range

// Countdown timer
for i in 3..0 {
    display_number(i)
}

Inclusive Range

for hp in 1..=max_health {
    draw_health_pip(hp)
}

Array Iteration

for item in inventory {
    item.draw()
}

Map Iteration

for player_id, score in high_scores {
    display_score(player_id, score)
}

Others

Anchor

Creates a compile time allocation with a specific name. The allocation and name is only available for Host (the game engine). Those identifiers can not be referenced in Swamp code.

The Host typically searches for the identifiers using the name. The Host use these anchor allocations to be mutated by calling member functions for those types, like update(self) and render(self).

anchor render = RenderState::new()
anchor simulation = Simulation { mode: WaitingForPlayers, .. }

Compiler Directives

Compiler directives provide metadata and instructions to the compiler rather than being part of your program’s runtime logic. They follow the general form:

#(keyword)(arguments)

Where:

  • # — directive prefix (required)
  • (keyword) — optional keyword that specifies the directive type (e.g., include)
  • (arguments) — arguments enclosed in either [...] or (...) depending on the directive

Examples:

  • Attribute (no keyword): #[extensions("png", "jpg")]
  • Include directive: #include[assets/file.png]
  • Future function-style: #some_feature(-1.0, 42, "hello")

Attributes

Attributes annotate types, functions, or other declarations with metadata that influences compilation behavior. They use the form #[attribute_name] without a keyword:

// Extension-based type verification for resource IDs
#[extensions("png", "jpeg", "jpg")]
struct Image {
    width: Int,
    height: Int,
}

Include Directive

The include directive embeds external files directly into your program at codegen/link/assembly time. The file’s contents become part of your compilation artifact (e.g. Swim file or executable), allowing you to bundle assets, configuration files, or data without file loading at runtime.

// Embed a PNG image as a byte array
const EMBEDDED_PNG = #include[assets/textures/player.png]

// The type is an array of bytes [U8; N]
// where N is the file size in bytes

The codegen/linker/assembler reads the file at the specified path (relative to your project root) and includes its raw bytes in the compilation artifact (e.g. Swim file). Similar to .incbin in ARM assembly.

Modules and Imports

The mod Keyword

To Review:

catnipped please review this

The mod keyword allows you to import modules, types, and functions from other parts of your codebase. This helps organize your code and makes it easier to access functionality defined elsewhere.

The dot notation in module paths directly corresponds to the file system structure in your project or crate locally only. Each dot represents a directory separator in the file path, and the module name is resolved to a .swamp file. For example:

  • mod gameplay resolves to gameplay.swamp
  • mod math::geometry resolves to math/geometry.swamp
  • mod engine::physics::collision resolves to engine/physics/collision.swamp

Basic Module Import

// Import an entire module
mod some_module

Nested Module Import

// Import from nested modules using dot notation
mod math::geometry::something

Selective Imports

// Import specific items from a module
mod math::geometry::{ utility_function, SomeType }

When you use selective imports, you can import multiple items at once by listing them inside curly braces. These imported items can then be used directly in your code without needing to prefix them with the module name.

The use keyword

To Review:

catnipped please review this

use is very similar to mod, but with the key difference is that use takes external modules and bring them into the namespace.

use std

use another_package::some_module::ThatType // you only need to write `ThatType`
use another_package::some_module::{ThatType, OtherType} // you only need to write `ThatType` or `OtherType`
use second_package::module_name // you don't need to write `second_package::`

Borrow

Swamp distinguishes between values (rvalues) and places (lvalues). A value is data, and a place is the memory location for that data.

A borrow gives temporary access to a place without moving or copying it.

There are two classes of places:

  • Identity-stable place

    The memory location is stable: while it is borrowed, the compiler guarantees that the same logical value stays in that location. The address is stable and cannot implicitly swap or replace the element.

  • Location-only place

    The memory location is unstable: the container may reuse or overwrite that slot during valid operations. The only guarantee is that the slot always contains a valid value of the right type.

Guarantees (Always):

  • ✅ Memory safe — no buffer overflows, no out-of-bounds writes
  • ✅ No dangling pointers — references can’t outlive their memory
  • ✅ No wild pointers — references can’t point to arbitrary/uninitialized memory
  • ✅ No process corruption — can’t write outside your process space
  • ✅ Correct alignment — types maintain their alignment requirements
  • ✅ No crashes or panics from memory errors

Identity borrow

This is the traditional aliasing borrow of an identity-stable place. The compiler guarantees that the borrowed reference continues to refer to the same logical value for the duration of the borrow.

Think of it as you are holding a reference for a specific person. The person can change their “properties” without you being involved, but it is still the same person to you.

Possible Issues (Within the borrow scope):

  • ⚠️ Aliasing surprises — another alias may mutate the same value between reads
  • ⚠️ But still deterministic — bugs are reproducible, not random

Location borrow

This is a borrow of a location-only place. The compiler only guarantees that the location always contains a valid value of type T. The location is not stable, the identity stored there may change at any time due to valid container operations.

Think of it as you are holding a note that says “the book in shelf slot 12”. Slot 12 will always hold a book of the same physical size, but it might be a different book after the shelf gets reorganized.

Because this can cause identity confusion, Swamp only allows location borrows inside a with scope, making the “identity may change”-window explicit, visible and local.

Possible Issues (Within the with borrow scope):

  • ⚠️ Logic errors — reading/writing data that’s semantically wrong
  • ⚠️ Identity confusion — reference points to different logical entity than expected
  • ⚠️ Stale data — seeing old values after logical deletion
  • ⚠️ But still deterministic — bugs are reproducible, not random

No return keyword

Swamp intentionally has no support for goto keywords that can exit a function early without reaching the end of a function, like return.

The Problem with Gotos

Edsger Dijkstra wrote, even before I was born, that “Go To Statement Considered Harmful”, arguing that unrestricted jumps make code difficult to reason about. When code can jump anywhere at any time, it becomes hard to:

  • Trace how execution arrived at a specific line
  • Predict what will execute next
  • Debug and maintain the codebase

While explicit goto statements are rare in modern programming languages, the same problems persist in its disguised form: return are jump/goto statements that bypass the natural flow. Especially since it is idiomatic in Swamp to have long functions with several scopes.

Swamp’s Structural Approach

In Swamp, the block structure directly reflects execution flow. You will always reach the end of a block, no matter what logic is inside it. This design provides several upsides:

  • No hidden jumps: There are no jumps to function ends.

  • Single exit point: Functions return only at their natural end, making it trivial to see what the function ultimately returns. No need to search for scattered return statements.

  • Reasoning simplicity: Control flow is determined entirely by block nesting and structured constructs (if/while/match), not by hunting for jump statements scattered throughout your code.

This means when you see a block, you can trust that:

fn process_items(items: [Item]) -> Int {
    mut total := 0
    for item in items {

        // We know that *all* items are considered,
        // there is no early break or continue
        total += item.value
    }

    // You know execution reaches here
    total  // Clear, single return point
}

Rather than worrying about code like this:

fn process_items(items: [Item]) -> Int {
    mut total := 0
    for item in items {
        if item.is_invalid {
            return -1  // Jump out here?
        }
        total += item.value
    }
    total  // Or does it return here?
}

Why = instead of := when binding variables?

You use = (not :=) because you’re not creating a new variable with a new type. Instead, you’re binding a name to an existing memory location. The type must match exactly what’s already there — you’re just creating a convenient “alias” to work with that location.

What is a “Place”?

A place is any expression that refers to a specific location in memory that holds a value.

What is “Borrowing”?

Borrowing means temporarily accessing a value at its memory location without making a copy (or taking ownership). The & symbol indicates you want to borrow (get a reference to) a place.

Type Inference

Swamp automatically determines types from context, so you rarely need to write them explicitly.

You only need to declare types explicitly when:

  • Declaring function parameters and return types
  • Creating struct or enum definitions
  • When the compiler needs help understanding your intent

The compiler will tell you when explicit types are needed.

4

Swap Remove. sometimes called ‘Swapback’ or ‘swap and pop’ Vec::swap_remove in Rust