Every bounded language faces the same bookkeeping problem: the interpreter is supposed to stop a runaway program, but the program’s attempt to run away is itself work the machine already did. A naive fuel counter that only charges successful steps can be gamed: a policy that tries a thousand impossible actions pays for none of them. Platonik’s cost model, built around the Meter and Cat types in crates/platonik-core/src/sim.rs, is a small, complete answer to the question of how to charge for everything, including the failures.
two caps, not one
The meter enforces two independent ceilings, and the layering is the design. A whole-experiment fuel budget caps the run: at most MAX_FUEL = 2_000_000 units across every tick, cell, and case. Inside it, a per-activation window caps any single cell’s turn: activation_fuel between 1 and 1,024 units under protocol versions 1–3, raised to MAX_VARIATION_ACTIVATION_FUEL = 16_384 under v4. The global cap says the world cannot run forever; the activation cap says no individual creature may hold the floor.
Meter::charge checks both on every unit:
pub(crate) fn charge(&mut self, category: Cat, amount: u64) -> Result<(), Stop> {
for _ in 0..amount {
let total = self.costs.total();
if total >= self.fuel {
return Err(Stop::Fuel);
}
if self.activation.is_some_and(|(start, limit)| total - start >= limit as u64) {
return Err(Stop::Activation);
}
// increment the category counter
}
Ok(())
}
Each unit is debited one at a time against both limits before the counter moves, so a charge can never overshoot: a ten-unit sensor sweep that hits the cap at unit seven fails having honestly paid for seven. begin_activation snapshots the running total when a cell’s turn starts and end_activation clears the window; the per-activation limit is defined as charged work within the turn, which means a cell pays for its own rule scan, its sensors, its memory traffic, and its attempted action out of the same allowance.
The two stop conditions have different physics and different consequences. Stop::Fuel means the experiment is out of money: the tick halts mid-frame, the run records FuelExhausted, and the partial frame is preserved. Stop::Activation means one cell exceeded its window: its state changes roll back entirely, the mission is marked ActivationLimit, and later cells still get their turns. An activation fault is a foul on one player, not the end of the game, though it does end the mission’s claim to a clean pass, since Outcome::passed requires status == Complete.
the ledger is the language
Thirteen categories share one ledger, declared as Cat in sim.rs and mirrored as public fields on Costs in model.rs: loading, scheduling, conditions, sensors, memory_reads, memory_writes, actions, messages, transfers, checking, draining, copying, construction. They are deliberately orthogonal: a run’s receipt doesn’t only say “2,412 units”; it says where the units went, which is what makes two programs with the same total interesting to compare.
Two design choices carry most of the weight. First, loading is a charged cost: before tick one, the meter debits Loading one unit per byte of the experiment’s canonical serialization: the serialized Experiment is hashed, measured, and billed, so a bigger program is more expensive before it runs once. This is what turns “canonical program bytes” into a real axis of competition rather than a style preference; the challenge leaderboard’s third tiebreak is exactly this quantity, and it works because loading was already in the ledger.
Second, interpreter overhead is modeled, not hidden. Scanning a rule costs Checking. Evaluating a condition costs Conditions plus either Sensors or MemoryReads depending on what it touches. Scheduling costs one unit per tick and one per activation. Even the engine’s own end-of-tick bookkeeping (verifying spark conservation and the beacon ledger) is charged Checking proportional to the live population before the invariants run. The meter does not pretend the referee’s work is free; it prices the referee too, which keeps the honest accounting honest when cells multiply.