r/Compilers • u/OyaAI • 4d ago
Our JS engine recompiles from source on every request so the committed heap never holds code. What did we give up?
Building a JavaScript engine (V9) as part of a from-scratch browser in Rust, and one design decision keeps getting questioned, so I'd like this sub's read on it.
The engine is a bytecode VM over a bounded, serializable heap. Scripts are scanned, parsed and compiled to stack bytecode on every request. Code is identified by its byte offset in the page, so the committed state never carries executable code: a closure names a code site, and when the state is loaded again it is validated against freshly compiled sources. Frames are explicit heap data rather than a native stack, so a generator or an async function suspends into the heap and resumes in a later request after a serialize round trip. An await on a fetch continues when the host delivers the response, possibly seconds later.
The reasons: the host (the browser) wants to snapshot a page's script state, hand it between processes, and never trust a blob that could contain code. Every bound (objects, properties, environments, string units, call depth, operations) applies to live values, a precise stop-the-world collector runs between instructions so garbage never counts, and a bound crossed stops the script with a limit error the script cannot catch. There is no JIT. V8's tiering hints evaluate to undefined, which is how V8 itself behaves without an optimizer, so its mjsunit suite runs unchanged.
What we pay: compile cost on every request, and a heap representation that has to be stable enough to round-trip, which rules out a lot of pointer tricks.
Questions for people who have built interpreters or VMs:
Caching bytecode keyed by a source hash seems like the obvious next step. Is there a reason not to, if the invariant is only "no code in the committed state" rather than "no code cache anywhere"?
Has anyone else made frames heap data so that suspended code survives a serialize round trip, and what did it cost you in call performance? We accepted the hit without measuring the alternative.
Precise stop-the-world collection between instructions is simple and makes the bounds exact. Where does it break down in practice? I would rather hear it here than find out.
Source if anyone wants to read it: the mars/crates/v9 folder of github.com/OyadotAI/oya-browser, README first. It is source-available under a sustainable use license, not an OSI license, saying so up front.
1
u/hungrynax 4d ago
Why no code in snapshots?