Repository navigation
Conversation
`dpsc` rewrites every member access whose key could name `location`, `parent`, `top` or `eval`. `ppsc` rewrites only the references to the window and the document, which the runtime answers with a Proxy, and leaves member accesses alone. Most of what a page does with the window never needs the proxy, so an oxc_semantic pass (ppsc/elide.rs) proves which references are only ever read through in ways the proxy would answer identically, and leaves those pointing at the real object: - a global, or a local holding one (`var d = document`, a UMD factory parameter, `var self = this`), is classified by every use it has; a computed member, an unsafe name or being passed on makes it unsafe - an unsafe local stays wrapped, and its safe uses are pointed back at the global when it provably still holds it, or else at a twin local (`d$r = $unwrap(d)`) kept in step with every write - with `ppsc_wrap_this`, a function's `this` gets the same treatment, rebound to a `$t` wrapped once in the function's prelude `ppsc-hybrid` also renames a static read of an unsafe name (`x.top`, `this.location`) to dpsc's `$sj_` accessor, so the proxy is only needed where the object is used as a value: a computed key, or being passed on. It is the default, with `ppsc_wrap_this` off. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
elide_diff runs every case in elide-tests/ twice in a boa realm modelling the page: as written with the globals bound to proxies, and rewritten with them bound to the real objects. Elision must not change a result, and must never hand a program the real `location`. elide-tests/generate.mjs writes random programs for it (ELIDE_DIFF=<dir>). `native verify <paths>` rewrites every script with each rewriter and checks the output still parses, timing each. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
tools/generate-unproxy-tables.mjs walks every IDL definition and writes out each operation, constructor and attribute that takes or returns a Window or a Document, for the ppsc runtime to patch. The output is committed; regenerate with `./cv core:gen-unproxy`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
`jsRewriter` picks the rewrite: "dpsc", "ppsc" or "ppsc-hybrid" (the default), with `ppscWrapThis` off. Under either ppsc mode the client makes the window and document proxies (client/global.ts) and the unproxy layer (shared/unproxy.ts) puts natives right: a proxy receiver is swapped for the real object once, in the slot dispatch (`fixReceiver`), and members that take or return a window or a document per the IDL tables get a layer that unwraps or wraps them. Members that need only the receiver fixed are patched bare (`Patch`). `$wrap` now hands back the proxies for the window, the document, and the pretend parent and top, compares against values read once rather than the accessors on every call, and returns early for non-objects. Added `$unwrap`, `$rw` and `$rd` for the elision's twins and `$t` prelude. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
The runway harness takes `?flags={...}` and hands them to the frame it
creates, so every benchmark can compare chromium, dpsc, ppsc and
ppsc-hybrid on one build, round-robin with the order rotated each round.
- benchmark-octane.mjs: Octane 2.0, per test
- benchmark-ops.mjs: ns per operation for common real-code shapes,
self-calibrating so the numbers survive large speed changes
- benchmark-sites.mjs: script time and new page errors on real sites
- capture-script-corpus.mjs: real-site scripts for `native verify`
- benchmark-speedometer.mjs: takes SCRAMJET_FLAGS
BENCHMARKS.md says which to run for what, with a baseline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
…tor traps The global proxy's `defineProperty` trap made every define through the window writable and configurable, and threw on an explicit `configurable: false`. The `Object.defineProperty` hook only routed that one entry point around it. With no trap, a define reaches the window with the descriptor the page wrote, through every entry point. The `Object.getOwnPropertyDescriptor` hook re-wrapped each accessor half in a fresh slot on every call. That made getter identity unstable, cost a slot per call on a hot builtin, and handed the real window to the native `location` getter, leaking the real Location. Without it an extracted native getter called on a proxy throws, as any unpatched native does. The proper fix for descriptor reads is the reflection module's stand-ins. The global proxy's `getOwnPropertyDescriptor` trap only forwarded, and a declared trap is slower than none. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
From review of #138: - A twin was declared right after its local, which in a `for (let h of xs)` head is a second binding the loop does not allow. Locals declared in a for-in/of head get no twin; the loop writes them without a reference the twin could follow anyway. - The visitor had no loop handling, so `for (window in o)` became `for ($wrap(window) in o)`. It now walks for-in/of targets the way it walks destructuring targets. - `location` written through a `var`, a destructuring pattern or a loop head assigned the real global and navigated unrewritten. Those now write `$temploc` and assign it through `$tryset`, as `location = x` already did; a module's `var` is left alone, since it is never the window's. - stamp and lazystamp never stamped a call under ppsc, so `postMessage` saw the wrong incumbent. The stamping moves out of the dpsc visitor into `stamp.rs`, which both visitors call. The elision's inserts at the end of an expression - a twin declaration, the close of a twin write - are added before the visitor runs, and so landed ahead of the expression's own closings at the same offset (a stamped `var c = Date.now()` put the twin inside the call). They are now `Trailer`s, which sort after every other insert at their offset, as does `CleanVariableDeclaration`. elide_diff also runs every case with `incumbency: stamp`, and `native verify` takes `--incumbency`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
…operator d9af2d0 made `$tryset` recognise another frame's Location, but set `lhs.href` on it: the real Location, so every bare `location = x` navigated to `x` unrewritten, in every rewriter. It now sets the owning client's location proxy. It also ignored the operator, so `location += "#a"` navigated to "#a" and `location ||= x` navigated at all. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
The unproxy table generator did not resolve typedefs, so `MessageEventSource` - `(WindowProxy or MessagePort or ServiceWorker)` - never classified as a window, and neither did `GeometryNode`. `MessageEvent.source` and its constructor and `initMessageEvent` are now in the tables, along with the geometry methods. The event layer answers `source` itself for scramjet's own messages, and handed back the sender's real window; under ppsc it is the sender's proxy, so `e.source === frames[0]` holds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
From review of #138: the tables knew Window, Document and Node but not their supertype, so `new FocusEvent(t, {relatedTarget: document})` threw, and `event.target`, `currentTarget`, `relatedTarget` and `composedPath()` handed out the real window and document. `EventTarget` classifies as "*", probed at runtime. A return or attribute can now also be a list of a kind (`"*[]"`), wrapped element by element - in place for a fresh list, as a cached frozen copy for a frozen one - or a promise of one (`"Promise<w>"`, `requestWindow()`). The ops benchmark gains two event cases: under ppsc a read of `event.target` pays the attribute trap, 6.6ns to 69ns. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
From review of #138: a string key was classified by name, and "0" is not an unsafe one, so `window["0"]` was left on the real window and handed out the child's real window. A string that is an array index is now unsafe on a window, as a numeric key already was. Named frame access is left as it is: keeping the proxy for every name a window might not have would give up most of the elision. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
…nstructor aliases From review of #138: - A dictionary argument had the members the IDL names unwrapped by writing them back into the page's object: a frozen or getter-only `{view: window}` threw, a mutable one was left holding the real window, and the getters ran in the wrong order and twice. The native now gets a view of the object that reads each member through when it asks, in its own order, unwrapping only the named ones. - A proxy was only recognised by the client that made it, so `child.Document.prototype.querySelector.call(document, ...)` threw "Illegal invocation". The box now records every client's proxies, which the receiver fix consults once there is more than one client, and which argument unwrapping uses in place of reading a symbol off the value. Wrapping a returned window or document goes through the box's maps too. - `RawProxy` on a constructor left `X.prototype.constructor` on the native: `MouseEvent.prototype.constructor !== MouseEvent`, and `new event.constructor(t, {view: window})` skipped the unwrapping. It now puts the prototype's `constructor` in the same slot, the way `Intercept` does, through a helper the two share. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
A native dispatch calls a listener with the real current target as its receiver, so under ppsc `this === window` was false in every listener on the window or the document, and in their `on*` handlers. Both paths now hand the listener what the page holds for it (`client.pageValue`). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
…e `with` From review of #138: - An immediately invoked function was taken to be its own only call, so its parameters were substituted with the global it was called with. A named function expression can call itself - or hand itself out - with other arguments: `(function f(w, n) { ... f({title: "other"}, 0) ... }) (document, 1)` read the document's title. It now only counts as the only call while nothing references its name. - Inside a `with` body only the global substitution was ruled out, and the use fell through to its twin, picking a binding the object environment lookup might not: `var w = window; with ({w: {...}}) w.innerWidth`. A use inside `with` now gets no substitution at all. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
…claration From review of #138: a parameter was taken to be initialized everywhere, so a use in an earlier parameter's default was substituted with the global: `(function (a = w.title, w) { return a })(undefined, document)` returned the title where it must throw. Parameters are initialized left to right, so only a use past the parameter's own declaration - a later default, or the body - is substituted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
From review of #138: a local copied from `self` was substituted with `self` at its later uses, but `self` and `frames` are [Replaceable] and `globalThis` is writable - `var w = self; self = {...}; w.innerWidth` read the replacement. Only `window` and `document`, which are unforgeable, now stand in for a local, and only through a chain of those two: `window.document`, but not `window.self` or `self.document`. A destructured window is no longer substituted, since a pattern does not say which key it read. Twins are unaffected: they hold the value the local was given. The differential test's window proxy answered `self` and the like with itself whatever the window held; it now wraps what the window holds, as client/global.ts does, which is what a replaced `window.self` needs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
From review of #138: `x instanceof C` was safe when C was an unresolved name starting with a capital, on the assumption that it named a platform constructor. Any global can be replaced by an object whose `Symbol.hasInstance` is handed the value - `window instanceof Custom` went from true to false. The proxy answers a platform constructor's prototype walk the same, so keeping it costs a trap on a rare check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9
This branch has not been deployed
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.
Adds two new JS rewriters next to the existing one, picked with a new
jsRewriterflag. The default becomesppsc-hybrid.jsRewriterdpsclocation/parent/top/evalgoes through$sj_accessors onObject.prototype.ppscProxy. Member accesses are left alone.ppsc-hybrid(default)ppsc, but a static read of an unsafe name (x.top,this.location) goes through dpsc's accessor instead, so the proxy is only needed where the object is used as a value (a computed key, or being passed on).ppscWrapThis(off by default) also rebinds an unsafethisto a$twrapped once per function.Proxy elision (Rust,
rewriter/js/src/ppsc/elide.rs)An
oxc_semanticpass proves which references are only used in ways the proxy would answer identically, and leaves those pointing at the real object:var d = document, UMD factory params, IIFE params,var self = this). Each is classified by every use it has.d$scramjet$r = $unwrap(d)) kept in step with every write.On 29 real sites this took proxy traps from 1.04M to 549k (−47%) with ppsc-hybrid and
$toff.Runtime
client/global.ts.client/shared/unproxy.ts, driven by tables generated from@webref/idl(./cv core:gen-unproxy), puts natives right when they're handed a proxy or return a real window/document.fixReceiver).Patch).locationgetter called on the proxy throws rather than leaks. The proper stand-ins come with the reflection work, and when that landsglobal.tsshould go back to_Proxy.$wrap: caches the values it compares against. It also hands back proxies for the pretendparent/top, which fixes a GTMwhile (w !== w.parent)hang on Discord.Testing
cargo test -p native --release:rewrite_tests,googleand the newelide_diff.elide_diffruns each case in a boa realm twice (as written against proxies, and rewritten against real objects) and fails on any difference or real-locationleak. It passes on the hand-written cases and on 20k generated programs in every asserted configuration.native verify: 0 parse failures over a 351 MB real-site corpus in all three modes. Rewrite cost is 9.4 / 19.5 / 19.1 ms/MB (dpsc / ppsc / ppsc-hybrid).postMessagesource identity.var location, destructuring, loop heads, and plainlocation =. This includes a fix to$tryset, which since d9af2d0 navigated the realLocationin every mode.developbuild (fast mode):postmessage: 24 failures against develop's 25, with the same unexpected set.location: 20 failures against develop's 27. This branch's unexpected set is a subset of develop's.event: 19 failures, the same as develop.function: 18 failures, the same as develop. One differs:functionctor-scopefails becausenew Function("return this")()returns the real window, which is expected withppscWrapThisoff. In exchange,rv23-destructure-location-var-in-functionnow passes.Benchmarks (
tools/BENCHMARKS.md)The harness now takes
?flags={...}, so every tool compareschromium/dpsc/ppsc/ppsc-hybridon one build:benchmark-octane.mjs,benchmark-ops.mjs,benchmark-sites.mjs,capture-script-corpus.mjs, plusSCRAMJET_FLAGSforbenchmark-speedometer.mjs.Octane in Chromium (median of 3):
Splay is bimodal in every scramjet mode and swings the total ±10%. See the README for the full tables.
Speedometer 3.1 (clean 18-suite subset) is within noise across modes: dpsc 17.3 / 17.0 / 16.1, ppsc 17.2, ppsc-hybrid 16.7 / 15.5 (±0.6–0.95). ppsc-hybrid came in slightly under dpsc in both paired runs.
Real sites, total script ms over 32: chromium 40103, dpsc 108950, ppsc-hybrid 101144. Three errors show up only under ppsc-hybrid:
MouseEventrejectsview: window, probably an inheritedUIEventInitmember missing from the tables.undefined is not iterable, with its script time roughly tripled.getReaderon undefined.These are listed in
tools/BENCHMARKS.mdand still need fixing.Known costs that ppsc adds, from
benchmark-ops:el.ownerDocumenthands back the document proxy: 16 → 100ns, hit by jQuery-styleel.ownerDocument === docchecks.getElementById27 → 47ns.event.target(andcurrentTarget,relatedTarget,composedPath()) goes through an attribute trap so the window and document come back as proxies: 6.6 → 69ns per read.Known open: a child frame reached by name (
window.myframe, baremyframe,document.myframe) is handed out as its real window under ppsc. Keeping the proxy for every name a window might not have would give up most of the elision, so this is left as it is.window["0"]is handled.tools/slot-proxy-to-function.mdis a separate note on replacing the slot apply proxies with plain functions, the next step for that call overhead.🤖 Generated with Claude Code
https://claude.ai/code/session_01CuZEvxaYZC8a1Ezv4Jnvn9