Rationale
Design motivation and tradeoffs behind the Swamp language
Why No Block Comments
Swamp intentionally supports line comments (//) and documentation comments (///), but does not support block comments.
The main reasons are practical:
-
Nested block comments are hard to reason about when comments contain commented-out code: is nesting allowed, what depth are you at, and is this code active again?
-
Version control diffs and merges become harder to inspect when code moves in or out of block-commented regions.
-
Long commented regions are easy to miss while scrolling, making it unclear whether you are reading code or commented text.
-
They encourage keeping large regions of dead code instead of deleting them.
-
Most editors already support toggling
//across multiple lines, so block comments are of little practical benefit.
Why Fixed-Capacity Collections
Swamp uses fixed-capacity collections because it targets deterministic systems, especially games and real-time servers.
In practice, compile-time capacity gives several advantages:
- Zero runtime allocation overhead, since memory is reserved up front.
- Predictable memory usage, because the compiler knows the maximum size.
- No garbage-collection pauses or allocator hiccups during runtime.
- No heap fragmentation from growing and shrinking collections.
- Better cache locality from contiguous storage.
- Faster iteration from contiguous traversal with no pointer chasing.
- Easier inspection, debugging, and serialization.
- A stronger fit for lockstep, replay, and binary-oriented networking.
The downside is that you must budget memory explicitly for each collection, especially those stored in application state. In Swamp, that explicitness is a feature, not a limitation.
Why Bits
The bits construct is for cases where several small fields can share a single compact integer representation.
This is especially useful for:
- Cases where you need to optimize memory footprint.
- Direct mapping to hardware registers.
- Network or file formats with fixed binary layouts.
- Many tiny flags would otherwise waste space.
Why Resource IDs
Resource IDs replace fragile string-based asset references with compile-time-checked identifiers.
In practice, this gives several benefits:
- Typos are caught at compile time instead of turning into runtime asset failures.
- Missing assets fail the build instead of shipping broken content.
- Asset lookups can compile down to compact numeric identifiers.
- Refactors are safer because missing and unused assets are surfaced early.
In short, Resource IDs move asset validation from runtime to compile time, with feedback in the editor as you type.
Why No Return Keyword
An early return is a jump: it exits the function immediately and bypasses everything below it. Dijkstra’s “Go To Statement Considered Harmful” argument applies just as well to early returns as to goto.
In Swamp, execution always reaches the end of every block. This has two practical consequences: finalization work runs exactly once, and in longer functions with inlined sequential blocks, no block is silently skipped by an exit buried in an earlier one.
Example: Finalization Duplication
When a function must always respond to a client and write to the replay log, early returns force those calls to be duplicated at every exit point. In Swamp the result accumulates through branching and finalization runs once:
fn handle_action(mut session: Session, action: Action) -> ActionResult {
mut result := ActionResult::Ok
if !session.player.alive {
result = PlayerDead
} else if !session.player.can_afford(action.cost) {
result = InsufficientResources
} else {
session.player.apply(action)
}
// Always runs: respond to client and record for replay
session.send_response(result)
session.replay_log.record(action, result)
result
}
With early returns the same finalization must be copied to every exit site:
fn handle_action(session: &mut Session, action: &Action) -> ActionResult {
if !session.player.alive {
session.send_response(ActionResult::PlayerDead);
session.replay_log.record(action, ActionResult::PlayerDead);
return ActionResult::PlayerDead;
}
if !session.player.can_afford(action.cost) {
session.send_response(ActionResult::InsufficientResources);
session.replay_log.record(action, ActionResult::InsufficientResources);
return ActionResult::InsufficientResources;
}
session.player.apply(action);
session.send_response(ActionResult::Ok);
session.replay_log.record(action, ActionResult::Ok);
ActionResult::Ok
}
Every new branch requires remembering to add the finalization calls again. Forgetting one is a silent bug: the client gets no response, or the replay log is missing an event.
Example: Inlined Function-Like Blocks
Swamp encourages inlining sequential passes directly into a function to avoid call overhead and makes it easier to refactor and iterate on. Because there is no return, every block is guaranteed to run each frame:
fn update(mut game: GameState) {
// Bots: decide intent for each entity
for mut entity in game.entities {
if entity.is_bot {
entity.intent = entity.compute_intent(game)
}
}
// Physics: apply movement and resolve collisions
for mut entity in game.entities {
entity.position += entity.intent.velocity * game.dt
entity.resolve_ground()
}
// Spawning: process pending spawns and despawns
for event in game.spawn_queue {
game.spawn_entity(event)
}
for entity in game.entities {
if entity.marked_for_removal {
game.despawn_entity(entity)
}
}
}
With early returns an error path inside one block can silently skip every subsequent block for the whole frame:
fn update(game: &mut GameState) {
// Bot block
for entity in &mut game.entities {
if entity.is_bot {
let Some(intent) = compute_intent(game, entity) else {
return; // physics and spawning never run this frame
};
entity.intent = intent;
}
}
// Physics block
for entity in &mut game.entities {
entity.position += entity.intent.velocity * game.dt;
resolve_ground(entity);
}
// Spawning block
for event in &game.spawn_queue {
spawn_entity(game, event);
}
for entity in &mut game.entities {
if entity.marked_for_removal {
despawn_entity(game, entity);
}
}
}
The return might be written with the intent of bailing out of only the Bot block, but it exits the whole function. Physics and spawning stop running for the entire frame, producing subtle desyncs and missed despawns that only surface when compute_intent fails.
Why Minimal Core Library
Swamp keeps the core library intentionally small. Functions are only included if they meet one of three criteria:
Performance Over User Space
The built-in version must be meaningfully more performant than implementing it manually. In practice this means the operation compiles down to a handful of assembly instructions, making the built-in version faster by avoiding function call overhead.
Examples:
-
Included:
abs,floor,sin— each maps to a single CPU instruction or compact instruction sequence -
Excluded:
sqrt— in games you compare squared distances instead; when you do need it, it’s a conscious performance decision, not a core primitive -
Excluded:
pow(x, y)— general exponentiation is a complex library function (exp(y * log(x))); specific cases likex * xare written inline
Reduce Common Boilerplate
Some operations are no faster as built-ins, but prevent duplication of common patterns. These are included when they appear repeatedly across typical game code.
Examples:
-
Included:
filter(),any(),map()on collections — equally fast as manual loops, but iterations over arrays are very common in game logic -
Included:
clamp,min,max— trivial to implement, but appear constantly in game math
Cannot Be Implemented in User Space
Some operations require compiler or runtime support and cannot be written by the user.
Examples:
- Included: Type conversions (
int.float(),float.string()) — conversion semantics are defined by the type system - Included:
rnd()— requires deterministic noise algorithm for procedural generation
Why This Matters
A minimal core keeps the language:
- Maintainable: Fewer built-ins means less code to port, test, and optimize for each platform
- Understandable: A small surface area is easier to learn and reason about
- Portable: Targeting new architectures or embedded systems requires implementing fewer primitives
- Predictable: Every built-in carries its weight; nothing is there “just in case”
- Lightweight: A small core stays out of your way; the language doesn’t feel heavy or impose unnecessary opinions
- Flexible: When the core doesn’t prescribe solutions, users are free to build what fits their needs instead of working around built-in assumptions
A bloated standard library makes the language feel restrictive: every built-in function is a choice that was made for you. A minimal core trusts you to know what your game needs.
When in doubt, leave it out. Libraries can provide convenience, but the core should stay lean.