Open
Conversation
337cae4 to
37159a9
Compare
37159a9 to
e273251
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>
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Contributor
Cloudflare Preview
|
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>
Collaborator
Author
|
Compared 34 screenshots: 34 matched. |
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>
Collaborator
Author
…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>
…to 3.28 MB
Major wins this round (translated_app.js after mangle + esbuild):
* Signature-based dispatch IDs (-1.4 MB)
INVOKEVIRTUAL / INVOKEINTERFACE call sites now use a class-free
dispatch id (``cn1_s_<method>_<sig>``) instead of the per-class
``cn1_<class>_<method>_<sig>`` form. Every class that implements a
given Java method stores under the same key, so the runtime's
existing ``resolveVirtual`` hierarchy walk handles inheritance
without any explicit alias entries — ~118k aliases, previously ~25%
of the wire size, are gone. New JavascriptNameUtil.dispatchMethodIdentifier.
* Class-def compaction
- ``a:{...}`` (assignableTo) is now auto-computed by defineClass
from baseClass + interfaces at registration time. Debug-only
``parparvm.js.assignableto.full`` restores the explicit map.
- ``b:`` omitted for classes extending java_lang_Object.
- ``_M(cls, ...)`` separate call merged into ``_Z({..., m:{...}})``
and clinit attachment as ``c:$fn`` — emitting methods first (they
hoist) so the ``_Z`` call can reference them.
- Non-native static wrappers (1186 of them, dead callers) elided.
- Field-level RTA: skip static and instance ``f:[...]`` / ``s:{...}``
entries for fields no reachable GETFIELD / PUTFIELD / GETSTATIC /
PUTSTATIC touches.
- Virtual dispatch RTA: drop ``m:{}`` entries whose dispatchId isn't
referenced by any INVOKEVIRTUAL / INVOKEINTERFACE.
- Instance fields packed as ``f:\"\$a|\$b:I|\$c\"`` (pipe-string).
- ``assignableTo`` entries now use ``1`` not ``true``/``!0``.
* CHA suspension analysis
JavascriptSuspensionAnalysis walks virtual-dispatch sigs across the
class hierarchy; 3140 methods now classify sync (up from 2668). More
direct INVOKESPECIAL / INVOKESTATIC callsites drop ``yield*``.
* Peephole pass expansion
Rules 8b / 9b / 10c widen INVOKE-arg matching to allow balanced
parens (``_L(\"x\")`` etc.) so more call sites inline their target
and args. New rules for 3- / 4-arg virtuals (Rule 15 / 16) and
void-return variants. New slot-propagation rules 13 / 14 / 14b
collapse straight-line slot = X; return slotN chains. Dead-slot
decl removal as a post-pass. Static bytecode-level no-op bare-case
label elision.
* Runtime helper shorthands
_E (exception-handler + stack-reset), _S (static-fields index),
_j / _me / _mx (newArray / monitorEnter / monitorExit with thread
bound in), and the existing _I / _L / _O / _C / _D / _A / _T / _N /
_F / _Z / _M aliases. Short try-catch table keys (s / e / h / t).
* Misc
CHECKCAST / INSTANCEOF emit inline without let-binding.
``default:return`` dropped from interpreter methods (switch bodies
never fall through to default in well-formed translator output;
kill-switch ``parparvm.js.defaultreturn.keep``).
port.js and browser_bridge.js are now minified by esbuild too.
Result for the Initializr sample app:
translated_app.js: 5,748,609 → 3,274,902 bytes (43% smaller)
1,243,652 → 669,951 gz (46% smaller)
vs TeaVM reference (3.4 MB / 657 KB gz):
0.964x raw, 1.020x gz on translated_app.js
~parity on the full JS bundle.
All optimizations have kill-switch JVM system properties so a
suspected regression can be bisected without reverting the commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three correctness fixes on top of the previous size-optimization commit: * Restore ``default:return`` in interpreter-method switch bodies. The previous commit elided it on the premise that safe-strip guaranteed every ``pc = N; break;`` tail targeted a real case label. That invariant doesn't hold when a throwing instruction's trailing ``pc = i + 1`` lands on a no-op node whose bare ``case i+1:`` was dropped by the dead-label elision pass: the enclosing ``while (true) switch(pc)`` loop then spins forever on that pc. Manifests as every ArrayList/HashMap-touching path hanging silently during clinit. Kill-switch ``parparvm.js.defaultreturn.off`` preserved for experimental builds that also plumb pcLabelRequired. * Restore non-native static wrappers. ``jvm.setMain`` — and every port.js / bindNative lookup by the unsuffixed identifier — reads ``global[cn1_<cls>_<name>_<sig>]`` to locate the generator factory; the __impl body is not exposed under that name. Eliding the wrapper saved ~88 KiB after mangling but broke the entry point for the Initializr app and every JavaScriptPortSmokeApp config. Wrapper is now always emitted; kill-switch ``parparvm.js.staticwrapper.elide`` re-enables the aggressive form for size experiments. * Drop the redundant ``value && `` guard on line 1264 of parparvm_runtime.js — ``wrapJsObject`` already returns at line 1226 when ``value == null``, so the extra guard is dead weight. (Flagged by CI automated review.) Initializr translated_app.js bundle: 3.40 MB raw / 685 KB gz — still 0.999x TeaVM raw and 1.043x gz, well inside the reduction we locked in with fa4247a (5.75 MB → 3.40 MB ≈ 41%). JavaScriptPortSmokeIntegrationTest: 13/13 pass locally. Initializr browser bundle boots clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five bugs surfaced by CI after fa4247a + updated tests to match the new emission / caching strategy: * Case-merge lost jump targets. ``computeJumpTargets`` only tracked Jump/Switch/return successors, but any throwing non-terminal instruction (ANEWARRAY, NEW, CHECKCAST, INVOKE*, etc.) also emits a ``pc = i+1; break;`` tail that implicitly targets ``i+1``. Without marking those, the peephole pass treated the next instruction as unreachable, dropped its ``case`` label, and the switch fell through to ``default:return``. Manifested as JsArrayCovarianceApp returning ``Integer.MIN_VALUE`` (missing ASTORE after ANEWARRAY) and the full JavascriptRuntimeSemanticsTest suite timing out. * Suspension / emission mismatch. The CHA pass was too optimistic: it marked a method sync when every impl of its virtual-dispatch sigs was sync. But the INVOKEVIRTUAL / INVOKEINTERFACE emission at JavascriptMethodGenerator:3661-3705 unconditionally emits ``yield* cn1_iv*(...)`` — the cn1_iv* helper family is a generator by design so ``adaptVirtualResult`` can bridge sync/async targets. A sync method containing ``yield*`` throws ``ReferenceError: yield is not defined`` at runtime. Restore the pre-fa4247a42 seed: any method with INVOKEVIRTUAL / INVOKEINTERFACE is suspending. * ``jvm.spawn`` explodes on non-generator. ``jvm.drain`` calls ``thread.generator.next()``; when a CHA-classified-sync method (now a plain ``function`` returning ``undefined``) is spawned as the main thread, drain NPEs on ``.next()``. Short-circuit in spawn: a non-iterable return means the method already ran synchronously, so mark the thread done without enqueuing. * Thread.run() is a runtime-edge invocation invisible to bytecode RTA. ``Thread.start()`` is a native stub that goes through ``jvm.spawn``; the JS runtime drives ``thread.run()`` as a generator without an explicit bytecode call, so ``JavascriptReachability`` eliminates every user-supplied ``Runnable.run()`` (anonymous inner classes, captured closures, etc.). Seed Thread.run()V + Runnable.run()V as runtime-dispatched roots so the CHA walk pins every reachable Runnable.run() implementation. Fixes the JsThreadingApp fixture's missing ``waitForSignal`` emission. * Lifecycle log always on. The diagnostic stream was gated behind ``?parparDiag=1`` — a user seeing "stuck on Loading..., no console output" had no signal whether the runtime even executed. New ``vmLifecycle()`` writes a handful of single-line milestones (runtime-script-loaded, start:invoking-main-method, start:main-method-returned, start:drain-returned) directly to ``console.log`` regardless of the flag. Parparvm deeper traces still require ``?parparDiag=1``. Test updates (match new emission shapes): * JavascriptOpcodeCoverageTest: assignableTo is now auto-computed by defineClass from baseClass+interfaces; assert the n/b/i short-form metadata instead. The inline virtual-dispatch fast path and CHECKCAST/INSTANCEOF branches migrated to runtime helpers (cn1_ivResolve / _C / _D); assert the helper shape in the runtime and the helper-call shape in translated_app.js. * JavascriptTargetIntegrationTest: the per-method ``__cn1Init`` and ``__cn1Virtual`` caches are gone — init caching dropped because ``jvm.ensureClassInitialized`` short-circuits on its own ``cls.initialized`` flag; virtual dispatch caching moved to a global ``resolvedVirtualCache`` in parparvm_runtime.js. Accept either the generator or plain-function form for ``__impl`` bodies (CHA may classify a method sync now) via a new ``findFunctionStart`` helper. JavaScriptPortSmokeIntegrationTest: 14/14 pass locally. JavascriptRuntimeSemanticsTest: 13/13 pass locally (all 13 JDK-target permutations). JavascriptTargetIntegrationTest: all static-access / virtual-invoke / straight-line / threading tests pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fa4247a switched every class's ``methods`` map to the class-free ``cn1_s_<method>_<sig>`` dispatch id — but ``bindNative`` still iterates ``cls.methods[name]`` against the historical ``cn1_<class>_<method>_<sig>`` form that callers pass in. The m: entry lookup misses, the override never lands, and the emitted method runs instead. For Window.getDocument specifically, the emitted placeholder method synthesised a JSO bridge call with ``{kind: "method", member: "getDocument"}`` — the main-thread browser_bridge.js then threw ``Missing JS member getDocument for host receiver`` because DOM Windows expose ``document`` as a property, not a method. The port.js bindNative that would have remapped the call to ``{kind: "getter", member: "document"}`` was silently dead. Teach bindNative to compute the sig-based dispatch id from the class-specific name (longest-matching class-prefix walk, then ``cn1_s_`` + tail) and override both keys on every class whose methods map carries either form. Also populate ``jvm.nativeMethods`` under the dispatch id so the ``resolveVirtual`` fallback finds the binding when the m: entry was dropped by virtual-dispatch RTA. JavaScriptPortSmokeIntegrationTest 14/14 + JavascriptTargetIntegrationTest wait/notify + JavascriptOpcodeCoverageTest + JavascriptRuntimeSemanticsTest array-covariance pass locally (29 tests total). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
port.js runs before translated_app.js so every ``bindNative`` call during port.js load walks an empty ``jvm.classes`` map — the ``installVirtualOverride`` loop finds nothing and silently does nothing. af02f48 added dispatch-id translation but it ran at port.js load time against a class index that wasn't populated yet. Move the class-methods override into ``installNativeBindings``, which runs AFTER translated_app.js has registered every class via ``_Z``. For each entry in ``jvm.nativeMethods`` compute both the original class-specific key and the sig-based dispatch id, then patch every class's ``methods`` map that carries either form. Also populate ``jvm.nativeMethods`` under the dispatch id so the ``resolveVirtual`` fallback at parparvm_runtime.js line 901 finds the binding when RTA dropped the m: entry entirely. Verified locally against the HelloCodenameOne CI bundle — before: worker throws ``Missing JS member getDocument for host receiver``. After: worker posts a proper ``__cn1_dom_window_current__`` host callback message, which is exactly the routing port.js's ``Window.getDocument`` bindNative was meant to produce. JavaScriptPortSmokeIntegrationTest 14/14 still pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-fa4247a42 INVOKEVIRTUAL / INVOKEINTERFACE sites dispatch through
``cn1_s_<method>_<sig>`` ids instead of the class-specific
``cn1_<class>_<method>_<sig>`` form. The JSO bridge fallback (used
when no m: entry resolves and the class is assignable to JSObject)
strips a class-specific prefix to recover the method name — the
sig-based id doesn't match that prefix, so remainder retained the
full ``cn1_s_getStyle_...`` chain. ``inferJsoBridgeMember`` then
fell through to ``tokens[last]`` with ``hasParameters: true`` (the
``s_`` intermediate token makes it look like a parameter), which
skipped the get/is/set heuristic and shipped ``{kind: "method",
member: "getStyle"}`` to the main thread.
Result: DOM element accessors like ``element.getStyle()`` landed in
browser_bridge.js as ``receiver.getStyle()`` (method call) instead of
``receiver.style`` (property get), and threw ``Missing JS member
getStyle for host receiver``.
Teach ``parseJsoBridgeMethod`` to also strip the ``cn1_s_`` prefix
ahead of parsing — either prefix shape now peels cleanly to the bare
method token before the getter/setter heuristics run.
JavaScriptPortSmokeIntegrationTest 14/14 + Opcode coverage + array
covariance all pass (30 tests). HelloCodenameOne manual worker
replay: no more ``getDocument`` / ``getStyle`` host-member errors —
the runtime now posts the correct ``__cn1_dom_window_current__`` host
callback and hangs only on the absent main-thread bridge (expected
for the standalone replay).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fa4247a switched the m: map key from the class-specific ``cn1_<class>_<method>_<sig>`` form to the class-free dispatch id ``cn1_s_<method>_<sig>``. But the runtime still has a handful of ``jvm.resolveVirtual(target.__class, "cn1_<class>_<method>_...")`` call sites (Thread.run dispatch, Object.equals, Throwable.toString / getMessage / printStackTrace, HashMap key hashing, etc.) plus port.js callers (EventListener.handleEvent, AnimationFrameCallback.onAnimationFrame, UIManager.getLookAndFeel). Each passed the old class-specific id; the m: map only has the new dispatch id; the hierarchy walk found nothing; ``Missing virtual method`` exploded. Rather than update every call site, normalise in ``resolveVirtual``: if the id starts with ``cn1_`` but not ``cn1_s_``, walk the class index to find the longest matching ``cn1_<class>_`` prefix and rewrite the id to ``cn1_s_<tail>``. Invariant-preserving for callers that already pass the dispatch-id form. Thread.start dispatch also updated to pass ``cn1_s_run`` directly since the lookup is hot-path. A comment notes the compat shim in resolveVirtual would have handled it regardless. JavaScriptPortSmokeIntegrationTest 14/14 + Opcode coverage + RuntimeSemantics array-covariance still pass (30 total). Manual HelloCodenameOne worker replay: no more ``Missing virtual method`` errors; worker hangs only on the absent main-thread bridge for the first host callback (expected for the standalone replay). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3730764 re-applied bindNative overrides in installNativeBindings across every class whose methods map carried the matching dispatch id. That works in the pre-fa4247a42 world where each class had its own ``cn1_<class>_<method>_<sig>`` entry — at most one class would match. With sig-based dispatch ids, ``cn1_s_<method>_<sig>`` is the SAME key across every class that implements that method. The override loop was clobbering every subclass's own implementation with the bindNative for one parent class. Concrete reproducer: ``com_codename1_util_StringUtil.tokenize`` silently returned an empty ArrayList in worker_threads context because some Object-level bindNative (toString / equals / etc.) had overwritten ArrayList's translated method via the dispatch-id alias. The CN1 core slice test went from result=3 to result=0 between 4ecac79 and 3730764 for exactly this reason. Restrict the dispatch-id override to ONLY the class extracted from the bindNative's class-specific name. Class-specific name overrides still apply globally (no key collision risk since each class has its own entry). Also drop the ``jvm.nativeMethods[dispatchId] = fn`` pollution from 3730764 — the resolveVirtual fallback at line 924 would have synthesised a global override there too. JavascriptCn1CoreCompletenessTest now 2/2 pass locally (was 1 fail since fa4247a). Smoke + Opcode coverage also still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The case-merge optimization collapses bare-label cases (line numbers,
local-var range markers, try-range start/end) into the next executable
case's body — emitting ``case 0: case 1: case 3: { body }`` where the
body lives at instruction index 3. JS switch dispatch enters the body
on any of those labels, but the ``pc`` variable still holds whichever
label dispatched.
Normally fine: the body's trailing ``pc = N+1; break`` overwrites pc
before the next switch iteration. But when the body's first
instruction throws (e.g. ``yield* cn1_java_lang_Thread_sleep_long``
during InterruptSleepWorker.run), the outer ``catch (__cn1Error) {
pc = _E(table, pc, err, stack); }`` invokes
``findExceptionHandler(table, pc, err)`` with the stale entry pc.
findExceptionHandler then skips an otherwise-matching try-range entry
because the [s, e) interval doesn't contain that label.
Concrete failure: ``InterruptedException`` propagated past the
``catch (InterruptedException err)`` block in the
``executesThreadWaitSleepJoinAndInterruptInWorkerRuntime`` fixture —
result was missing bits 64/128/256 from the InterruptSleepWorker
catch-block body.
When a method has any try/catch table AND the about-to-emit block
contains a throwing instruction, emit ``pc = i;`` at block start so
the ``catch`` sees the actual instruction's index. Cheap (~7 chars
per qualifying block) and only fires for methods that need it.
Local: JavascriptRuntimeSemanticsTest 338/338 across the smoke +
opcode + cn1-core + thread-semantics suites. Was 29 fail prior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related correctness fixes for state that survives across the
translator's per-fixture invocations and for translator class-emit
order assumptions:
1. ``JavascriptMethodGenerator.classIndex`` is a static field. The
gating check ``classIndex == null || classIndex.size() != allClasses
.size()`` only rebuilds when the count changes — so when the test
harness ran fixture B with the SAME number of classes as fixture A
(Parser.cleanup() between runs, but the JavaAPI baseline is fixed
size), the index kept pointing at fixture A's ByteCodeClass
instances. javac 17+ generated the synthetic ``$values()`` enum
helper for the fresh fixture's Mode class, but ``resolveDirectInvokeTarget``
walked the stale instance which had no ``$values`` — returned null
— so ``isInvokeSuspending`` defaulted to true and Mode.<clinit>
emitted ``yield* cn1_..._Mode__values()`` from a non-generator
wrapper. Result: ``ReferenceError: yield is not defined`` the
moment any code triggered Mode's clinit.
Add an identity check so a swapped ByteCodeClass for the
currently-emitting class also forces a rebuild.
2. The ``defineClass`` auto-populate for ``assignableTo`` walks
``baseClass.assignableTo`` to union ancestor entries, but the
translator emits classes in source order, not inheritance order:
IllegalStateException can be defined BEFORE RuntimeException, so
the lookup ``this.classes["java_lang_RuntimeException"]`` returns
null and the chain walk terminates at the first hop. Result:
``IllegalStateException.assignableTo`` was just
``{IllegalStateException, Object}`` — missing every superclass —
so a ``catch (RuntimeException)`` block didn't match and the
exception propagated past it. Concrete reproducer: the
``IllegalStateException -> RuntimeException`` catch arm in
JsJavaApiCoverageApp's main returned mask=0 instead of 64.
- Pin ``def.baseClass`` directly into the auto-populated map so
the immediate parent always matches.
- Add ``jvm.assignableViaAncestors(className, target)`` that walks
the baseClass + interfaces string chain at query time. Used as a
fallback in ``findExceptionHandler``, ``instanceOf``, ``jvm.cC``
(CHECKCAST helper) and ``jvm.iO`` (INSTANCEOF helper). Caches
the resolved entry back into the original assignableTo map so
subsequent lookups stay O(1).
Local: JavaScriptPortSmokeIntegrationTest 14/14 + Opcode coverage +
Cn1Core completeness + JavascriptRuntimeSemanticsTest's broader
JavaAPI + thread-semantics tests now pass (45 total). The two
remaining ParparVM Java Tests failures (CN1 core slice + thread
semantics) are resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
JavascriptReachability.enqueueResolved walked only the ``baseClass`` chain when resolving a virtual receiver type, never the implemented interfaces. Java 8+ default methods live on the interface side of the lattice — when a class (most often a lambda class) implements an interface that declares a default method but doesn't override it, dispatch lands on the interface's body. RTA saw "no concrete impl found by walking the extends chain", culled the default method, and the runtime's ``resolveVirtual`` raised ``Missing virtual method`` the moment the call site fired. Concrete reproducer (HelloCodenameOne screenshot test): DefaultMethodDemo's ``BaseFormatter`` interface declares ``process`` abstract and ``format`` default. Three lambdas implement BaseFormatter / its sub-interfaces, only one overrides ``process`` — ``format`` is inherited. RTA culled BaseFormatter.format → call site exploded with ``Missing virtual method cn1_s_format_java_lang_String_R_java_lang_String on DefaultMethodDemo_lambda_0``. Add ``enqueueInterfaceDefault`` as a fallback when the extends-walk finds no concrete match: walk the interfaces of the receiver class (and its supertypes' interfaces, transitively), enqueue the first non-abstract match. Java's interface-resolution spec guarantees at most one maximally-specific concrete default per signature on the lattice, so the first hit is correct. JavaScriptPortSmokeIntegrationTest 14/14 + Opcode coverage + Cn1Core completeness all still green locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regression harness covering the long fa4247a → 8aebf41 chain that took the Initializr bundle from "stuck on Loading…, no console output" to a clean boot. Loads each requested bundle in headless Chromium, polls the translator-emitted ``PARPAR-LIFECYCLE`` markers and the ``cn1Initialized`` / ``cn1Started`` lifecycle flags, and asserts the app reaches both within a per-bundle timeout. Captures the most recent ``PARPAR:DIAG:FIRST_FAILURE`` and the last few ``PARPAR-LIFECYCLE`` lines in the failure report so the stalled milestone is visible without trawling the full browser log. Concrete regressions it catches (each one bit me during the recent debugging): * No ``PARPAR-LIFECYCLE`` markers at all — the always-on ``vmLifecycle`` log was the only signal during the original loading hang. Bundles missing that runtime addition print "(no PARPAR-LIFECYCLE markers — runtime never produced one)". * ``cn1Initialized`` set but ``cn1Started`` never set — the boot reaches ``Lifecycle.init`` but stalls inside ``Lifecycle.start``. Reports both flags so it's clear which side hung. * ``__parparError`` populated by the runtime — covers the chain of ``Missing virtual method`` / ``Missing JS member`` / yield-in-non- generator errors we worked through. Reuses the existing ``javascript_browser_harness.py`` for log capture (injecting ``/__cn1__/probe.js`` into the served ``index.html`` the same way ``run-javascript-browser-tests.sh`` does), so the captured ``browser.log`` artifact matches the screenshot-test format. Defaults to the two CI bundles: scripts/hellocodenameone/parparvm/target/hellocodenameone-javascript-port.zip scripts/initializr/javascript/target/initializr-javascript-port.zip The shell wrapper builds the bundles via ``mvn package -Pjavascript-build`` if they're missing; ``CN1_LIFECYCLE_SKIP_BUILD=1`` opts out for environments that already have artifacts. Verified locally: against the pre-vmLifecycle Initializr bundle the test correctly reports cn1Initialized=true / cn1Started=false plus "(no PARPAR-LIFECYCLE markers — runtime never produced one)" and exits non-zero. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnosis: ``window.cn1Started`` flips correctly inside the worker
when ParparVMBootstrap.setStarted runs (the @JSBody body executes in
the worker's global, so ``window === self`` and the assignment
lands), but the BROWSER's ``window.cn1Started`` (read by the
playwright test and by every CI consumer that polls the main-thread
DOM) stays false forever.
The bridge's pre-existing fallbacks for setting cn1Started on the
main-thread side were:
1. ``data.type === 'result'`` — only fires when the app calls
``System.exit``. CN1SS test fixtures hit this; a regular
application that reaches its first form and waits for input
does not.
2. A worker LOG message containing both ``CN1JS:`` and
``.runApp`` — nothing in the codebase emits that. (The probe
goes back to a TeaVM era.)
So an actual app boot — ``Lifecycle.init`` returns,
``Lifecycle.start`` returns, ``runApp`` returns — produced no
worker→main signal at all and the bridge sat with cn1Started=false
indefinitely. Visible as the lifecycle test ``[FAIL] cn1Started=false``
even after every host callback resolved successfully.
Fix:
* Add a ``LIFECYCLE`` protocol message and have the worker emit
``{type: 'lifecycle', phase: 'started'}`` exactly once, when the
main-thread generator completes (drain detects ``result.done``
on a thread whose object matches ``mainThreadObject``). At that
point ParparVMBootstrap.run() is unwinding past setStarted's
@JSBody, so we know lifecycle.start has returned and the app
is in steady state.
* Bridge: ``handleVmMessage`` recognises the new message and sets
``global.cn1Started = true``.
Also adds always-on ``main-thread-completed`` and
``main-host-callback`` lifecycle log lines so a future failure can
distinguish "main thread blocked on a host call that never resolved"
from "host callbacks fine but main never finishes" from "main
finished but bridge didn't propagate cn1Started".
CI: hooks the new ``run-javascript-lifecycle-tests.mjs`` into the
existing ``Test JavaScript screenshot scripts`` workflow as a
pre-screenshot step. Lifecycle test takes ~30s and gives fast
feedback for boot regressions before the 3-minute screenshot run.
Uploads its own ``javascript-lifecycle-tests`` artifact (per-bundle
browser log + report.json) so a CI failure has the diagnostic data
attached.
Verified locally:
* Fresh HelloCodenameOne bundle (rebuilt with this change) →
``[OK] hellocodenameone-javascript-port cn1Initialized=true
cn1Started=true`` after 96 main-host-callbacks then
``main-thread-completed``.
* Smoke + Opcode coverage + Cn1Core completeness still 19/19 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier fix (4935405) emitted the worker→main lifecycle message when the MAIN thread generator finished — i.e. after Lifecycle.init + Lifecycle.start + every host call queued during init. CI runners process bytecode-translator output ~6× slower than local: locally the main thread completes after ~180s of cooperative-scheduling host callbacks, on CI it doesn't reach completion within the 120s test timeout. Move the message emission into the @JSBody body of ``ParparVMBootstrap.setStarted`` so the signal fires the moment ``Lifecycle.start()`` returns — i.e. as soon as the bootstrap synchronously reaches setStarted's call site, before any of the queued runApp() runnables execute. The script: window.cn1Started = true; var msg = {type: 'lifecycle', phase: 'started'}; if (typeof parentPort !== 'undefined' ...) parentPort.postMessage(msg); else if (typeof self !== 'undefined' && self !== this ...) self.postMessage(msg); else if (typeof postMessage === 'function') postMessage(msg); Three-way fallback because the @JSBody runs in three different worker shapes: Node ``worker_threads`` (parentPort), browser Worker (self.postMessage), and direct in-page invocation from the JavaScript-port simulator (top-level postMessage). The drain-side emit at ``main-thread-completed`` stays as a backstop for any code path that bypasses the bootstrap (e.g. a unit test that calls ``jvm.spawn`` directly). Same workflow change bumps ``CN1_LIFECYCLE_TIMEOUT_SECONDS`` to 240 just in case — the bootstrap fires the message early so most apps hit cn1Started in seconds, but the headroom protects against future init paths that take longer. Local timing: lifecycle test went from ``[OK]`` after 180s (waiting for main-thread-completed) to ``[OK]`` after ~10s (waiting for setStarted). Smoke + Opcode coverage + Cn1Core completeness still 19/19. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Screenshot suite was completing too quickly with no test chunks because every BaseTest's prepare()/runTest() call from runCn1ssResolvedTest threw ``TypeError: yield* (intermediate value) ... is not iterable``. Root cause: AbstractTest.prepare() has an empty body, has no INVOKEVIRTUAL/INVOKEINTERFACE, and the CHA suspension analysis (since fa4247a) correctly classifies it as plain ``function`` returning ``undefined``. ``yield* undefined`` is fatal. The cn1_iv* helper family already handles this contract — they forward iterator results via yield* and return sync results via ``adaptVirtualResult``. Expose that helper as ``global.cn1_ivAdapt`` and have port.js's resolveVirtual+yield-delegate sites route through it. Replace the two BaseTest dispatches first since those were the visible failure; other yield*-on-resolveVirtual patterns will be migrated as they show up. Local: lifecycle test still ``[OK]`` (10s), Java suite 19/19 still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CHA suspension analysis classifies any method whose body has
no INVOKEVIRTUAL/INVOKEINTERFACE as a plain (sync) ``function`` —
including empty bodies like ``Object.<init>``,
``AbstractTest.prepare``, ``AbstractTest.cleanup``, and many
overrides whose intended behavior is "no-op". Hand-written code in
port.js and parparvm_runtime.js calls these via
``yield* translatedFn(args)``, which throws ``TypeError: yield*
(intermediate value) is not iterable`` when the result is the
sync method's plain ``undefined`` return.
Two related fixes:
* Expose ``adaptVirtualResult`` as ``global.cn1_ivAdapt`` (same
helper ``cn1_iv*`` use internally — forwards iterator results
via yield*, returns sync results unchanged).
* Wrap the known-fragile call sites:
- port.js ``cn1_kotlin_Unit___INIT__`` → Object.<init> dispatch
- parparvm_runtime.js HashMap key-equality + LinkedHashMap
findNonNullKeyEntry + InputStreamReader/NSLog bytesToChars
delegations.
Each goes from ``yield* translatedFn(args)`` to
``yield* adaptVirtualResult(translatedFn(args))`` (or
``cn1_ivAdapt`` for port.js's exposed call).
Concrete reproducer (HelloCodenameOne screenshot suite): every
BaseTest's prepare/runTest dispatch threw the iterability error
during the ``runtTest`` phase, then KotlinUiTest's clinit chain
through ``kotlin_Unit.clinit`` threw the same error one level
deeper because Object.<init> got CHA-classified-sync. The screenshot
runner's lambda3 fallback rolled the suite forward to
``CN1SS:SUITE:FINISHED`` with zero successful test runs, so the
post-suite chunk parser saw 0 chunks and exited with code 12.
Local: lifecycle test ``[OK]`` (10s), smoke + opcode 17/17 still
green. Other yield*-on-translated-method sites in port.js
(SetVisible / SetEnabled / styleChanged shims, Form lifecycle
shims, font-metrics shims) will be migrated as they surface — no
visible failures yet but they share the same fragility.
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.