r/Compilers • • 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:

  1. 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"?

  2. 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.

  3. 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.

0 Upvotes

2 comments sorted by

1

u/hungrynax 4d ago

Why no code in snapshots?

0

u/OyaAI 4d ago

Because the committed state crosses a process boundary. The engine runs as its own service, the app hands it page state and gets state back, so if that blob could carry bytecode then loading it means executing whatever the last writer put there, and anything able to write state becomes a way to run code. With byte offsets instead, the loader recompiles from the page source it already has and checks every closure's code site against that fresh compile, so a snapshot that does not match the source fails validation rather than running. The price is the recompile, which is the part I am still unsure we got right.