Roadmap
Future plans and development roadmap for Swamp
Roadmap
- Truly Random Resource Ids.
@8fad1938orpath/something/elsethat looks up the id during compile time. - Implicit projection (Desugar Embedded) Types
- Shared memory pools (maybe?)
- SoA support
Resource IDs
- Resource Ids.
@8fad1938orpath/something/elsethat looks up the id during compile time. - When working in a bigger team, those ids can be randomly generated if needed.
Remove/erase keywords in collection loops
Be able to safely remove elements from a collection. Compile lowering will know how to change the index to not miss any iterations (if it is swap-removed, stay on the same index)
for space_ship in ships {
if ship.x < -10 {
remove space_ship // removes the space_ship in a safe way
}
}
Named function arguments
Be able to specify function arguments by name. Evaluation order: for ordered arguments it is left to right, but for named arguments I guess it is also left to right?
fn my_function(health: Int, damage: Int, modifier: Int) {}
my_function(health: 10, modifier: 10, damage: 5) //don't need to remember the order
my_function(10, 5, 10) //if not using the field names, need to be correct order
Parser is already handling it and putting it in AST, so it is prepared for later phases.
Suggested by @catnipped
ZII arguments (rest operator)
can use rest .. operator in function arguments, both for named and normal arguments
my_function(health: 10 ..)
my_function(10, ..)
will be desugared into zero for each argument not specified:
my_function(health: 10, modifier: 0, damage: 0)
Suggested by @catnipped
Implicit projection (Desugar Embedded) Types
you don’t have to type the name of the contained types, when it can be inferred from the type. There is no overhead at all:
the syntax is far from decided, this is just to communicate the idea. proposed keyword embed
struct Position {
x: Int,
y: Int,
}
struct MovementLogic {
speed: Int,
is_jumping: Bool,
}
fn print_position(position: Position) {
print('pos: {position}')
}
struct Monster {
embed position: Position,
embed movement: MovementLogic,
}
monster = Monster { position: Position { x: 10, y: 20 } }
print_position(monster) // this works since it knows the type, is desugared to: `print_position(monster.position)`
Intra-region references
“Same parent idea”
Goal: Allow raw &T fields when the compiler can prove the pointee lives long enough and shares the same memory parent (memory region, or allocation in scratch/arena).
-
Useful when you want to iterate through things of the same type (e.g. positions). And also if the number of things can vary a lot and you don’t want to allocate the worst-case in each type (e.g. storing 8 positions in each Monster)
-
Avoid worst-case duplication: Instead of storing multiple Positions inside every Monster “just in case,” store references to a shared positions collection. Memory scales with the actual number of positions, not a per-monster worst case.
-
Fast iteration & cache locality: Keep each type in its own contiguous block (e.g., a SoA/SparseSet for positions). You can iterate positions in tight cache-friendly loops while monsters keep raw
&Positionto the ones they need. -
(Almost) Zero-cost access
Intra-region references are real pointers — no handles, no lookups, no reference counting. When the compiler can prove safety, dereferencing is as cheap as reading a field.
The only practical cost is the usual cache miss when following a pointer to a different memory area, but there’s no runtime overhead or indirection beyond that. As long as the referenced data is laid out contiguously, locality stays good and performance remains predictable.
struct Monster {
attack_position: &Position, // inserts lookup information (id) to a position. can only be set if compiler can verify that it is secure
target_position: &Position, // inserts lookup information (id). can only be set if compiler can verify that it is secure
}
struct World {
monsters: [Monster; 256],
positions: [Position; 256],
}
impl World {
fn create(mut self) -> Monster {
self.monsters[1] = Monster {
position: &self.positions[23], // this position pointer is safe to store in monster, since they share parent (`self` is allocated in the same contiguos memory region)
}
}
fn update_all_positions_in_game(mut self) {
for mut pos in self.positions {
pos.x += 1
}
}
}
Shared Memory Pools Pointers (maybe? unsure)
Description:
- A pool is a ’static, non-moving region of T slots (Sparse slots).
- A field like position: Pool(Position)? holds a permanent, non-movable, non-copyable pointer to one slot.
remove(): frees the slot and clears the field to null.
After that, the pool may reuse the slot for someone else.
the syntax is far from decided, this is just to communicate the idea.
pool cool_positions: [Position; 512] // the pool is always alive
struct Monster {
attack_position : Pool(cool_positions) , // internally keeps a pointer to an element in `cool_positions`
target_position : Pool(cool_positions), // internally keeps a pointer to an element in `cool_positions`
}
fn spawn_a(mut a: Monster) {
a.position = positions.alloc_slot() // binds pointer to a.position (until remove() is called)
a.position.x = 10
}
fn despawn_a(mut a: Monster) {
a.position.remove() // frees slot, field pointer is set to zero
}
Call Guard
A call guard is a boolean expression that controls whether a function executes. The guard is checked at each call site before the function is invoked. If the condition fails, the call is skipped entirely — no arguments are evaluated, no registers are saved or restored, and the function body never runs.
Syntax
Guards are declared using the | operator after the function parameters:
// Only cares about Elite Celestial Wizards
fn enemy_defeated(evt: EnemyDefeated, mut loot: LootTable)
| evt.enemy == CelestialWizard && evt.is_elite {
loot.spawn_rare(evt.position, ItemId::AstralGate)
}
Why Use Call Guards?
Clarity: The precondition is declared in the function signature, making it immediately obvious when the function will run. No need to dig through the implementation to understand the entry requirements.
Performance: Guards are desugared at compile time into conditional checks at each call site. Failed guards skip the function call entirely, eliminating overhead from argument evaluation and register saving/restoring.
How It Works
At compile time, each call site is desugared into a guarded invocation:
// Original call
enemy_defeated(evt, &table)
// Desugared to
if evt.enemy == CelestialWizard && evt.is_elite {
enemy_defeated(evt, &table)
}
This transformation happens at every call site, ensuring zero runtime dispatch overhead.
Parameter Field Binding
When a struct parameter is declared without a name, its fields are automatically bound as local variables:
fn check(PlayerSpawned) {
// PlayerSpawned fields (health, team, etc.) are directly accessible
if health > 25 {
print('strong player with health {health}')
}
}
This is purely syntactic sugar. The function still receives the full struct; the compiler simply generates field access code for you. It’s equivalent to:
fn check(evt: PlayerSpawned) {
if evt.health > 25 {
print('strong player with health {evt.health}')
}
}
Static Event Dispatch
Event Rules (Handlers)
Event handlers (also called rules) are defined using the on keyword instead of fn. The compiler tracks all on functions that listen to a specific struct type (the first parameter) and generates a dispatch function for each event type. You don’t call event handlers directly — they’re invoked through the generated dispatch function.
Event handlers commonly combine Call Guards for conditional execution and Parameter Field Binding for concise field access.
enum Team {
Red
Blue,
}
struct PlayerSpawned {
health: Int,
team: Team,
}
// fields of `PlayerSpawned` are automatically bound to variables
// in almost all cases you need one or more context parameters, but it
// is optional.
on player_spawned(PlayerSpawned) {
print('a player spawned in with health {health}')
}
// fields of PlayerSpawned` are automatically bound to variables
on healthy_blue_player_joins(PlayerSpawned)
| health > 10 && team == Blue -> {
print('a healthy blue player spawned in with health {health}')
}
// explicit name for the event; access fields
// normally (evt.team, evt.health)
on cool_red_player_enters(evt: PlayerSpawned, mut battle: Battle)
| evt.health > 45 && evt.team == Red -> {
print('a healthy red player spawned in with health {evt.health}')
battle.message("strong player joined for team red!")
}
Emit Events
Events are dispatched using the emit() macro, which triggers all matching event handlers (rules). The compiler automatically generates the appropriate dispatch function based on the event type.
The emit macro is replaced at compile time with a call to the generated dispatch function, e.g. __emit_player_spawned(player_spawned, battle)
player_spawned := PlayerSpawned { health: 20, team: Blue }
// sends the event to all matching handlers
// internally this will be replaced with:
// __emit_player_spawned(player_spawned, battle)
emit(player_spawned, battle)
Lowered at Compile Time
At compile time, event handlers are lowered into a specialized dispatch function. Guard expressions become conditional branches, ensuring zero runtime overhead for routing the events — all dispatch logic is resolved statically.
fn __emit_player_spawned(evt: PlayerSpawned, mut battle: Battle) {
// guards from the rules are lowered to if statements
if evt.health > 10 && evt.team == Blue {
// context omitted since this specific rule doesn't use it
healthy_blue_player_joins(evt)
}
if evt.health > 45 && evt.team == Red {
// this rule required the battle context
cool_red_player_enters(evt, &battle)
}
some_event_handler_with_no_guard(evt)
// optional host call for notification
host_call(evt, context)
}
Intercept any Function
Inspired by @catnipped, this feature enables intercepting any function for event dispatch. The function must have a clear first parameter that is a struct (not counting self). self will serve as an automatic context. A potential issue might be when mut is needed by the event handler but not used by the function itself.
At compile time (or when patching the .swim file for Marsh VM), a dispatch code block is inserted before the function body.
Mark functions to intercept as events using a keyword. The syntax is not clear at this time, assume it is an intercept keyword for now:
intercept some_game::logic::Battle::spawn_unit
The spawn_unit() function is then patched with a dispatch block at the start:
fn spawn_unit(mut self, unit_info: UnitInfo) {
// Injected dispatch block
{
if unit_info.type == SpaceWizard && unit_info.faction == Friendly {
// context parameter omitted since this rule doesn't use it
user_defined_rule_name(unit_info)
}
if unit_info.starting_health > 45 && unit_info.faction == Enemy {
// this rule requires the self context
another_user_defined_rule_name(unit_info, self)
}
}
// original code:
...
}