The Swamp Way

Idioms, best practices, and philosophy for writing Swamp code.

Single Assignment

Avoid:

mut a = 0
if something {
  a = 4
} else {
  a = 5
}

Prefer:

a = if something 4 else 5

Avoid:

mut direction = Vec2 { .. }
if input == Input::Left {
    direction = Vec2::new(-1, 0)
} else if input == Input::Right {
    direction = Vec2::new(1, 0)
} else {
    direction = Vec2::new(0, 0)
}

Prefer:

direction = match input {
    Left  -> Vec2::new(-1, 0),
    Right -> Vec2::new(1, 0),
    _     -> Vec2::new(0, 0),
}
👉

Principle:

One assignment tells the whole story — mutation muddies it.

ZII - Zero Is Initialization

Zero should always be safe. Zero means nothing, and nothing is safe.

Embrace zero and ZII. The value 0, none, or the first variant of an enum (discriminant is zero for the first) should always be safe and well-defined. This makes it easy to check for “empty” or “non-existent” without introducing optional types.

The benefit is not just simpler code, but also leaner code generation — the compiler doesn’t need to emit extra instructions for wrapping and unwrapping optionals.

Option Overhead

Comparison of memory layout between Int and Int?

Only use ZII if there is a “natural” existing field that can be used to detect if the type is “nothing”, inactive or harmless. Do not add a field just to make it ZII, use an optional in that case: T?.

Avoid:

struct Avatar {
    id: Int,
}

fn find_avatar(distance: Int) -> Avatar? {
    ...
}


avatar = find_avatar(20)
when avatar {
    render_avatar(avatar)
}

Prefer:

struct Avatar {
    id: Int,
}

// id will be zero if avatar is not found
fn find_avatar(distance: Int) -> Avatar {
    ...
}

avatar = find_avatar(20)
if avatar.id != 0 {
    render_avatar(avatar)
}

Enums and ZII

The first variant of an enum is always the zero value — and it should always be safe. This way, an uninitialized or default value never causes harm.

enum Action {
    Idle,
    DrinkPotion,
}

fn avatar_do_action(avatar: Avatar, action: Action) {
    match action {
        Idle -> {} // nothing "unsafe" happens
        DrinkPotion -> avatar.health += 100,
    }
}

IDs and ZII

When using 0 to mean “not found” or “empty,” make sure your ID generators never produce 0. Always start IDs at 1 and count upward. This keeps the zero value reserved as the safe default.

When Zero Can’t Mean “Nothing”

Sometimes zero is a valid value: coordinates, vectors, matrices, and other numeric types where 0 has real meaning. In these cases, you cannot reserve zero to signal “empty.”

For these types, use an optional (T?) instead.

Rest Operator

Fill in what matters, zero the rest.

When creating struct literals, the .. operator automatically sets all unspecified fields to their zero value. This makes code shorter, clearer, and consistent with the ZII principle.

Instead of manually filling in every field, you can focus only on the values that matter — the rest are guaranteed safe because zero is safe.

If the type being constructed has a default() associated function, it’s used for unspecified fields; otherwise each unspecified field falls back to its own default() if present; if neither applies, the unspecified field is zero-initialized.

todo: new sentence: If struct has default(), use it first; else each field’s default(); else zero-init.

Avoid (explicit zeroes):

struct Player {
    id: Int,
    score: Int,
    gold: Int,
    last_checkpoint: Int,
}

player = Player {
    id: 42,
    score: 0,
    gold: 0,
    last_checkpoint: 0,
}

Prefer (rest operator):

struct Player {
    id: Int,
    score: Int,
    gold: Int,
    last_checkpoint: Int,
}

player = Player {
    id: 42,
    ..
}

Max Four Parameters

Functions should be simple to call and easy to read.

Functions with many parameters are confusing: the order is easy to mix up, the meaning of each argument is unclear, and the code becomes harder to maintain.

There’s also a performance cost:

  • Each parameter must be stored to a temporary register, moved into ABI order, and sometimes copied back for scalars.

  • On many platforms, when the number of parameters grows, extra ones must be spilled to the stack, introducing memory traffic and alignment adjustments.

  • With a ready struct, the overhead is minimal — just passing the pointer to the struct.

Instead, if you must have more than four arguments, group related values into a named or anonymous struct and pass that as a single argument. This improves readability, makes the code self-documenting, and lets you extend later without breaking call sites.

Note that if some fields are mutable and some are not, you need to create two different structs. Unfortunately there is currently a bug where you need to set storage for those fields, but that will change in the future.

Functions with many parameters are harder to use correctly. Research suggests developers struggle with more than four parameters RamaKak2013, and psychology shows that people can juggle around four items in working memory Cowan2010.

Avoid:

fn draw_avatar(x: Int, y: Int, rotation: Float, scale: Float,
    opacity: Float, avatar: Avatar) {
    ...
}

draw_avatar(120, 200, 0.0, 1.0, 0.8, Avatar {..})

Prefer:

struct DrawParams {
    x: Int,
    y: Int,
    rotation: Float,
    scale: Float,
    opacity: Float,
}

fn draw_avatar(params: DrawParams, avatar: Avatar) {
    ...
}

fn draw_spaceship(params: DrawParams, spaceship: Spaceship) {
    ...
}

draw_avatar(DrawParams {
    x: 120,
    y: 200,
    rotation: 0.0,
    scale: 1.0,
    opacity: 0.8,
}, Avatar {..})

Anonymous Structs for Rare Cases

Don’t name what you only use once.

If a group of fields is only used in one or a few places, there’s no need to define a named struct for it.

Anonymous structs are perfect for bundling values together for a single call site or a temporary grouping.

fn draw_special_button( special_params: {
    width: Int,
    color: Color,
    tab_count: Int,
} ) { ... }


draw_special_button( {
    width: 10,
    color: Color::new(1.0, 0.8, 0.8),
    tab_count: 1,
} ) { ... }

Use in sub structs inside a struct

If the struct grouping is not used in many places, use an anonymous struct:

struct Avatar {
    // This is only used for the avatar target info
    target_info: { unit: Unit, distance: Int },
    speed: Int,
}

No Single-Use Functions

Functions are for reuse. Scopes are for structure (and speed).

It’s good practice to group related code into scopes {} or with blocks for readability. But don’t create named functions that are only ever called once or twice. If the code isn’t reused, keep it inline — it makes the flow easier to follow and avoids cluttering your code with unnecessary names.

Single-use helpers force the compiler to juggle calling conventions: shuffle args into temps, reorder for the ABI, push/pop callee-saved registers, emit prologue/epilogue, branch to call and back on ret. Inline scopes skip all of that, yielding fewer instructions and better cache/branch behavior.

Inline code is easier to refactor. You don’t have to update function signatures or chase down call sites when adding or removing a local variable.

Avoid:

fn draw_main_menu() {
    // new_game_button will only be called from here
    draw_new_game_button()

    // quit_game_button will only be called from here
    draw_quit_game_button()
}

Prefer:

fn draw_main_menu(gfx: Gfx) {
    // New Game Button
    {
        draw_button_border(gfx, BLUE)
        draw_text(LocalizedString::NewGame)
    }

    // Quit Game Button
    {
        draw_button_border(gfx, GREEN)
        draw_text(LocalizedString::QuitGame)
    }
}

Or:

fn draw_main_menu() {
    // New Game Button
    with gfx {
        draw_button_border(gfx, BLUE)
        draw_text(LocalizedString::NewGame)
    }

    // Quit Game Button
    with gfx {
        draw_button_border(gfx, GREEN)
        draw_text(LocalizedString::QuitGame)
    }
}

Note: Another upside with scopes is that variables defined in the scope are not taking up registers outside the scope. The variable name can therefore be reused in other scopes.

Associated Functions

Keep behavior with the type it belongs to.

When a function conceptually belongs to a type, make it an associated function (impl) instead of a free function.

This has two big advantages:

  • Discoverability: Code completion and intellisense will show all available functions when you type value and a dot .. You don’t have to remember the name of free functions.

  • Documentation: The impl block becomes the single place to look when you want to know what operations a type supports.

Avoid (free function):

fn avatar_target_range(a: Avatar) -> Int {
    a.base_range + a.boosted_range
}

Prefer (associated member):

impl Avatar {
    fn target_range(self) -> Int {
        self.base_range + self.boosted_range
    }
}

range = avatar.target_range()

No Strings

Strings are for people, not for game code.

In game code you should rarely use strings. Strings are very heavy: they take more memory, require extra allocations, and slow down comparisons.

Worse, strings lose information. If you turn structured data (like an ID or a health value) into a string, you throw away its type and meaning. Getting it back requires error-prone parsing (never ever do that) — and usually ends up slower and less flexible.

Use identifiers, enums, or numeric handles instead. Strings should only appear as late as possible in rendering — that way you can support localization, keep your game code lightweight, and preserve information in its structured form.

  • Memory: numbers and enums are far cheaper to store than strings.

  • Performance: string comparisons and hashing are much slower than integers.

  • Information: strings flatten structured data and throw away semantics.

  • Flexibility: separating data from presentation is a good general rule anyway, and makes localization and content changes easy.

👉

Principle:

Strings belong at the edge of your game — for the player, not the game code. Inside the game, keep information structured.

Wrong:

if action == "DRINK_POTION" {
    avatar.health += 100
}

Right:

enum Action {
    Idle,
    DrinkPotion,
}

if action == Action::DrinkPotion {
    avatar.health += 100
}

Sometimes:

There are only a few valid uses of strings in Swamp code:

  • Debugging, assertions, panics and logging:
info('loading level {level_id}')
debug('new game was selected')
print("starting boss fight")
assert(avatar.velocity < 1000, "Avatar unreasonable velocity")
panic("value was out of range")
  • Storing player names: e.g. names coming from external services(Steam, PSN, etc.).

  • Presentation edge:

When rendering localized, interpolated strings to the player

enum LocalizedStringId {
    StartGame,
    AreYouSure,
}

enum Language {
    Swedish,
    English,
}

fn get_localized_string(
    language: Language,
    id: LocalizedStringId,
    game: Game) -> String {
    ...
}
  • Parsing text formats

Parsing text formats should usually be done by the engine, not in Swamp — but there are times when it’s necessary.

Everywhere else: keep your data structured.

No Defensive Coding

A hidden bug is a delayed disaster.

Do not accept faulty states or “fix” them silently by clamping, resetting, or ignoring invalid values. This only hides the real bug and makes it harder to track down.

Instead, fail fast and loud with asserts. During development, asserts will catch invalid states immediately. In release builds, they can be stripped automatically, so they don’t cost performance.

Wrong (defensive coding):

fn update_avatar(mut avatar: Avatar) {
    if avatar.gold < 0 {
        avatar.gold = 0 // silently fix
    }
}

Right:

fn update_avatar(mut avatar: Avatar) {
    assert(avatar.gold >= 0, "Gold amount should never be negative")
}

Wrong (defensive coding):

my_position = match get_joystick_value() { //this can only ever be between 0-127
    0 -> 0,
    1..127 -> 1,
    _ -> 0 // defensive coding, we are handling an illegal value
}

Right:

my_position = match get_joystick_value() { // this can only ever be between 0-127
    0 -> 0,
    1..127 -> 1,
    _ -> panic("joystick value was not in range")
}
👉

Principle:

Bugs should surface at the moment they happen, not be hidden under “safe defaults.”

When Clamping Is Okay

Clamping or handling wrong states are valid in two situations:

  1. At the edges of the game — when input comes from outside sources you cannot fully control (e.g. input, hardware, or network).

  2. As part of the simulation rules — when the game design itself defines a hard limit.

// Movement speed cannot exceed max velocity by design
avatar.velocity = avatar.velocity.clamp(0, MAX_VELOCITY)

Compile time over Runtime

Decide as early as possible.

Swamp is designed for determinism and performance. When a value can be known at compile time, prefer to make it a constant instead of computing or looking it up at runtime.

This leads to:

Performance: fewer runtime instructions.

Predictability: no hidden work during simulation.

Simplicity: easier to reason about when values never change.

Avoid:

fn update_physics() {
    gravity = 9 * 1000 / 60  // recalculated every tick
    velocity += gravity
}

Prefer (compile-time constant):

const GRAVITY_PER_TICK = 9 * 1000 / 60 // computed once at load time

fn update_physics() {
    velocity += GRAVITY_PER_TICK
}

Code Is Data

If you know the data ahead of time, bake it directly into the code as constants instead of loading from a file.

Avoid (runtime loading):

fn load_cards() -> [Card] {
    read_json_file("cards.json")
}

Prefer (compile time):

struct Card {
    id: Int,
    name: LocalizedStringId,
    power: Int,
}

const CARDS = [
    Card { id: 1, name: LocalizedStringId::Fireball, power: 5 },
    Card { id: 2, name: LocalizedStringId::Healing,  power: 3 },
]

Here the entire card library is in constant memory. No runtime parsing, no file I/O, no indirection — just direct access to data that never changes.

👉

Principle:

If you already know it, compile it in. Runtime is for the unknown.

Type Inference

Infer more, clutter less.

Swamp’s type inference makes code shorter, cleaner, and easier to read. Explicit types are only needed when type cannot be inferred.

Avoid:

player: Player = Player::new()
score: Int = 0

Prefer (inferred):

player = Player::new()
score = 0

Sometimes:

Optionals and other cases where a value has multiple meanings may need explicit annotation.

maybe_score: Int? = 0

Here, 0 is lifted into Some(0), by the Int? annotation.

👉

Principle:

Infer the obvious, state the meaningful.

Return Values Instead of Mutating Parameters

Builders should build — not patch.

If a function’s purpose is to initialize a value, make it return that value. Don’t take an existing variable as a mut parameter and fill it in.

This makes such functions easy to use directly inside struct initializers and expressions, where mutation isn’t possible. It also improves performance: returning writes the result directly into the struct field (via sret), avoiding a separate variable and the extra store/“copy” into the field.

Avoid (mutating parameter):

fn create_spawn_from_id(mut pos: Position, id: Int) {
    pos = Position { x: id, y: 0 }
}

a = Avatar {
    position: {
        mut p = Position {..}
        create_spawn_from_id(&p, 42)
        p
    },
}

Prefer (returning the value):

fn create_spawn_from_id(id: Int) -> Position {
    Position { x: id, y: 0 }
}

a = Avatar {
    position: create_spawn_from_id(42),
}

Use Direct Construction

Construct in place — not in a temporary.

When returning aggregate types from functions, the way you construct the return value can significantly impact performance. For example:

struct Logic {
    i: Int,
}

fn new() -> Logic {
    logic := Logic {
        i: 10
    }

    logic  // ❌ This creates a local variable, then copies it to the return value
}

What happens under the hood:

  1. A local variable logic is allocated on the stack (4 bytes in this example)
  2. The struct is initialized in that local variable
  3. The entire struct is copied from the local to the return location (MemCpy operation)

For a 4-byte struct this is negligible, but for larger structs, this becomes a significant performance cost.

The Solution

Instead, construct the aggregate directly as the return expression:

fn new() -> Logic {
    Logic {
        i: 10
    }  // âś… Constructed directly in the return location (no copy!)
}

What happens under the hood:

  1. The struct is initialized directly in the caller’s return location (no intermediate variable)
  2. No MemCpy operation needed
  3. Zero overhead!

Real-World Impact

For small structs (4-16 bytes), the copy overhead is usually acceptable. But as structs grow larger, the cost increases linearly:

Struct SizeCopy Overhead
4 bytes~1-2 instructions
256 bytes~64 instructions
10 000 bytes~2,500 instructions

Tuples for Small, Self-Explanatory Groups

For small, short-lived groups where order is obvious, prefer a tuple. Tuples avoid boilerplate and keep code concise.

If the grouping is reused, or if field names add clarity, use a struct instead.

Avoid:

struct Coords {
    x: Int,
    y: Int,
}

fn move_avatar(delta: Coords) { ... }

move_avatar(Coords { x: 5, y: -3 })

Prefer (tuple):

fn move_avatar(delta: (Int, Int)) { ... }

move_avatar((5, -3))

Guards Over If-Else Chains

Guards read top-to-bottom: the first true condition yields the value. They remove nesting, flat, scannable logic, make intent explicit, and gives a clear default with _.

Avoid (if-else ladder as an expression):

fn classify(a: Int, b: Int) -> Int {
    if a > 3 {
        4
    } else if b < 9 && a > 4 {
        99
    } else {
        0
    }
}

Prefer (guards):

fn classify(a: Int, b: Int) -> Int {
    | a > 3              -> 4
    | b < 9 && a > 4     -> 99
    | _                  -> 0
}

Only the first matching guard runs.

Branch Ordering

Branches are not equal — put the common one first.

A hot branch is the case that happens most often at runtime. A cold branch is rare — the fallback, error, or unusual case.

Write the hot branch first in an if, match, or guard (|). Put the uncommon cases later, and the default (else, _) last. This makes the code read naturally — common → less common → rare → default — and helps both the compiler and the CPU keep the hot path fast.

It also helps future readers, since the most common path becomes immediately clear.

fn classify(a: Int, b: Int) -> Int {
    | a > 3          -> 4     // hot
    | b < 9 && a > 4 -> 99
    | _              -> 0     // cold
}

The same principle applies to if/else chains and match arms: put the common case first, the default last.

Use power-of-two sizes

Whenever possible, design data structures and sizes to be a power of two: 2, 4, 8, 16, 32, 64, 128, …

Why is this so effective?

  • Fast wrap-around: i = (i + 1) & (SIZE - 1) replaces modulo with a single bitmask.
  • Cheap alignment: Power-of-two sizes align naturally with hardware caches and DMA transfers.
  • Simpler code: No need for slow /, just shifts and masks.
  • Hardware Synergy: Power-of-two sizes often align data perfectly with the CPU’s cache lines and DMA transfer requirements.
  • Simpler & Faster Math: Many calculations become simpler. For example, dividing by a power-of-two can be replaced with a much faster bit-shift operation (>>).

What if you can’t use a power of two?

  • Conditional Reset: For simple counters that increment by one, an if statement is often clearer and faster than a modulo.

    frame = frame + 1
    if frame >= 10 {
        frame = 0
    }
    
  • Subtractive Loop: If the input value is expected to be close to the limit (e.g., wrapping tile coordinates), a simple loop can be effective. However, be cautious, as this can be slow if the value is large.

    mut offset = x
    while offset >= 30 {
        offset -= 30
    }
    
  • Explicit Division: If you must perform a true modulo or division, use Swamp’s explicit .div() or .rem() intrinsics. This makes the computational cost visible in your code and reminds you to keep it out of performance-critical inner loops.

Rarely use division

This might seem like a strange suggestion, but surprisingly division is still one of the slowest basic operations in a CPU. On modern x86, a multiplication may take ~3 cycles, while a division can be 20–30 cycles (7-10x slower). On M1 the ballpark for multiplication is 3 cycles, and integer division is 9-14 cycles (about 3x slower). On retro CPUs like the GBA’s ARM7TDMI, integer division has no hardware instruction at all and must be emulated in software — which means about 50–100x slower than a multiply.

That difference is huge in gameplay code, where tight loops run thousands of times per frame.

Unlike other arithmetic, division has a fatal edge case: division by zero. This doesn’t just produce a weird number, it causes a panic that crashes the program.

The remainder operation (%) is just as slow as division because it is fundamentally part of the same calculation (basically a sdiv with a msub)

Division loses information

Unlike multiplication, which preserves all bits of its result until overflow, division is destructive.

Integer division truncates (rounds toward zero) and throws away the remainder.

That means division isn’t just slower — it’s also less predictable. The cost and the result are both things you should be aware of when you write gameplay code.

When do you actually need division?

In practice, most gameplay uses of division/modulo fall into just a handful of patterns that have faster and clearer alternatives:

  • Wrap-around counters (animation frames, cycling lists) → use an if with a reset.

  • Odd/even checks → use bitwise AND (i & 1).

  • Indexing with power-of-two sizes → use a mask (i & (size-1)).

  • Scaling by constants → use multiplication by a reciprocal (often precomputed as fixed-point).

For the rare cases where you really need “true division,” Swamp requires an explicit .div() intrinsic. That way, you know you’re paying the cost, and you can consciously keep it out of inner loops.

SoA Future-Proofing

Swamp will support SoA natively in the future, but you can design today so you benefit now. The idea: keep your high-level game state ergonomic (AoS), but process performance-critical paths over parallel slices (SoA shape).

When to use SoA

  • You operate on many elements per frame.

  • The loop is hot (runs every tick) and does the same operation across elements.

  • You only need a subset of fields (e.g., position/velocity), not full structs.

  • Memory access should be sequential and predictable (good for caches).


const TIME_PER_TICK: Int = 1 << 16 // 1.0

// caller fans out of AoS, in the future there will be real SoA buffers.
fn integrate_positions(
    position_xs: [Int],
    position_ys: [Int],
    velocity_xs: [Int],
    velocity_ys: [Int],
) {
    n = position_xs.len
    // assert(len equality across slices)
    for i in 0..n {
        position_xs[i] += (velocity_xs[i] * TIME_PER_TICK) >> 16
        position_ys[i] += (velocity_ys[i] * TIME_PER_TICK) >> 16
    }
}

Mind the Stack

Stack space is small and precious.

Swamp stacks are deliberately small for speed and predictability. That means you must budget your stack use carefully:

  • Keep call depth flat → avoid long chains of function calls. Inline or restructure when practical.

  • Watch local sizes → large structs and fixed-capacity collections can exhaust stack space quickly.

  • Keep bulk storage out of the stack → store big collections as part of your game state, not as temporary locals.

  • Think capacity, not just type → a collection with capacity 256 is heavy even if it’s empty at runtime.

The goal is to make stack usage bounded, predictable, and cache-friendly. Every function should run safely within Swamp’s intentionally small stack size.

Large scratch buffers belong in arena/state, not the stack.

Comment with purpose

Comments explain why, code shows how.

Explain intent, constraints, and trade-offs. The code already shows what happens and how — don’t narrate it.

  • Use /// Markdown doc comments just before types and functions.

  • Use //! for module/package files (e.g., lib.swamp, main.swamp).

  • Avoid line-by-line // inside functions (except for TAGS, see below); reserve // for scope headers or block notes (e.g., a { ... } or with block).

Unimplemented:

Scanning and parsing of Markdown doc comments is not implemented yet.

Write doc comments when the name + signature aren’t self-explanatory. If a function is short and obvious, prefer clear names over doc comments.

State invariants should be asserts and not comments.

Avoid (narrating):

fn tick(mut avatar: Avatar) {
    // increase stamina
    avatar.stamina += 1

    // if alive then move
    if avatar.health > 0 {
        // move right
        avatar.pos.x += 1
    }
}

Prefer (just code):

fn tick(mut avatar: Avatar) {
    avatar.stamina += 1

    if avatar.health > 0 {
        avatar.pos.x += 1
    }
}

Function Doc Comments

First paragraph should be a short summary of what the intent is, usually without (extensive) markdown.

You can add sections with #. Subsections ##, ### are possible but rarely needed.

You refer to parameters with `parameter` and types with `module::sub_module::Name`

Code blocks are wrapped in ```, and default to Swamp.

/// Updates the avatar simulation.
///
/// # Parameters
///
/// - `self`: the `shared_types::Avatar` to be simulated.
/// - `game_grid`: grid used for movement and collisions.
///
/// # Effects
///
/// Increases stamina; moves only when alive.
///
/// # Example
///
/// ```
/// mut avatar = Avatar { .. }
/// avatar.tick((10, 20), game_grid)
/// ```
fn tick(mut self, game_grid: Grid) {
    ...
}

Tracking Tags

Use tags for actionable work items — short, imperative, and easy to grep.

Semantics:

  • TODO: planned work that’s safe to defer for later (feature, improvement, refactor, docs, tests).

  • HACK: Intentional workaround that violates the ideal solution for a specific reason (deadline, demo, dependency). Should include a removal plan.

  • FIXME: something is wrong now (bug, correctness/safety issue, crash, broken invariant). Bugs should generally be fixed right away, but it is decided to be temporarily deferred (e.g., lower priority, blocked, or awaiting info).

  • BUG: known defect/limitation with unique id, often cross-cutting and almost always tracked in external bug tracker.

  • NOCHECKIN: temporary commit/merge blocker. Any change containing it must not be committed, pushed, or merged; pre-commit hooks and CI should fail when it’s present. Use it to fence off local test code, WIP refactors, or temporary hacks that aren’t meant to ship.

The tag format is TAG(optional-info)[optional-category]: message:

  • TAG is TODO, FIXME, HACK or BUG. for local use: NOCHECKIN.

  • optional-info (in parens) can include issue IDs, owners, dates: (#233,@piot,2025-08-12)

  • optional-category (in brackets) is a short bucket like: [perf] [safety] [refactor] [docs] [test]

  • Prefer one category; add more only if it truly helps.

Good message style:

  • Imperative: “avoid…”, “add…”, “split…”, “bounds-check…”

  • Specific condition: “when count == 0”

  • One concern per tag.

// TODO: improve jump feel by decreasing gravity
// TODO(#233,@piot): maybe get achievement if watching the credits
// TODO(#501,@catnipped)[docs]: add `# Example` for `Position::create_spawn_from_id`
// TODO(#612)[perf]: fuse the two passes in damage calculation loop
// FIXME: when more than 64 units are spawned it panics
// FIXME(#612)[correctness]: `ZII` violated - zero `SpellId` triggers effect
// FIXME(@piot,2025-09-12)[safety]: negative health possible after multi-hit

Package Doc Comments

//! # Avatar Package
//! Handles movement and actions for the Avatar

TL;DR — The Swamp Way

Bullet points

  • Single assignment → prefer expressions (if, match, guards |) over mutation.

  • ZII (Zero Is Initialization) → zero means “nothing” and is always safe; reserve 0 for empty/none, start IDs at 1.

  • Rest operator (..) → fill only what matters, zero the rest.

  • Max four parameters → group extra arguments into a struct.

  • Functions should be meaningful → function calls are expensive; avoid tiny single-use helpers. Use scopes for structure, and keep functions for reusable, non-trivial work. (functions should often be larger than you might be used to)

  • Associated functions → put behavior with the type it belongs to.

  • No defensive coding → fail fast with asserts instead of silently fixing bad state.

  • Compile time over runtime → bake in constants; runtime is for unknowns.

  • Code is data → bake known data as constants instead of loading at runtime.

  • Return values instead of mutating parameters → builders should build, not patch.

  • No strings in game code → use enums, IDs, or numbers; use strings only at the very last edges (UI, logging).

  • Type inference → infer the obvious; only very rarely annotate when it clarifies intent.

  • Tuples for small, obvious groups → otherwise use a struct with names.

  • Guards | over if-else ladders → flat, top-to-bottom logic with a clear default.

  • Branch ordering → put the common case first, default last.

  • Use power-of-two sizes → enables masks and shifts instead of modulo.

  • Avoid division → use masks, shifts, or reciprocal multiply; keep .div()/.rem() out of hot loops.

  • SoA in hot loops → for large, uniform per-frame work, process parallel slices; keep AoS at the edges.

  • Comment with purpose → explain why, not what.

Solved by the Design of Swamp

Some performance pitfalls are so common that Swamp eliminates them entirely by design. You don’t need to work around these issues — the language guarantees they can’t happen.

Natural Alignment

All fields are naturally aligned. Packed or misaligned layouts are not allowed. You always get fast, single-instruction loads/stores.

Fixed Capacity

All collections in Swamp are fixed capacity. There are no hidden allocations, no reallocation churn, and no unpredictable memory growth in the middle of a frame. You allocate once up front, then reuse forever.

No Floats

Swamp uses fixed-point type and arithmetic only. This guarantees determinism across platforms and avoids the cost and inconsistency of floating-point math (especially on older ARM CPUs without hardware FPU).

Single-Threaded

This guarantees deterministic execution order. Threads may be used for background I/O or asset loading at the host (engine) level, but game code runs on a single tick loop.

Single-Pass, Acyclic Modules — No Recursion by Design

Swamp modules are evaluated top-to-bottom in a single pass, and the module graph is acyclic (no cyclic dependencies). A function can only call symbols defined above it or in earlier modules.

What this means

  • No self-calls / no forward calls: a function can’t call itself inside its own body (it isn’t defined yet), and it can’t call functions defined below.

  • No mutual recursion: with an acyclic module graph, A → B and B → A can’t both hold.

  • Together, the call graph is a DAG — recursion isn’t expressible. Use loops (or explicit stacks/queues) instead.

Why this is good (beyond speed)

  • Predictable initialization order: definitions flow one way; no “who runs first?” puzzles.

  • Improved code organization: modules naturally layer from low-level → high-level; no spaghetti cycles.

  • Simpler tooling: single pass resolution makes builds and analysis more straightforward.

  • Determinism by default: no hidden call cycles; easier to reason about cost and control flow.

What Is Recursion?

Recursion is when a function calls itself as part of its own definition. Each call pushes a new frame on the stack, and the function won’t finish until all recursive calls return.

A classic example is computing factorial:

fn factorial(n: Int) -> Int {
    if n == 0 {
        1
    } else {
        n * factorial(n - 1) // the function calls itself
    }
}

Why avoid it?

On both retro and modern CPUs, a function call is expensive:

  • Arguments must be shuffled into ABI registers. With nested calls, intermediate results usually can’t go straight into the right registers; they’re stored in temporaries first, then moved again for the next call.

  • Callee-saved registers may need to be spilled and restored.

  • The link register is saved and restored.

  • The CPU pipeline must branch out and back.

With recursion, you pay this overhead again for every level, and most recursive functions are small — meaning the call overhead dominates the actual work.

On top of that, each recursive call consumes additional stack memory. Swamp stacks are by design deliberately small — for both speed and predictability. This guarantees bounded memory use and keeps stack data hot in cache. But it also means deep or unbounded recursion can overflow the stack quickly. The only workaround is to reserve a very large stack up front — which wastes memory and hurts cache performance.

Why Swamp Disallows Function Pointers

First-class function pointers (or arbitrary indirect calls) would re-open doors Swamp deliberately keeps shut:

  • They can reintroduce recursion indirectly (cycles through tables/dispatch).

  • They hide call targets, blocking inlining and making call cost unpredictable.

  • They complicate determinism, profiling, and cache behavior (unpredictable I-cache, worse branch prediction).

Swamp keeps call targets static and visible so the compiler (and you) can reason about cost, layout, and determinism. If indirection is ever needed, prefer explicit enums + match or tables of data, not tables of code.

No Global Mutable State

Swamp provides constants only; there is no global mutable memory in language code. This guarantees determinism and eliminates hidden aliasing.