hraness

designing a vm cost model: instruction pricing for a bounded language

fuel, budgets, and the price of every step

Drafted by an AI agent at Ben Guo's direct request from the Hraness source repositories, and checked against those sources before publication.

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.

failure is not free

The load-bearing rule is in policy.rs: an activation executes into a clone of the world state and commits that clone on success, and also on ordinary action failure. A pickup on an empty source or a move into a wall returns an error, records the failed attempt in the frame, and commits. What rolls back is only a limit fault: fuel or activation exhaustion discards the clone, but the meter, which is not part of the clone, keeps every unit the doomed attempt already spent.

So failed work is real work twice over: it is charged, and it is visible. The trace records each activation’s error, its position_before and position_after, and its work_before/work_after span. In the ark diagnostic this produced the quietly damning observation that the frugal crew’s winning run still paid for eighty-eight failed source_empty pickups: the ledger kept the receipt of every lunge at an empty shelf. “Failed actions still consume their declared execution fuel,” as docs/engine.md puts it, is the rule that makes conservative policies cheaper than reckless ones measurably, not morally.

the full price list

Here is where every category is charged, traced through policy.rs, sim.rs, and construction.rs:

Category Charged for Where
loading Every byte of the canonical experiment JSON, once, before tick 0 sim.rs run_through
scheduling 1 per tick plus 1 per cell activation tick preamble, activate
conditions 1 per condition evaluated, until a rule’s first false check_condition
sensors Non-memory conditions; take_message; reading a BitSource::Message check_condition, read_bit
memory_reads memory{slot,value} conditions; BitSource::Memory check_condition, read_bit
memory_writes write_memory, remember writes, take_message store, clear_memory events (4 units) execute, event handling
actions 1 per attempted action; 1 per applied world event execute, event handling
messages Per link scanned on send, per delivered/expired signal, per take_message emit, delivery, expiry
transfers Successful move, turn, pickup, depot drop, and each beacon credit execute, credit
checking Rule-scan steps, blocked probes (1 + closed_edges.len()), action guards, delivery bookkeeping, events, end-of-tick invariant reserve (cells + beacons + sparks + closed edges + construction state) throughout
draining 1 per beacon drain that actually fires drain phase
copying 1 per copied body byte during construction (32 bytes per build step) construction.rs
construction Placement, staged wiring, activation of an assembly construction.rs

Notice how checking behaves like a congestion charge: probing a blocked direction costs one plus the number of already-closed edges, so the price of a sense grows with the amount of declared damage the world carries. The meter prices the work the engine does, not the work a textbook would assign.

Three fine points repay attention. BitSource reads are itemized: a send carrying a constant bit pays Checking, from memory pays MemoryReads and copies the register’s spark evidence provenance, from the inbox pays Sensors and fails with no_message if the port is empty. route composes its charges: read the bit, Checking for the valve lookup and adjacency, then Transfers for the actual depot-to-beacon credit, the same physical delivery a drop performs, billed through the same credit function. And event application clones state per event: each declared link_enabled, valve_enabled, clear_memory, or edge_blocked pays its own Checking/Actions/MemoryWrites before mutating, so a crowded event schedule is itself a budget line.

what the meter does not measure

The documentation is emphatic about the boundary, because the whole epistemics of the project rests on it. Modeled work is a deterministic game economy: it is not wall-clock time, not host CPU, not physical energy, and not the external agent’s token bill. docs/rust-bridge.md states it plainly: “Modeled execution fuel is distinct from sparks, code length, memory, wall-clock time, and an external agent’s tokens.” The checker that re-executes a receipt consumes real CPU the meter does not see; the meter’s numbers are the same on a fast machine and a slow one, which is the entire point of a replayable ledger.

This separation produced one of the repository’s best pieces of evidence hygiene. The first agent diagnostic counted logical evaluations (68) separately from actual engine executions (2,572), against a predeclared ceiling of 4,096. The gap is exactly the unglamorous work a scoreboard hides: receipt verification, history replay, recovery probes, and capacity measurement, each command emitting an EXECUTIONS counter (sim.rs keeps it as process-local telemetry, explicitly outside the deterministic receipt). A study that billed only its 68 headline trials would have understated its compute by a factor of thirty-eight.

The loading category earns a second mention here because it resolves a real design tension. The ports evaluation compared candidate programs head-to-head and found the smallest submitted bundle lost on total work: it saved 840 loading units but spent 918 more during execution. A meter that charged only execution would have crowned the wrong program; a meter that charged only size would have missed that the savings were real but insufficient. Keeping loading and actions in separate columns is what let the result be stated as a measured tradeoff instead of a slogan about minimalism.

cost models as game design

The deeper lesson is that a cost model is a hypothesis about what should be expensive. Platonik’s choices (sensing is charged per probe, movement per transfer, messaging per link scanned, construction per byte and per stage, refereeing per inhabitant) encode a physics where locality and restraint win. Different prices would select different organisms. The complexity-and-scale doc makes the corollary explicit: an improvement that exists only under a favorable cost table is a game result, and the board’s lexicographic ordering (cases passed, then lower charged work, then fewer canonical bytes) even applies to failures: among entries that pass nothing, the one that consumed less of the world ranks higher, a deliberate ledger reading rather than a reward for idleness.

When you design instruction pricing for your own bounded language, the transferable rules are these. Charge at the granularity the engine actually pays, per condition evaluated, per link scanned, so the ledger is an honest mirror of the interpreter’s loop rather than an abstraction above it. Charge failed work, or the optimal strategy becomes reckless attempts that cost nothing. Keep the modeled units rigorously separate from host CPU and agent tokens, and instrument the real costs separately; Platonik measures both and publishes the misses (a 324 MiB peak against a 256 MiB target became a documented repair to 156 MiB, not a quietly moved goalpost). And expose the category breakdown, not only the total: the distribution of a run’s spend is where the actual information lives.

sources

  • platonik: crates/platonik-core/src/sim.rs for Meter, Cat, and Stop; crates/platonik-core/src/policy.rs for per-action charges; crates/platonik-core/src/model.rs for the Costs record and MAX_FUEL / MAX_VARIATION_ACTIVATION_FUEL
  • docs/engine.md: “failed actions still consume their declared execution fuel” and the resource separation rule
  • docs/rust-bridge.md: modeled work vs. host cost, and the measured memory repair
  • docs/agent-evaluation.md: the 68-vs-2,572 execution accounting
  • docs/ports-evaluation.md: the 840-loading / 918-execution tradeoff
  • docs/complexity-and-scale.md: cost models as declared experimental apparatus

keep reading: free for subscribers

the rest of this lesson is free. add your email once and every subscriber lesson on this site stays unlocked.

already subscribed? enter the same email to unlock.