Repository navigation
Debug runtimes freeze values handed to the world - #178
Merged
Merged
Conversation
Values passed to spawn, insert, set/update, resource writes, emit, send, next-state set, and Runtime.make were stored by reference. Mutating one afterwards changed the world with no write, and a rewind restored the mutated value. A runtime made with `debug` now deep-freezes each value as it is queued or written, so the mutation throws where it happens. Release runtimes are unchanged. Also: Runtime.tick points to FixedLoop.start, the JSDoc of set/update, Command.entry/spawn, ReadonlyValue, and Debug says the world owns what it is given, and the ritmo skill treats variables a system closes over like module variables. Found while reviewing Collision Chamber (collision-chamber.gavri.xyz). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Found while reviewing Collision Chamber, a pinball game on Ritmo 0.10. Its adapter runs
structuredClone(...) as Ballon every read and write at 240 Hz, because nothing else stops this:The world stored values by reference, so a later mutation changed the world with no write.
changedreaders missed it, andrewindbrought back the mutated value, breaking the "exactly as it was" promise inCheckpoint.Change
A runtime made with
debug: { hostClock }deep-freezes every value it is handed, as it is queued or written:Game.Command.spawn/entry/commands.insertset/update/setRaw, including entity statesemit,runtime.send, next-stateset, machineupdateGame.Runtime.makeA later mutation throws a
TypeErrorat the line that does it. The walk stops at frozen objects, so{ ...value, x }freezes one object. Typed arrays are skipped;Map/Setcontents stay writable.Release runtimes are unchanged. They freeze nothing, and the write hot path is untouched: debug runtimes get separate cell classes, chosen once when the cell is made.
Docs:
WriteCell.set/update,Command.entry/spawn,ReadonlyValue, andDebugsay the world owns what it is given.Runtime.tickpoints toFixedLoop.start; the game hand-wrote a fixed-step accumulator.ritmoskill now treats a variable a system closes over like a module variable, which a rewind can't restore.Breaking for debug runtimes only. The changeset carries the fix: build a new value instead of mutating the one handed over.
Checks
pnpm run check: passes (1286 tests).test/Runtime.ownership.test.tscovers each entry point, the rewind case, and that release runtimes freeze nothing; 5 of its 8 tests fail without the fix.pnpm --dir website build: passes.pnpm bench:checkfails locally onrender/sync-1k-changed-of-10k(critical).mainfails it the same way on this machine (+25% over the committed baseline), andmainagainst its own earlier run moved +38–57%, so it is machine noise. Allocations are unchanged.🤖 Generated with Claude Code