Language Specification

Detailed specification of the Swamp programming language

Comments

Swamp has line comments (//) and documentation comments (///). Documentation comments are included in generated documentation and editor tooltips.

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

/// 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
}

Block comments are not supported. Use consecutive // lines instead.

Rationale: Why No Block Comments

Naming

Swamp uses fixed naming rules for declarations:

  • Variables and functions use snake_case.
  • Types and enum variants use CamelCase.
  • Constants use SCREAMING_SNAKE_CASE.

Variables

Variable Definition

Defining a variable in Swamp is simple:

player_name := "Hero"

You don’t need to declare the type explicitly; it is, in most cases, inferred.

Variables are immutable by default. Use mut to declare a mutable variable.

Rationale: Why Mut

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

Variable Reassignment

Only mutable variables can be reassigned.

// 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
    }
    // bonus is not accessible here
}   // power_up is automatically cleaned up here

Mutability and Reference Rules

Variables are immutable by default.

  • Use mut to declare a variable that may be reassigned.

  • References are requested with &.

  • References can only be used in specific contexts and cannot be stored.

Idiom: Immutability First

Variable Type Annotation

Type annotations are required when the variable type cannot be inferred from the initialization, e.g. for none, [], or [:].

a : Int? = none

numbers : [Int] = []

settings : [Int:Float] = [:]

Type Inference

Swamp automatically determines types from context when possible.

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.

Basic Types

Swamp provides these basic types:

  • Int: whole numbers.
  • Float: decimal values (technically fixed-point numbers).
  • Bool: true or false.
  • String: text.
  • Char: code points.

Low level:

  • U8: 8-bit unsigned integer.
  • U16: 16-bit unsigned integer.
  • U32: 32-bit unsigned integer.

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 delimited by "". Quote characters inside a string are escaped as \".

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, write struct followed by the name of the struct and a pair of curly brackets {}. Inside the brackets, list the fields the struct contains and their types.

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

Struct Instantiation

Struct instantiation assigns a value to each field unless .. is used.

player := Player {
    position: Point { x: 0.0, y: 0.0 }
    health: 100
    mana: 50
    speed: 5.0
}
Initializer lists for fields

Struct values may also be constructed positionally inside initializer lists. The values are assigned to fields in declaration order.

struct Color {
    r: U8
    g: U8
    b: U8
    a: U8
}

colors: Vec<Color; 16> = [
    [0xff, 0xff, 0x19, 0xff],
    [0x18, 0x39, 0xAA, 0xFF],
]
Using .. in struct literals

This is one use of the Rest Operator.

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

  1. Check if there is a default() function associated with the type:
    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 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 }
  1. If no default() functions 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() on the field type.
      • Using built-in defaults for primitive types:
        • Int0
        • Float0.0
        • Boolfalse
        • T?none
        • String"" (empty string)

    Example (No default() function):

    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

Fields are accessed with a period (struct.field).

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

Struct Field Assignment

Fields of a mutable value may be assigned new values.

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

impl attaches 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 do not have 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 define a type with one of several variants. Each variant may optionally carry data.

Enum Definition

To define an enum, write enum followed by its name and a pair of curly brackets {}. Inside the brackets, list the variants the enum can contain.

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

Enums may be pattern matched.

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!")
        }
    }
}

Enum Variant Testing

Enums can be tested inline using == and !=. Currently only the discriminant (the variant tag) is tested for equality. Any payload fields must be written as _ wildcards.

// Match any Weapon, regardless of stats
if item == Item::Weapon(_, _) {
    show_message("You have a weapon!")
}

// Match any item that is not a Potion
if item != Item::Potion(_) {
    show_message("Not a potion")
}

Tag Type

When all enum variants are defined without payloads, the enum becomes a Tag type. Tag types are represented as U8, U16, or U32.

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

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.

Purpose

bits packs multiple fixed-width fields into a single unsigned integer storage type.

Rationale: Why Bits

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

Type alias

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)

Collection Types

Swamp collections have a fixed maximum capacity known at compile time. There is no runtime growth.

Rationale: Why Fixed-Capacity Collections

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
enemy_ids: Vec<Int; 64> = []

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. In function parameters, [T] accepts any array-like collection of T.

Fixed-size array initializer lists follow the Rest Operator rules for omitted trailing elements and explicit .. completion.

CollectionOrderDescription
Fixed Size Array [T;N]Fixed-length array. len() always returns N.
VecOrdered sequence. .add() appends to the tail. .remove() preserves order.
Stack✅ (LIFO)
BagUnordered collection. Erase uses swap-remove1.
Grid✅ (spatial)Fixed-size two-dimensional collection. All positions exist.
PoolUnordered collection with element identifiers. Erase uses swap-remove1.

Vec

A Vec is an ordered collection of items of the same type.

Vec Type Declaration
fn my_function (my_list: [Int]) {}

In parameter position, square brackets [] denote an array-like collection of that element type.

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

Circular Collections

Elements do not move in memory; a cursor tracks the logical start.

CollectionDescription
Queue✅ (FIFO)
RingOverwriting circular buffer. When capacity is exceeded, the oldest elements are replaced.
WrapGeneric view over circular collections. Assumes non-overwriting semantics. Mostly to allow cursor-relative subscript

Lookup Types

CollectionOrderUse cases
MapKey-value lookup collection. Iteration order is not defined.

Maps

Maps store key-value pairs.

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

A map type is written with two types inside square brackets []. The first type is the key type.

Key Type Restrictions

Map keys in Swamp are restricted to Int or tag types (simple enums without payloads). For enums with simple payloads, a hash() associated function may convert the enum to an Int for use as a key.

Map Instantiation

enum SpawnPoint {
    StartingLevel
    SecondLevel
    SecretArea
}

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

Each key-value pair in a map literal must use the same key and value types.

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 }

Constants and Resources

Constants

Constants are named values that remain unchanged throughout program execution. Constant identifiers must use SCREAMING_SNAKE_CASE. They are available throughout the program, they have no scope.

Constant Definition

Use the const keyword followed by a name and assign it to an expression. Constants usually 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.

Rationale: Why Resource IDs

Resource ID Syntax

Resource IDs always start with the @ symbol followed by a path to your asset file (including the file extension).

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.wav

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

The path is relative to your project’s asset directory.

Indexed Resource IDs

Unimplemented:

Indexed Resource IDs is not implemented yet

Indexed Resource IDs address numbered variants of a resource family.

// Reference a specific card by index
// The compiler expects files like cards_00.png, cards_01.png, etc.
card : Res<Image> = @gfx/cards.png[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.png      // OK

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

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

Extension-Based Type Inference and Type Checking

Unimplemented:

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

The #[extensions()] attribute on a struct definition supplies file extensions for resource verification. The compiler may use that information during resource ID checking.

// 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
}

Functions

Function Definition

fn my_function(parameter: Type) {
}

Functions are declared with fn followed by the function name and parameters in parentheses. Each parameter also needs a declared type.

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. The last expression of the function body is always the return value; there is no return keyword.

Rationale: Why No `return` Keyword

Parameters

Parameter Mutability

Parameters marked with mut may be modified within the function body.

// 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)
}

Out Parameters

The out keyword designates a parameter intended for initialization rather than incremental modification. The parameter type must be an aggregate. An out parameter is implicitly mutable.

out behaves in almost all respects as a normal mut parameter.

Functions with out parameters can be called using return-value syntax. In that form, the compiler automatically provides the structured return (sret) destination.

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 as its first parameter and returns ().

  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)
}

Types of Functions

Swamp has three kinds of functions:

Member Functions

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

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 {
    fn create_sword() -> Weapon {
        Weapon {
            damage: 10
            range: 2.0
            weapon_type: Sword
        }
    }
}

sword := Weapon::create_sword()

Standalone Functions

Standalone functions are functions that are not associated with a type.

/// 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)

Omitting arguments with ..

The .. operator may be used in function calls with both named and positional arguments. This is one use of the Rest Operator. Unspecified arguments are filled using the type’s default value when available; otherwise they use the zero-initialized value.

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)

Implicit Receiver

The leading dot . operator provides syntactic sugar for accessing fields and member functions on an implicit receiver. Currently it only supports self inside member functions.

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
    }
}

At compile time, leading-dot expressions are desugared to explicit receiver access (self.field). This is purely syntactic sugar resolved at compile time.

Rest Operator

The rest operator .. fills in the parts of a construct that the programmer did not specify explicitly.

It may be used in:

  • initializer lists,
  • struct literals,
  • and function calls.

When .. is used, Swamp completes the unspecified parts using the applicable default-completion rules for that construct:

  1. use an explicit default() for the constructed value when that form defines one,
  2. otherwise use the default for each omitted part when available,
  3. otherwise use the zero-initialized value.

The exact completion rules depend on the construct where .. appears.

In initializer lists

Initializer lists have two distinct completion behaviors:

Initializer-list elements may themselves be aggregate values. In that case, an initializer list may fill the fields positionally in declaration order.

  • If values are omitted without .., the remaining elements are filled using the zero-initialized value of the element type.
  • If .. is present, the remaining elements are filled using the element type’s default() value when available; otherwise they use the zero-initialized value.
enum SomeTag {
    First
    Second
    Third
}

impl SomeTag {
    fn default() -> SomeTag {
        Second
    }
}

arr: [SomeTag; 4] = [Third, Third]
// Result: [Third, Third, First, First]

arr: [SomeTag; 4] = [Third, Third, ..]
// Result: [Third, Third, Second, Second]

Only the second form uses the rest operator.

Control Flow

Control flow determines evaluation order and repetition.

If

Every block is an expression that yields a value. If an if expression has no else block, the missing path yields unit ().

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

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()
}

Break

break exits the innermost enclosing loop immediately.

It may be used inside any loop form.

mut projectile := spawn_projectile()
while projectile.is_active {
    if projectile.health <= 0 {
        break
    }

    projectile.update()
}

For

// 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()
}

Ranges

for loops can iterate over exclusive (..) and inclusive (..=) ranges.

// Exclusive range: 3, 2, 1 (does not include 0)
for i in 3..0 {
    display_number(i)
}

// Inclusive range: 1 through max_health
for hp in 1..=max_health {
    draw_health_pip(hp)
}

Continue

continue skips the rest of the current loop iteration and proceeds with the next iteration of the innermost enclosing loop.

continue may only be used in for loops.

for enemy in enemies {
    if !enemy.is_active {
        continue
    }

    enemy.update()
}

Match

match evaluates a value against a sequence of patterns and selects the first matching arm.

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")
}

Pattern with &&

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

Guard Expressions

Guard expressions evaluate a sequence of guards in order and yield the result of the first matching guard. Each guard has the form | condition -> result. A wildcard guard (_) must be specified to 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.

Transformers

Collection member functions such as .filter(), .find(), and .map() accept a non-capturing lambda and return a transformed result.

Non-Capturing Lambda

Non-capturing lambdas look similar to closures, but do not capture surrounding variables or build state. They are inlined at code generation and cannot be stored or passed as function values.

// With an expression
| id | id + 2

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

Transformer Example

for Transformer

// 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() )

Operators

Binary Operators

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

// Bitwise: |, &, ^, <<, >>
mask := flags_a | flags_b
intersection := flags_a & flags_b
toggled := flags ^ 0b00000100
shifted_left := value << 2
shifted_right := value >> 1

// Rotate: <<<, >>>
rotated_left := bits <<< 3
rotated_right := bits >>> 5

// 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()
}

Scoped Bindings

When

Bindings for optional types.

// 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()
}

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). The borrow is valid only inside the following block. Because it is a location borrow, the binding refers to the addressed location for the lifetime of that block.

Immutable Binding

An immutable with binding provides read access through that location borrow:

// 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 evaluates a block in a restricted scope containing only the explicitly bound variables.

Bindings may be written explicitly, such as a = something_else or mut a = b. A bare name is shorthand for binding the name to itself, such as a meaning a = a.

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 `only` block
}

Modules and Imports

The mod Keyword

The mod keyword imports modules, types, and functions from other parts of the codebase.

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 }

Selective imports list multiple items inside curly braces. Those imported items may then be referenced without a module prefix.

The use keyword

use imports external modules into the namespace.

// `math` is available to use as a prefix
use math

// you only need to write `ThatType` or `OtherType` without prefix
use another_package::some_module::{ThatType, OtherType}

// you only need to write `module_name::`, without the `second_package::` prefix
use second_package::module_name

Anchor

Creates a compile-time allocation with a specific name. The allocation and name are available only to the Host (the game engine). Those identifiers cannot be referenced in Swamp code.

The Host may resolve these allocations by name and invoke member functions on the allocated values, such as 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. 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 an external file into the compilation artifact at code generation, link, or assembly time.

// 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 code generator, linker, or assembler reads the file at the specified path relative to the project root and includes its raw bytes in the compilation artifact.

Glossary

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

An identity borrow is a 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.

Other aliases may still mutate that value during the borrow.

Location borrow

A location borrow is a borrow of a location-only place. The compiler guarantees only that the location contains a valid value of type T. The logical identity stored at that location may change during the borrow due to valid container operations.

Location borrows are only allowed inside a with scope.

1

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