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
mutto 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:trueorfalse.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:
- Check if there is a
default()function associated with the type:- Initializes the struct using the values returned by
default()function. - Overwrites any fields that are explicitly set during instantiation.
- Initializes the struct using the values returned by
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 }
-
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:
Int→0Float→0.0Bool→falseT?→noneString→""(empty string)
- Calling
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 } - Swamp iterates through each field that is not explicitly set during
instantiation and fills them individually by:
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 bits | type |
|---|---|
| 1–8 | U8 |
| 9–16 | U16 |
| 17–32 | U32 |
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
| mask | field |
|---|---|
00000001 | is_attacking |
00011110 | small_id |
00100000 | is_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.
| Collection | Order | Description |
|---|---|---|
Fixed Size Array [T;N] | ✅ | Fixed-length array. len() always returns N. |
| Vec | ✅ | Ordered sequence. .add() appends to the tail. .remove() preserves order. |
| Stack | ✅ (LIFO) | |
| Bag | ❌ | Unordered collection. Erase uses swap-remove1. |
| Grid | ✅ (spatial) | Fixed-size two-dimensional collection. All positions exist. |
| Pool | ❌ | Unordered 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.
| Collection | Description |
|---|---|
| Queue | ✅ (FIFO) |
| Ring | Overwriting circular buffer. When capacity is exceeded, the oldest elements are replaced. |
| Wrap | Generic view over circular collections. Assumes non-overwriting semantics. Mostly to allow cursor-relative subscript |
Lookup Types
| Collection | Order | Use cases |
|---|---|---|
| Map | ❌ | Key-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
-
Function Definition: The function takes an
outparameter as its first parameter and returns(). -
Call Site: The caller can omit the
outparameter and use the function as if it returns theoutparameter’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:
- use an explicit
default()for the constructed value when that form defines one, - otherwise use the default for each omitted part when available,
- 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’sdefault()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 gameplayresolves togameplay.swampmod math::geometryresolves tomath/geometry.swampmod engine::physics::collisionresolves toengine/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.
Swap Remove. sometimes called ‘Swapback’ or ‘swap and pop’ Vec::swap_remove in Rust Language Internals.