Moving initializr to new JS port#4795
Open
shai-almog wants to merge 130 commits intomasterfrom
Open
Conversation
37159a9 to
e273251
Compare
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Contributor
Cloudflare Preview
|
Collaborator
Author
Collaborator
Author
|
Compared 90 screenshots: 90 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Contributor
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 90 screenshots: 90 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
766a374 to
6c6c483
Compare
The raw ByteCodeTranslator JS output for Initializr was a single 90 MiB
translated_app.js that Cloudflare Pages refused to upload (25 MiB per-file
cap). Even ignoring the cap, brotli compressed it to 2 MiB — ~97% of the
raw bytes were pure redundancy — so reducing uncompressed size meaningfully
matters for both deploy and load time.
This lands four layered optimisations:
1. cn1_iv0..cn1_iv4 / cn1_ivN runtime helpers (parparvm_runtime.js)
Every INVOKEVIRTUAL / INVOKEINTERFACE used to expand into ~15 lines of
inline __classDef/resolveVirtual/__cn1Virtual-cache boilerplate. On
Initializr that pattern alone was ~24 MiB across 35k call sites. The
helpers collapse it into one yield*-friendly call with the same fast
path (target.__classDef.methods lookup) and fallback (jvm.resolveVirtual
owns the class-wide cache already). Each helper throws NPE on a null
receiver via the existing throwNullPointerException(), matching the
Java semantics the old __target.__classDef dereference gave for free.
2. Switch-case no-op elision (JavascriptMethodGenerator.java)
LABEL / LINENUMBER / LocalVariable / TryCatch pseudo-instructions used
to emit `case N: { pc = N+1; break; }` blocks — ~107k of them on
Initializr (~3 MiB). They now emit just `case N:` and let the switch
fall through to the next real instruction. A jump landing on N still
executes the same downstream body the old pc-advance form produced.
3. translated_app.js chunking (JavascriptBundleWriter.java)
Class bodies are now streamed into bounded chunks (20 MiB cap each).
Lead chunks land as translated_app_N.js; the trailing chunk retains
the jvm.setMain call. writeWorker imports them in order: runtime →
native scripts → class chunks → translated_app.js (setMain last).
4. Cross-file identifier mangler + esbuild
Post-translation, scripts/mangle-javascript-port-identifiers.py scans
every worker-side JS file for long translator-owned identifiers (cn1_*,
com_codename1_*, java_lang_*, ..., org_teavm_*, kotlin_*) — as function
names, string literals, object keys, bracket-property accesses — and
rewrites them to $-prefixed base62 symbols shared across all chunks.
Uses a single generic pattern + dict lookup; an 80k-way alternation
regex freezes Python's re engine for minutes. Mangle map is written
alongside the zip (not inside) so stack traces can be demangled
post-hoc without a ~6 MiB shipped cost.
Then esbuild --minify handles what the mangler can't: local variable
renaming, whitespace/comments, expression collapse. Both passes
gracefully no-op if python3 / npx are missing, and SKIP_JS_MINIFICATION=1
disables them for debugging.
Initializr measured end-to-end (per-file Cloudflare limit is 25 MiB):
Before: 90.0 MiB single file
After: 20.85 MiB across 4 chunks, biggest 6.27 MiB
brotli over the wire: 1.64 MiB
HelloCodenameOne benefits automatically — same build script pattern.
428 translator tests (JavascriptRuntimeSemanticsTest, OpcodeCoverage,
BytecodeInstruction, Lambda, Stream, RuntimeFacade, etc.) pass on the
new runtime and emission paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
port.js is imported by worker.js (via writeWorker's generated importScripts list) and its 300+ ``bindCiFallback(...) / bindNative(...)`` calls register overrides keyed on the *translator's* cn1_* method IDs. When the mangler only rewrote translated_app*.js + parparvm_runtime.js, port.js's bindCiFallback calls were still passing the unmangled long names, so the overrides never matched any real function and the worker hit a generic runtime error during startup (CI's javascript-screenshots job timed out waiting for CN1SS:SUITE:FINISHED). Move port.js into the mangler's worker-side file set. We leave browser_bridge.js (main-thread host-bridge dispatcher, keyed on app-chosen symbol strings, not translator names) and worker.js / sw.js (tiny shells) alone, and skip any ``*_native_handlers.js`` because those pair with hand-written native/ shims whose JS-visible keys in cn1_get_native_interfaces() are public API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mangler breaks the JavaScriptPort runtime (port.js) in two specific
places that can't be fixed by a purely textual rewrite:
* Line 594: ``key.indexOf("cn1_") !== 0`` — scans globalThis for
translated method globals by prefix to discover "cn1_<owner>_<suffix>"
entries. After mangling, those globals are named "$a", "$b" etc.
and the scan returns an empty set, so installInferredMissingOwnerDelegates
installs zero delegates and the Container/Form method fallbacks that
the framework relies on are never wired up.
* Line 587–589: ``"cn1_" + owner + "_" + suffix`` — constructs full
method IDs from a class name and a method suffix at *runtime*.
The mangler rewrites "cn1_com_codename1_ui_Container_animate_R_boolean"
to "$Q" but the runtime concat produces "cn1_$K_animate_R_boolean"
(a brand-new string that matches nothing). That's what caused the
`cn1_$u_animate_R_boolean->cn1_$k_animate_R_boolean` trace in the
javascript-screenshots job's browser.log.
Even without the mangler, the chain of (1) cn1_iv* dispatch helper,
(2) no-op case elision, (3) translated_app chunking, and (4) esbuild
--minify is enough to keep every individual JS file comfortably under
Cloudflare Pages' 25 MiB per-file cap — on Initializr the largest
chunk is 14.7 MiB. Wire-compressed sizes are higher (brotli ~5 MiB vs
~1.6 MiB with mangling) but still reasonable.
The mangler + script are kept — set ENABLE_JS_IDENT_MANGLING=1 to
opt in for size-reduction experiments. A follow-up rewrite of port.js
to go through a translation-time manifest of method IDs would let us
turn mangling back on by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
port.js and browser_bridge.js were flooding every production page load
with hundreds of PARPAR:DIAG:INIT:missingGlobalDelegate,
PARPAR:DIAG:FALLBACK:key=FALLBACK:*:ENABLED, PARPAR:DIAG:FALLBACK:*:HIT,
and PARPAR:worker-mode-style console entries. Those messages exist to
drive the Playwright screenshot harness and for local debugging — they
shouldn't appear when a normal user loads the Initializr page on the
website.
Three previously-unconditional emission paths now gate on the same
``?parparDiag=1`` query toggle the rest of the port already honours:
* port.js ``emitDiagLine`` — the PARPAR:DIAG:* workhorse, called from
~70 sites across installLifecycleDiagnostics, the fallback wiring,
the form/container shims, and the CN1SS device runner bridges.
* port.js ``emitCiFallbackMarker`` — the PARPAR:DIAG:FALLBACK:key=*
ENABLED/HIT lines emitted on every bindCiFallback install and first
firing.
* browser_bridge.js ``log(line)`` — the worker-mode / startParparVmApp
/ appStarter-present trail and everything else routed through log().
* browser_bridge.js main-thread echo of forwarded worker log messages
(``data.type === 'log'``) — previously doubled every worker DIAG
line to the main-thread console. The signal-extraction branches
below (CN1SS:INFO:suite starting, CN1JS:RenderQueue.* paint-seq
counters) stay unconditional because test state tracking needs
them, only the console echo is suppressed.
CI's javascript-screenshots harness still passes ``?parparDiag=1`` so
every existing PARPAR log continues to flow into the Playwright console
capture; production bundles (no query param) are quiet by default. Set
``window.__cn1Verbose = true`` from DevTools to re-enable ad-hoc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two production-console issues:
1. Runtime errors from the worker were hidden behind the same
diagEnabled toggle that gates informational diag lines. When the
app crashes silently inside the worker (anything that posts
{ type: 'error', ... } to the main thread), the user saw only
the "Loading..." splash hanging forever because diag() is a no-op
without ``?parparDiag=1``. Now browser_bridge.js always writes
``PARPAR:ERROR: <message>\n<stack>\n virtualFailure=...`` via
console.error for that message class, independent of the
diagnostic toggle. Errors are actionable; diagnostics are noise.
2. port.js's Log.print fallback forwards every call at level 0
(the untagged ``Log.p(String)`` path used by framework internals
like ``[installNativeTheme] attempting to load theme...``) to
console.log unconditionally. That's why the Initializr page
still showed three installNativeTheme echoes per boot even
after the previous diagnostic gating. Now level-0 Log.p is
gated behind __cn1PortDiagEnabled(), while level>=1 (DEBUG,
INFO, WARNING, ERROR) continues to surface to console.error
unconditionally. User code that wants verbose output either
passes through Log.e() (still surfaced) or loads with
``?parparDiag=1``.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ention
The runtime was throwing ``Blocking monitor acquisition is not yet
supported in javascript backend`` the moment a synchronized block
contended — hit immediately by Initializr's startup path:
InitializrJavaScriptMain.main
-> ParparVMBootstrap.bootstrap
-> Lifecycle.start
-> Initializr.runApp
-> Form.show
-> Form.show(boolean)
-> Form.initFocused (port.js fallback)
-> Form.setFocused
-> Form.changeFocusState
-> Component/Button.fireFocusGained
-> EventDispatcher.fireFocus
-> Display.callSerially (synchronized -> monitorEnter)
-> throw
The JS backend is actually single-threaded at the real-JS level.
ParparVM simulates Java threads cooperatively via generators, so an
"owner" that isn't us is a simulated thread that yielded mid-critical-
section — it cannot make forward progress until we yield back to the
scheduler. Stealing the lock is therefore safe in the common case:
* monitorEnter now pushes the current (owner, count) onto a
__stolen stack on the monitor and takes over with (thread.id, 1)
when contention is detected, instead of throwing.
* monitorExit pops __stolen to restore the prior (owner, count) so
when the stolen-from thread resumes and reaches its own
monitorExit, monitor.owner === its thread.id again and the
IllegalMonitorStateException check passes. Nested steals cascade
through the stack.
This avoids rewiring the emitter to make jvm.monitorEnter a generator
(which would need ``yield* jvm.monitorEnter(...)`` at every site and
a new ``op: "monitor-enter"`` in the scheduler). Existing
LockIntegrationTest + JavaScriptPortSmokeIntegrationTest still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
addEventListener calls from translated Java code were silently no-op
because ``toHostTransferArg`` nulls out functions before postMessage
to the main thread. Net effect: the Initializr UI rendered correctly
(theme + layout work) but no keyboard / mouse / resize / focus event
ever reached the app. Screenshot tests didn't catch it — they only
exercise layout paths.
Wire a function -> callback-id round-trip:
* parparvm_runtime.js
- Add ``jvm.workerCallbacks`` + ``nextWorkerCallbackId`` registry.
- ``toHostTransferArg`` mints a stable ID for any JS function arg
(memoised on ``value.__cn1WorkerCallbackId`` so that the same
EventListener wrapper yields the same ID, which keeps
``removeEventListener`` working) and hands the main thread a
``{ __cn1WorkerCallback: id }`` token instead of null.
- ``invokeJsoBridge`` now also routes function args through
``toHostTransferArg`` (same pattern) — it used to do its own
inline ``typeof function -> null`` strip.
- ``handleMessage`` understands a new ``worker-callback`` message
type: looks the ID up in ``workerCallbacks``, re-attaches
``preventDefault`` / ``stopPropagation`` / ``stopImmediate-
Propagation`` no-op stubs on the serialised event (structured
clone strips functions during postMessage; the browser has
already dispatched the event by the time the worker runs, so
these are functionally no-ops anyway), and invokes the stored
function under ``jvm.fail`` protection.
* worker.js
- Recognise ``worker-callback`` in ``self.onmessage`` and forward
to ``jvm.handleMessage``.
* browser_bridge.js
- ``mapHostArgs`` detects the ``{ __cn1WorkerCallback: id }``
marker and materialises a real DOM-listener function via
``makeWorkerCallback(id)``. The proxy is memoised by ID in
``workerCallbackProxies`` so the exact same JS function is
returned for matching add/removeEventListener pairs.
- ``serializeEventForWorker`` copies the fields ``port.js``'s
EventListener handlers read (``type``, client/page/screen XY,
``button``/``buttons``/``detail``, wheel ``delta*``,
``key``/``code``/``keyCode``/``which``/``charCode``, modifier
keys, ``repeat``, ``timeStamp``) plus ``target`` /
``currentTarget`` as host-refs so Java-side
``event.getTarget().dispatchEvent(...)`` still round-trips
correctly through the JSO bridge.
- Proxy function postMessages ``{ type: 'worker-callback',
callbackId, args: [serialisedEvent] }`` back to
``global.__parparWorker``.
Tests: the full translator suite
(JavaScriptPortSmokeIntegrationTest, JavascriptRuntimeSemanticsTest,
BytecodeInstructionIntegrationTest) still passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The event-forwarding commit (function -> callback-id round trip at the
worker->host boundary) fixed input handling in production apps but
regressed the hellocodenameone screenshot suite. Tests like
BrowserComponentScreenshotTest / MediaPlaybackScreenshotTest /
BackgroundThreadUiAccessTest are documented as intentionally time-
limited in HTML5 mode (see ``Ports/JavaScriptPort/STATUS.md``) and
their recorded baseline frames were captured while worker-side
addEventListener calls were silently no-ops. Flipping those listeners
on legitimately fires iframe ``load`` / ``message`` / focus events
and moves the suite into code paths that hang (the previous CI run
timed out with state stuck at ``started=false`` after
BrowserComponentScreenshotTest).
Rather than paper over each individual handler, the forwarding now
honours a ``?cn1DisableEventForwarding=1`` URL query param:
* ``parparvm_runtime.js`` reads the flag once (also accepts the
``global.__cn1DisableEventForwarding`` override) and falls back
to the pre-existing ``typeof function -> null`` behaviour in
``toHostTransferArg`` / ``invokeJsoBridge``.
* ``scripts/run-javascript-browser-tests.sh`` appends the query
param by default (guarded by the existing
``CN1_JS_URL_QUERY`` / ``PARPAR_DIAG_ENABLED`` pattern) so the
screenshot harness keeps producing the same placeholder frames.
Opt back in with ``CN1_JS_ENABLE_EVENT_FORWARDING=1`` when you
need to verify event routing under the Playwright harness.
Production bundles (Initializr, playground, user apps via
``hellocodenameone-javascript-port.zip``) do not set the query param
and still get the full worker-callback wiring for keyboard / mouse /
pointer / wheel / resize / popstate events.
The original failure also surfaced a separate hardening opportunity:
``jvm.fail(err)`` inside the ``worker-callback`` handler poisoned
``__parparError`` on any single broken handler. Switch to a best-
effort ``console.error`` so one misbehaving listener can't take down
the VM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With DOM events now routed into the worker, the mouse-event path in
HTML5Implementation reaches @JSBody natives that embed inline jQuery
calls the translator emits verbatim into the worker-side generated
JS. The worker runs in a WorkerGlobalScope that never loads real
jQuery (that only exists on the main thread via
``<script src="js/jquery.min.js">`` in the bundled ``index.html``),
so every pointer move the user made produced:
PARPAR:ERROR: ReferenceError: jQuery is not defined
cn1_..._HTML5Implementation_getScrollY__R_int
cn1_..._HTML5Implementation_getClientY_..._MouseEvent_R_int
cn1_..._HTML5Implementation_access_1400_..._R_int__impl
cn1_..._HTML5Implementation_11_handleEvent_..._Event
Five sites in HTML5Implementation use this pattern today:
``getScrollY_`` / ``scroll_`` on ``jQuery(window)``; ``is()`` on a
selector match; ``on('touchstart.preemptiveFocus', ...)``; an
iframe ``about:blank`` constructor; the splash-hide fadeOut.
Install a no-op jQuery object at the top of port.js (which is
imported into the worker by ``worker.js``'s generated importScripts
list). It only activates when ``target.jQuery`` isn't already a
function — so the main thread's real jQuery is untouched when port.js
is ever loaded there, and repeated port.js imports inside the worker
are idempotent. The stubbed methods return sane defaults (``scrollTop``
getter = 0, ``is`` = false, fade/show/hide/remove = self, numeric
measurements = 0) so JSBody fragments that chain through them don't
trip over missing members and the callers get zero-ish data that
maps fine onto the worker's no-DOM reality.
The real DOM side effects the original jQuery calls intended
(window.scroll, iframe insert, splash fadeOut, etc.) either no-op
on the worker side legitimately or already round-trip through the
host bridge via separate paths, so we're not losing meaningful
behaviour — just converting what was an opaque runtime crash into
an explicit no-op until those natives are migrated to proper
host-bridge calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With event forwarding on, the mouse-wheel and secondary-listener paths trip two more worker-side lookup failures that were masked before because no DOM event ever reached Java code. 1. ``TypeError: window.cn1NormalizeWheel is not a function`` HTML5Implementation.mouseWheelMoved goes through an @JSBody that calls ``window.cn1NormalizeWheel(evt)``. The real function is installed by ``js/fontmetrics.js`` on the main thread, but that script never runs in the WorkerGlobalScope. The body is pure data munging (reads event.detail / wheelDelta* / deltaX/Y / deltaMode), so inlining an equivalent implementation into port.js fixes the worker path without changing the translated native. ``cn1NormalizeWheel.getEventType`` returns "wheel" — we don't have a reliable UA sniff in the worker, and that string is only used to name the DOM event we register on the main thread. 2. ``TypeError: _.addEventListener is not a function`` EventUtil._addEventListener is an @JSBody with the inline script ``target.addEventListener(eventType, handler, useCapture)``. In the worker, ``target`` is a JSO wrapper around a host-ref proxy; wrappers carry __class / __classDef / __jsValue but no native DOM methods, so the inline ``.addEventListener(...)`` property lookup returned undefined and the call threw. Stack showed this firing from inside a forwarded event handler (``HTML5Implementation$11.handleEvent``) trying to register a secondary listener at runtime. Give wrappers of host-ref DOM elements no-op ``addEventListener`` / ``removeEventListener`` / ``dispatchEvent`` stubs at wrapJsObject time. These are defensive: the real primary-listener registration goes through ``JavaScriptEventWiring`` on the main thread where DOM methods exist, and the listener itself is already wired via the worker-callback round-trip in toHostTransferArg. Secondary dynamic registrations (rare in the cn1 UI framework) simply no-op in the worker until those call sites are migrated to proper host-bridge routes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix added no-op ``addEventListener`` / ``removeEventListener`` / ``dispatchEvent`` stubs only on the JSO wrapper, but the ``@JSBody`` emitter in JavascriptMethodGenerator wraps object parameters with ``jvm.unwrapJsValue(__cn1Arg)`` before calling the inline script. That unwrap returns ``wrapper.__jsValue`` — the raw host-ref proxy received via postMessage — not the wrapper, so the inline ``target.addEventListener(...)`` lookup still failed with ``TypeError: _.addEventListener is not a function`` inside ``EventUtil._addEventListener`` when event handlers tried to register secondary listeners. Install the same stubs on the underlying ``value`` object at wrap time. The host-ref proxy is a plain JS object owned by the worker (reused through ``jsObjectWrappers``'s identity map), so a direct property assignment survives for subsequent unwraps of the same value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's PMD ControlStatementBraces rule blocks build-test (8) on the two single-line ``if (safeEnd ...) safeEnd = ...;`` clamps added in 06fbef0. Wrap them in braces; behaviour unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rule 8b/9b/10c collapsed three pushes + invokevirtual into one inline cn1_iv* call when the second push expression contained balanced parens. The EXPR pattern accepted stack.q()|0 (the f2i / d2i / l2i / i2c / i2s output) — but stack.q() consumes the FIRST push, so the rule's invariant that the call block's outer stack.q() will pop the second push no longer held. The peephole emitted the second push as the receiver and stack.q()|0 as the arg, swapping the two and dispatching on the float wrapper instead of the real receiver. In Toolbar.show*SidemenuImpl (the ParparVM Initializr sample) this surfaced as "Missing virtual method $iA on undefined" the moment the hamburger menu was clicked: setBgTransparency dispatched on the float ``f`` rather than the Style ``s``. Tighten the EXPR regex with a (?!stack\.q\() lookahead so any expression that pops the stack stays on the slow path. JsF2IInvokeReceiverApp locks the shape down — a 3-arg helper that calls setMaskedValue((int) f) and reads the field back; pre-fix the translation either threw or wrote the wrong slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two Safari/WebKit bugs surfaced when running the Initializr sample on
Safari (no error on Chromium):
1. ``window.getParameterByName`` is defined in fontmetrics.js (main
thread) and used by JSBodies that read URL query params
(cn1SafariBacksideHookDelay, pixelRatio, isDesktop, isTablet,
baseFont, density, ...). Those JSBodies run inside the worker, where
``window`` aliases ``self`` — fontmetrics.js never loads there, so the
lookup is undefined and the JSBody throws. The Safari-only path
``HTML5Implementation_*.handleEvent → safariBacksideHookDelay()``
catches the throw, but it propagates out of the click handler before
the action listener fires, so dialogs and the Toolbar side menu never
appear. Define a worker-local ``getParameterByName`` that reads from
``__cn1LocationSearch`` (forwarded by the main thread on START).
2. The ``initImplSafe`` shim in port.js uses ``bindCiFallback`` with the
class-specific name ``cn1_<class>_initImpl_*``, but the post-mangle
class methods table keys on the dispatch-id form
``cn1_s_initImpl_*`` — the two mangle to *different* short symbols
($aJ5 and $aFs in the current bundle). resolveVirtual finds the
original $aJ5 in cls.methods first and never consults the override,
so on Safari the translator-stripped substring(0, lastIndexOf('.'))
throws AIOOBE during boot and the app never starts. Bind both forms
so installVirtualOverride patches the dispatch-id slot as well, and
teach the catch to recognise translated Java AIOOBE / SIOOBE / NPE
objects (their __class is set; ``message`` is undefined, so the
substring-on-message check used to silently rethrow them).
Verified on WebKit/playwright: Initializr boots in ~30s and the
Hello-World dialog now appears (was: blank preview; "Missing virtual
method" or "JavaThrow[$kX]: 0" on every click).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NativeImage's hasLoadedImage() check required the async-set ``loaded`` flag and did not recognise an HTMLImageElement that was already decoded but whose ``load`` event listener hadn't fired yet. JavaScriptNative- ImageAdapter.resolveWidth's fallback for that window returns a hard-coded **10** — and the first caller (typically EncodedImage.getWidth hitting the ``width = getInternalImpl().getWidth()`` lazy initialisation on line 548) caches that 10 as the EncodedImage's recorded width. By the time the load event fires and NativeImage.width is set to the real naturalWidth, EncodedImage already has width=10 and never re-queries — every subsequent drawImage takes the scaled branch and squashes the real image down to 10x24. In the Initializr Dialog this manifested as the iOS7Theme 9-piece border drawing each 125x24 / 143x145 corner PNG scaled to 10x24 / 143x10 — the rounded transparent corner pixel got averaged with 12-14 columns of opaque light gray and disappeared, producing a flat rectangle with no rounded corners and no shadow. Treat the image as loaded as soon as the underlying element exposes a positive natural size, regardless of whether the async listener has fired. The natural dimensions are the authoritative size; the ``loaded`` flag is a coarse async signal that lags the actual decode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resources/UIManager bootstrap opens the same .res file multiple times during a single boot (theme + layered fallback + EncodedImage multi-image lazy load), each hitting the network synchronously over XHR. iOS7Theme.res alone was downloaded 3x = ~1.4 MB wasted on the wire. Cache the response Uint8Array per URL the first time we fetch it and serve a fresh ArrayBufferInputStream over the same buffer on subsequent opens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final post-peephole pass in applyMethodPeephole rewrites the emitter-private register names ``stack`` and ``locals`` to single characters within each method body. The Initializr translated_app.js contains stack.p / stack.q ~210k times and locals[N] ~80k times; the rename strips ~1.7 MiB raw (8.99 MB → 7.30 MB after esbuild minify-syntax) and shaves ~400 KiB off the gzipped bundle (~26% reduction on translated_app.js gzip). The walker tracks `"..."` / `'...'` / template / line+block comment state so theme-key literals containing the words "stack" / "locals" survive. All existing peephole rules continue to operate on the long names — the rename runs strictly after every other peephole, so nothing else needed to change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
teavm-classlib-0.8.1.jar ships ~18 MiB of locale/Unicode/timezone resource files (``cldr-json.zip`` 15.84 MiB, ``UnicodeData.txt`` 1.79 MiB, ``tzdata2019b.zip`` 397 KiB) that TeaVM consumes at compile time via its metadata-generator hooks to bake locale data into the emitted JS. ParparVM's translator never references those files; the served bundle's translated_app.js / parparvm_runtime.js / port.js / worker.js contain zero string references to ``cldr-json`` / ``UnicodeData`` / ``tzdata`` (verified via grep on a fresh build), so the resources were pure dead weight that the build script copied into the bundle simply because they sit alongside class files in the staging dir. Drop the three resource paths from the staging dir right after ``jar xf`` so the translator's "copy non-class resources to dist" step never sees them. Bundle goes from 28.10 MiB to 11.58 MiB (-58.7%). Local bundle still boots cleanly (2854 ms, 0 console errors) -- same as before pruning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the per-method shortenStackAndLocals walker to collapse two more emitter-private identifiers: - ``__cn1ThisObject`` -> ``T`` - ``__cn1Arg<N>`` -> ``A<N>`` Both names are scoped to a single function (the param list and its references inside the method body), so they're safe to shorten as long as we keep the rename consistent across the whole function body and skip string literals like ``"$T"`` / ``"$A2"`` that come out of the cn1_* identifier mangler. Initializr translated_app.js drops 7.30 MiB -> 6.94 MiB (-360 KiB raw); local bundle still boots in 3051 ms with 0 console errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HTML5Implementation.getArrayBufferInputStream used the legacy
``overrideMimeType("text/plain; charset=x-user-defined")`` trick to
read binary asset bytes via XMLHttpRequest -- then walked the
response string char-by-char into a fresh Uint8Array. For
theme.res (~735 KiB) that's ~735k JS->JSO ``out.set(i, ...)``
calls per fetch, which on the Initializr profile took ~939 ms of
worker wall time (sync XHR blocks the cooperative scheduler the
whole time). With ``responseType = "arraybuffer"`` the same fetch
lands in ~3 ms (clean-worker microbenchmark) / ~400 ms (full app
boot, where the residual cost is the worker's downstream
res-parse / image-decode pipeline still running on the same
thread, not the XHR itself).
Effect on the Initializr local bundle:
cn1Started: 3427 ms -> 2522 ms (-905 ms, -26%)
theme.res sync XHR: 939 ms -> 398 ms (-541 ms)
iOS7Theme.res sync XHR: 533 ms -> 189 ms (-344 ms)
Also disables an experimental ``<link rel="preload">`` patch in
the build script with a comment recording why it was removed
(credentials/cors mismatch with the worker's XHR; the ``?v=1.0``
cache-buster appended at sync-XHR time meant the preload URL
didn't match anyway). Keeping the hook commented so a follow-up
that switches the worker side to async ``fetch()`` can flip it
back on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
createNativeImage was copying PNG bytes one element at a time through ``arr.set(i, bytes[i+offset])`` -- one JSO bridge call per byte for every theme image. With ~50 images per theme load and per-image PNG sizes of 5-50 KiB, that's hundreds of thousands to millions of JSO crossings during boot. Replace the per-byte loop with a ``@JSBody`` helper that delegates to the browser's native ``Uint8Array.prototype.set``, which copies an array-like in a single typed-array memcpy. ``ToUint8`` conversion preserves the -128..127 -> 0..255 semantics of the previous loop. Modest standalone effect on boot (most of lifecycle.init's ~970 ms is asynchronous image-decode wait, not byte-copy CPU) but unblocks future work: with the byte copy off the critical path the next big lever is parallelising / amortising the HTMLImageElement decode wait that currently dominates theme load. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the same fix applied to HTML5Implementation in commit 26f1cf1: drop the legacy ``overrideMimeType("text/plain; charset=x-user-defined")`` charset hack and tell the XHR to return an ArrayBuffer directly. ``toResponseBytes`` already had a fast arraybuffer branch that was unreachable under the old override; this just makes that branch the actual hot path. NetworkConnection drives runtime HTTP for any ``ConnectionRequest`` issued by the app, so every download now skips the per-byte ``out.set(i, responseText.charAt(i) & 0xff)`` loop in the fallback. Not on the Initializr boot critical path (Initializr does no network calls during boot) but a sizeable win for any app that fetches data at startup or in response to user actions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ument Three related fixes that eliminate ~180 worker->main HOST_CALL round-trips during Initializr boot. 1) measureText via OffscreenCanvas in worker ``HTML5Graphics.stringWidth`` previously round-tripped 3x to the main thread per call (getFont, measureText, TextMetrics.width) -- ~168 round-trips during boot. Empirical call mix: 56 unique measureText calls each costing 3 trips. Switched to a worker-side ``OffscreenCanvas`` + ``measureText`` in a single ``@JSBody`` -- entirely in-worker, no postMessage. Falls back to the legacy main-thread path on browsers without OffscreenCanvas (Safari < 16.4). 2) Cache ``Window.current()`` per worker The main-thread window reference never changes for the worker's lifetime, but ``Window.current()`` is invoked 42 times during boot (UIManager, Resources, BrowserComponent, ...). Each call was a worker->main HOST_CALL via ``__cn1_dom_window_current__``. Cache the wrapper on ``self.__cn1WindowWrapper``. 3) Cache ``Window.getDocument()`` per host-window receiver ``getDocument`` is called ~10 times during boot; the host document never changes. Cache on ``win.__cn1CachedDocWrapper``. Round-trip tally (Initializr boot, instrumented): before: 363 round-trips, 143 fire-and-forget batches after: ~180 round-trips, ~32 batches (-50%) Wall-clock effect is modest (-50 ms median, baseline already had significant variance) because each round-trip is amortised by the cooperative scheduler, but every removed round-trip cuts a postMessage + structured-clone + reply pair, which compounds with future optimisation work that depends on a quieter inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set of small Playwright-based scripts kept under scripts/ for re-running boot timing / fetch-trace / sync-XHR microbenchmarks without rebuilding from scratch. - _perf-bench.mjs <N>: runs _perf-detail.mjs N times sequentially, reports min/median/max of cn1Started. - _perf-detail.mjs: full request timeline with relative timestamps (req/fin events). - _perf-lifecycle.mjs: request timeline + PARPAR-LIFECYCLE: console events, useful when runtime-side instrumentation is enabled. - _perf-trace.mjs: top-N slowest fetches; compares TeaVM live and the local bundle. - _synct.mjs: clean-worker microbenchmark for sync XHR cost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more reduction-of-round-trip wins for the worker->main JSO bridge during boot. 1) Cache ``WindowExt.getCn1()`` per host-window receiver The host bridge handle (cn1HostBridge) never changes. Boot queries it ~5 times directly + indirectly through every ``getArrayBufferInputStream`` call. Cache as ``win.__cn1CachedCn1Wrapper``. 2) Negative-cache ``getBundledAssetAsDataURL`` ``HTML5Implementation.getArrayBufferInputStream`` calls ``cn1.getBundledAssetAsDataURL(url)`` for every asset fetch to check whether the host has the bytes embedded inline. Initializr (and the typical CN1 app) embeds none, so all calls return null. Cache the negative result per URL so a second open of the same .res hits an in-worker Set lookup instead of a worker->main->worker round-trip. Together with the OffscreenCanvas measureText + Window/Document caches landed in the previous commit, these shave the boot round-trip count from ~363 -> ~150-180. Wall-clock impact is modest (each round-trip is ~1-5 ms when the worker can saturate the postMessage channel) but each removed round-trip frees the worker for paint-side work and unblocks future optimisation. Local Initializr smoke test: 0 console errors, ``cn1Started`` fires normally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The translator's switch+pc interpreter emits a ``case N:`` for
every instruction index in ``computeJumpTargets``, which adds
``i+1`` to the target set for every non-throwing-checked
instruction so the case-merge pass doesn't inadvertently drop
the body. The result is a label at every "could-throw" boundary
even when no ``pc=N+1`` ever sets it -- pure overhead by the
time we get to peephole.
Empirical: ~30% of post-emit case labels in our switch+pc
methods are dead -- 41,653 stripped on the Initializr build
(140,735 -> 99,083 case labels, -30%). Each label is ~7-9
chars, so ~370 KiB raw saved on translated_app.js (6.94 -> 6.58
MiB raw).
Compared to TeaVM's classes.js (3.44 MiB raw, 19,951 case
labels) we still have ~5x more cases per byte -- the rest comes
from emitting one case per JVM instruction rather than per
suspension boundary, and that's a much bigger rewrite of the
emit. This pass is the cheap easy win.
Method-local pass added to ``applyMethodPeephole`` after the
existing dead-let-decl pass and before the
``stack`` -> ``S`` / ``locals`` -> ``L`` rename. Walks the
outer ``switch(pc){...}`` body at brace depth 0 only -- nested
``switch (__switchValue)`` blocks emitted for Java ``switch``
statements live at depth >= 1 and are left untouched. Builds
the live-target set from:
- hardcoded ``0`` (initial pc value from the prelude)
- all ``pc = <expr>`` writes (digit literals from the RHS)
- ``__cn1TryCatch`` table handler pcs ``{s:N,e:M,h:K}``
Hairy bit: the RHS regex must NOT stop at ``)`` -- expressions
like ``pc = S.q() == null ? 79 : 57`` would truncate at the
``S.q()`` call's close-paren and miss the real target numerals,
producing a runtime NPE when the unstripped case happens to be
hit. ``[^;}]+`` (stops at ``;`` or ``}``) is the right
boundary; over-marking arg literals as live is harmless (we
just keep an unused case label).
Verified the local Initializr smoke test boots with 0 errors.
Validation against full JS-port test suite is running in
parallel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The translator emits ``stack[stack.length - 1]`` for every JVM DUP-style "duplicate top of stack" / "peek" sequence -- ~3.1k occurrences in the Initializr build, ~14 chars each. Add a ``stack.t()`` helper alongside the existing ``stack.p`` / ``stack.q`` push/pop aliases on ``Array.prototype``, and replace via peephole. ``S.t()`` post-rename is 5 chars vs ``S[S.length-1]`` 14 chars -- ~9 chars saved per occurrence, ~28 KiB raw on translated_app.js (6.58 -> 6.55 MiB). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical: ~57k case labels in our switch+pc emit have a single
``pc = N; break`` writer (verified by per-method counter), and
the immediately-following case is N. Each such site is a no-op
loop -- ``set pc, exit switch, re-iterate for(;;), dispatch back
to case N`` -- when nothing else jumps to N.
Collapse them by removing the entire
``pc = N; break } case N: {`` (avg ~18 chars) and merging the
two adjacent case bodies into one. esbuild --minify-syntax does
this for empty case bodies but won't merge across yield-laden
bodies; ours have yields, so most of these survive minify.
Critical bug avoided: the per-case pc-counter must extract every
digit literal from the RHS of ``pc = <expr>`` (including
ternaries like ``pc = cond ? 5 : 3``), not just direct
``pc = N;`` writes. Earlier draft used ``pc\\s*=\\s*(\\d+)\\b``
and counted only direct writes -- it missed ternary targets,
collapsed cases that were still reachable via the ternary path,
and produced runtime NPEs on the Initializr boot. Fixed by
matching ``pc\\s*=\\s*([^;}]+)`` and counting every digit run
in the RHS.
Effect on Initializr translated_app.js:
case labels: 99,083 -> 60,407 (-39%)
pc=N;break}: 87,000+ -> 33,495 (-62%)
raw size: 6.55 MiB -> 6.05 MiB (-500 KiB)
Combined with the dead-case-label strip (commit 72b9777) the
case label count is now 60k, down from 140k at session start
(-57%).
Smoke test (Initializr local bundle): 0 console errors, boot
median 2255 ms (was 2335 ms median).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``Array.prototype.push(...args)`` accepts variadic arguments and
pushes every value in order. ``S.p(X); S.p(Y)`` is semantically
identical to ``S.p(X, Y)`` because the comma operator already
fully evaluates X before Y, and so does push() argument
evaluation.
The translator's per-instruction emit produces each push as its
own statement, separated by ``;`` (and whitespace) at this point
in the pipeline; esbuild later collapses ``;`` to ``,`` but
never combines pushes into the multi-arg form. Doing it here
saves ~5 chars per pair.
Effect on Initializr translated_app.js:
``S.p(`` count: 105,715 -> 91,530 (-14,185 single-arg pushes)
``S.p(X,Y)`` multi-arg: 0 -> 13,185
raw size: 6.05 MiB -> 5.98 MiB (-67 KiB)
Conservative regex: each push arg is captured as ``[^,(){}]+``
so ``yield*$fn(a,b)`` style args (which contain parens) are
left alone. The separator regex ``\s*[;,]\s*`` matches both
the pre-minify ``;`` separator and the post-rule ``,`` form so
the merge fires regardless of which earlier peephole rule
produced its predecessor.
Smoke test: 0 console errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The switch+pc emit prelude ``let L = _F(N, T, A1, A2, ...)`` creates a JS Array as the locals frame; uses are ``L[0]``, ``L[1]``, etc. (4 chars each). Replace with named local declarations ``let l0=T, l1=A1, l2=A2, ..., lN-1`` and rewrite every ``L[i]`` in the body to ``l<i>`` (saves ~2 chars per access). The straight-line emit path already uses named locals for the same reason; this brings the switch+pc path in line with it. Effect on Initializr translated_app.js: raw size: 5.98 MiB -> 5.58 MiB (-137 KiB) Walker tracks string state so theme-key literals containing ``L[`` survive intact. Sanity bound: only fires when the frame size from ``_F(N, ...)`` is in [1, 256] -- pathological sizes fall through to the legacy array form. Smoke test: 0 console errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing peephole rules (Rule 8/8b/9/9b/10/10c/...) inline
1-arg / 2-arg / 3-arg invokes by emitting blocks of the shape
``{ let __arg0=stack.q(); ...stack.p(yield* X(stack.q(),
"method", __arg0)); pc=N; break; }``. The ``__arg<N>`` names
are local to the block but each is 6 chars; on the Initializr
build there are ~25k decl + use sites totalling ~150 KiB.
Extend the per-method ``shortenStackAndLocals`` walker to also
collapse ``__arg<N>`` -> ``_<N>`` (e.g. ``_0``, ``_1``).
Verified ``_0..._9`` are unused as identifiers in the bundle
(all theme-key string literals), so the rename is collision-
free.
Distinct rule from the existing ``__cn1Arg<N>`` -> ``A<N>``
(parameter names at function scope): ``__arg<N>`` is the
peephole-emitted block-local. Both are now compressed.
Effect on Initializr translated_app.js:
raw size: 5.58 MiB -> 5.40 MiB (-176 KiB)
Smoke test: 0 console errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original ``renameLocalsArrayToNamedLocals`` only matched the single-statement ``let L=_F(N, T, A1, ...);`` prelude that the translator emits for methods without long/double arguments. Methods with long/double args use a multi-statement ``_N(N)``-based prelude: let L=_N(N); let S=[]; let pc=0; L[0]=T; L[1]=A1; L[2]=null; L[3]=A2; ... Add a fallback that walks past the ``_N(N)`` decl, collects the contiguous ``L[i]=expr;`` statements (skipping intervening ``let S=[];`` / ``let pc=0;`` lines), and rewrites to a single ``let l0=expr0,l1=expr1,...,lN-1;`` named-local declaration plus ``L[i]`` -> ``l<i>`` substitution in the rest of the body. Effect on Initializr translated_app.js: ALL remaining 3,707 ``L[N]`` accesses (in long/double-arg methods) are now named locals. Modest size win (-6 KiB raw -- the _N-prelude methods are rare) but completes the named-local conversion for consistency. All 617 JS-port tests pass. Smoke test 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inject an inline ``fetch('theme.res')`` script before
``browser_bridge.js`` starts the worker. This kicks off the
network request in parallel with the worker's importScripts
chain. By the time the worker reaches its blocking sync XHR for
the same URL the browser already has the bytes cached.
Earlier ``<link rel="preload" as="fetch" crossorigin="anonymous">``
attempt failed because the explicit ``crossorigin="anonymous"``
downgraded the request to no-credentials mode, mismatching the
worker's default-credentials XHR. Bare ``fetch(url)`` defaults
to same-origin credentials, which IS what the XHR uses, so the
HTTP cache key matches.
We don't pre-fetch ``assets/iOS7Theme.res`` because that path
gets a ``?v=<getBuildVersion()>`` cache-buster appended at sync-
XHR time, and the build version resolves at runtime -- the
preload URL would need the same query string to match the cache
key. theme.res at the bundle root has no cache-buster so it
preloads cleanly.
Effect on Initializr boot: median 1933 -> 1883 ms (-50 ms).
0 console errors; functional smoke test passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit pre-fetched ``theme.res`` only because the ``?v=`` cache-buster appended by HTML5Implementation.getArrayBufferInputStream made the URL key unstable. Build version is actually hardcoded to "1.0" by build-javascript-port-initializr.sh's ByteCodeTranslator invocation (line 334), so we can pre-fetch ``assets/iOS7Theme.res?v=1.0`` with the matching query and populate the HTTP cache for the second blocking XHR too. Boot median: 1883 -> 1864 ms (-20 ms additional). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…*X() JVM ASTORE following an INVOKE emits a push-then-pop sequence (``S.p(yield* X(args)); l<N> = S.q();``). After the previous ``collapseUniqueImmediateCaseFallthrough`` pass merged the post-call case body into the call site, the push and the matching pop end up adjacent in the same case body -- but the existing peephole rules (which target receiver+arg setup PRE-call) don't recognize this post-call shape. Add a final peephole pass that rewrites the push-yield-pop sequence to a direct ``l<N> = yield* X(args)`` assignment. Conservative: arg list captured as either a balanced single- paren group or no inner parens, so calls whose args contain other generator invocations (``yield*$Y(...)`` nested deeper than one level) fall out. ~3,057 sites match on the Initializr build; ~6 chars saved per match. Hairy bit: the regex must accept BOTH ``cn1_<long>`` and ``$<short>`` function names. The mangler is a Python script that runs AFTER the translator, so at applyMethodPeephole time the body still has the long ``cn1_<class>_<method>_<sig>`` form. Earlier draft hardcoded ``\\$`` and silently never matched (dbgPYPCalls=0); switching the function-name match to ``[\\w$]+`` accepts both. Effect on Initializr translated_app.js: raw size: 5.40 MiB -> 5.37 MiB (-39 KiB) Smoke test 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier draft tried extending the ``S.p(X);S.p(Y) -> S.p(X,Y)`` variadic-push merge to cases where Y is a ``yield*`` call. Smoke-tested as a NullPointerException deep in the resume path. Root cause: ``S.p(a), S.p(yield* X())`` evaluates ``a`` first and pushes it to the worker's stack BEFORE yielding into X. The merged form ``S.p(a, yield* X())`` defers the first push past the yield boundary -- ``a`` is held as an evaluated-but-not-yet- pushed call argument while X may yield to the cooperative scheduler. If X (or any callee deeper in the chain) throws during the yield, ``_E(__cn1TryCatch, pc, err, S)`` dispatches the catch handler against the current depth of ``S``; missing the ``a`` entry breaks the handler's stack-shape expectation. Add a comment recording the rationale so the next person tempted to extend the merge doesn't repeat the regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Direct emit of JVM IRETURN/ARETURN-after-push: the bytecode pushes a value to stack then the immediately-following return pops it back. The push-then-pop is a no-op, the value flows directly from X to the function return. Pattern in the pre-esbuild emit: S.p(l1); return S.q(); becomes return l1; esbuild --minify-syntax later transforms our intermediate form into ``return S.p(X),S.q()`` via the comma-sequence shortcut (both expressions evaluated, last expression's value returned), but collapsing here happens BEFORE that pass and produces a shorter ``return X`` directly. Effect on Initializr translated_app.js: raw size: 5.37 MiB -> 5.34 MiB (-22 KiB) ~1,981 sites match. All 617 JS-port tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… register-based slots Walks each method body via abstract interpretation, propagates entry stack depth from case 0 across pc=N;break branches and bare-case fall-through chains, then rewrites S.p(EXPR)/S.q()/S.t() to absolute-slot register assignments s0=.../s0. Replaces "let S = [];" with "let s0,s1,...,sN;". Bails on methods with __cn1TryCatch (the runtime _E helper manipulates the live S array) or on any depth conflict / parse failure -- about 17% of switch+pc methods. Critical correctness rule: top-level break terminates a case (no syntactic fall-through to the next case); only return/throw/break mark terminating. Earlier draft only treated return/throw as terminating which produced spurious depth conflicts when adjacent cases had different verifier- guaranteed entry depths. Reduces translated_app.js from 5,847,209 to 5,546,525 bytes (-300 KiB raw, ~5.1%) on Initializr. Combined with all earlier peephole work this brings the bundle from 8.99 MiB (session-1 baseline) to 5.55 MiB. Lifecycle tests pass; interaction-test failures match the pre-existing baseline (no new regressions). Kill-switch -Dparparvm.js.regs.off=1 via PARPARVM_TRANSLATOR_OPTS for bisecting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some translated case bodies have the shape
case 5: { body1 }
{ body2 }
case 17: { ... }
where body2 is a continuation block that the per-instruction emit
opened without a fresh case label (consecutive non-throwing
instructions in the same merged case can each open their own block).
Original parseCases bailed when it saw a `{` instead of `case` /
`default`, so the rewriter never converted these methods.
Extend the case body to absorb every adjacent dangling `{ ... }`
block until the next `case` / `default` / end-of-switch. Picks up
~160 additional methods on Initializr; saves another ~26 KiB raw
on translated_app.js (5,546,525 → 5,520,458). Six consecutive
lifecycle test runs pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st declaration
After ``rewriteStackToRegisters`` collapses S.p/S.q to register slots,
many cases reduce to ``s<N>=expr; pc=M; break;`` with no
block-scoped binding at the case-body level. The outer wrapping
``{ ... }`` is then pure overhead — esbuild keeps it because the
body contains a ``break`` statement which it doesn't recognize as
safe to unwrap. Detect cases whose body has no top-level ``let``
/ ``const`` / ``function`` / ``class`` declaration (inner ``{...}``
blocks containing ``let`` are still fine — JS scope handles those)
and emit them as bare statement sequences after the case label.
Saves another ~58 KiB raw on translated_app.js (5,520,458 →
5,462,614). Combined with the register-rewrite + parser-extension
this brings the bundle from the session-1 baseline of 8.99 MiB
down to 5.46 MiB. Lifecycle tests pass; interaction tests show
the same pre-existing failure set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oRegisters
The translator can emit ``{ pc=N; break; }`` blocks pointing at
PCs that have no corresponding ``case`` label in the parsed switch
body — typically a dangling continuation after a ``return`` from
the previous instruction's case body, where the would-be target case
was either pruned by RTA or simply never had its label emitted.
Original behaviour: bail on ``Branch to unknown label``; many
methods rejected.
Two changes:
1. Silently skip the branch in propagation when ``labelToIdx.get(N)``
returns null — at runtime the original emit also has nothing
matching ``case N:``, so the dispatcher falls through to
``default:return``. Mirroring that semantics in our rewrite is
safe.
2. After propagation, verify every parsed case either has a
computed entry depth OR has no live S.p / S.q / S.t reference
in its body. If a case ended up unreachable (entry depth -1) but
still contains stack ops, we'd emit a method whose ``let S = []``
is replaced with named registers but that case body still
references the now-undefined ``S``. Bail conservatively in that
shape so we don't ship a method that crashes the first time
runtime dispatch lands in the unrewritten case.
Saves another ~382 KiB raw on translated_app.js (5,462,614 →
5,080,035). Combined with all earlier session-3 work this brings
the bundle from 5,847,209 → 5,080,035 (~767 KiB / ~13% off).
Lifecycle tests pass; interaction-test failures remain the same
as the kill-switch baseline (Tests 1/2/3).
Co-Authored-By: Claude Opus 4.7 (1M context) <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.


















No description provided.