Skip to content

Moving initializr to new JS port#4795

Open
shai-almog wants to merge 144 commits intomasterfrom
moving-initializr-to-new-js-port
Open

Moving initializr to new JS port#4795
shai-almog wants to merge 144 commits intomasterfrom
moving-initializr-to-new-js-port

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

No description provided.

@shai-almog shai-almog force-pushed the moving-initializr-to-new-js-port branch 6 times, most recently from 37159a9 to e273251 Compare April 23, 2026 01:41
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [HTML preview] [Download]
    • ByteCodeTranslator: 2 findings (Low: 2)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented Apr 23, 2026

JavaScript port screenshot updates

Compared 18 screenshots: 0 matched, 18 missing references.

  • AnimateHierarchyScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/AnimateHierarchyScreenshotTest.png.

    AnimateHierarchyScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as AnimateHierarchyScreenshotTest.png in workflow artifacts.

  • AnimateLayoutScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/AnimateLayoutScreenshotTest.png.

    AnimateLayoutScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as AnimateLayoutScreenshotTest.png in workflow artifacts.

  • AnimateUnlayoutScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/AnimateUnlayoutScreenshotTest.png.

    AnimateUnlayoutScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as AnimateUnlayoutScreenshotTest.png in workflow artifacts.

  • ComponentReplaceFadeScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/ComponentReplaceFadeScreenshotTest.png.

    ComponentReplaceFadeScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as ComponentReplaceFadeScreenshotTest.png in workflow artifacts.

  • ComponentReplaceFlipScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/ComponentReplaceFlipScreenshotTest.png.

    ComponentReplaceFlipScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as ComponentReplaceFlipScreenshotTest.png in workflow artifacts.

  • ComponentReplaceSlideScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/ComponentReplaceSlideScreenshotTest.png.

    ComponentReplaceSlideScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as ComponentReplaceSlideScreenshotTest.png in workflow artifacts.

  • CoverHorizontalTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/CoverHorizontalTransitionTest.png.

    CoverHorizontalTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as CoverHorizontalTransitionTest.png in workflow artifacts.

  • FadeTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/FadeTransitionTest.png.

    FadeTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as FadeTransitionTest.png in workflow artifacts.

  • FlipTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/FlipTransitionTest.png.

    FlipTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as FlipTransitionTest.png in workflow artifacts.

  • MotionShowcaseScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/MotionShowcaseScreenshotTest.png.

    MotionShowcaseScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as MotionShowcaseScreenshotTest.png in workflow artifacts.

  • SheetSlideUpAnimationScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/SheetSlideUpAnimationScreenshotTest.png.

    SheetSlideUpAnimationScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as SheetSlideUpAnimationScreenshotTest.png in workflow artifacts.

  • SlideFadeTitleTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/SlideFadeTitleTransitionTest.png.

    SlideFadeTitleTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as SlideFadeTitleTransitionTest.png in workflow artifacts.

  • SlideHorizontalBackTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/SlideHorizontalBackTransitionTest.png.

    SlideHorizontalBackTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as SlideHorizontalBackTransitionTest.png in workflow artifacts.

  • SlideHorizontalTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/SlideHorizontalTransitionTest.png.

    SlideHorizontalTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as SlideHorizontalTransitionTest.png in workflow artifacts.

  • SlideVerticalTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/SlideVerticalTransitionTest.png.

    SlideVerticalTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as SlideVerticalTransitionTest.png in workflow artifacts.

  • SmoothScrollScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/SmoothScrollScreenshotTest.png.

    SmoothScrollScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as SmoothScrollScreenshotTest.png in workflow artifacts.

  • TensileBounceScreenshotTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/TensileBounceScreenshotTest.png.

    TensileBounceScreenshotTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as TensileBounceScreenshotTest.png in workflow artifacts.

  • UncoverHorizontalTransitionTest — missing reference. Reference screenshot missing at /home/runner/work/CodenameOne/CodenameOne/scripts/javascript/screenshots/UncoverHorizontalTransitionTest.png.

    UncoverHorizontalTransitionTest
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as UncoverHorizontalTransitionTest.png in workflow artifacts.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented Apr 23, 2026

Compared 90 screenshots: 90 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 286 seconds

Build and Run Timing

Metric Duration
Simulator Boot 91000 ms
Simulator Boot (Run) 2000 ms
App Install 15000 ms
App Launch 30000 ms
Test Execution 288000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1941.000 ms
Base64 CN1 encode 1837.000 ms
Base64 encode ratio (CN1/native) 0.946x (5.4% faster)
Base64 native decode 1220.000 ms
Base64 CN1 decode 1568.000 ms
Base64 decode ratio (CN1/native) 1.285x (28.5% slower)
Base64 SIMD encode 520.000 ms
Base64 encode ratio (SIMD/native) 0.268x (73.2% faster)
Base64 encode ratio (SIMD/CN1) 0.283x (71.7% faster)
Base64 SIMD decode 521.000 ms
Base64 decode ratio (SIMD/native) 0.427x (57.3% faster)
Base64 decode ratio (SIMD/CN1) 0.332x (66.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 135.000 ms
Image createMask (SIMD on) 46.000 ms
Image createMask ratio (SIMD on/off) 0.341x (65.9% faster)
Image applyMask (SIMD off) 338.000 ms
Image applyMask (SIMD on) 123.000 ms
Image applyMask ratio (SIMD on/off) 0.364x (63.6% faster)
Image modifyAlpha (SIMD off) 168.000 ms
Image modifyAlpha (SIMD on) 202.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.202x (20.2% slower)
Image modifyAlpha removeColor (SIMD off) 229.000 ms
Image modifyAlpha removeColor (SIMD on) 94.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.410x (59.0% faster)
Image PNG encode (SIMD off) 1287.000 ms
Image PNG encode (SIMD on) 1097.000 ms
Image PNG encode ratio (SIMD on/off) 0.852x (14.8% faster)
Image JPEG encode 725.000 ms

Comment thread vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js Fixed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 25, 2026

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 721 total, 0 failed, 2 skipped

Benchmark Results

  • Execution Time: 10630 ms

  • Hotspots (Top 20 sampled methods):

    • 23.43% java.lang.String.indexOf (444 samples)
    • 19.37% com.codename1.tools.translator.Parser.isMethodUsed (367 samples)
    • 11.82% java.util.ArrayList.indexOf (224 samples)
    • 6.60% com.codename1.tools.translator.Parser.addToConstantPool (125 samples)
    • 5.01% java.lang.Object.hashCode (95 samples)
    • 3.38% com.codename1.tools.translator.BytecodeMethod.optimize (64 samples)
    • 2.37% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (45 samples)
    • 2.37% java.lang.System.identityHashCode (45 samples)
    • 1.95% com.codename1.tools.translator.Parser.getClassByName (37 samples)
    • 1.64% com.codename1.tools.translator.ByteCodeClass.fillVirtualMethodTable (31 samples)
    • 1.48% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (28 samples)
    • 1.42% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (27 samples)
    • 1.37% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (26 samples)
    • 1.21% java.lang.StringBuilder.append (23 samples)
    • 1.16% com.codename1.tools.translator.ByteCodeClass.markDependent (22 samples)
    • 0.90% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (17 samples)
    • 0.84% com.codename1.tools.translator.Parser.cullMethods (16 samples)
    • 0.69% com.codename1.tools.translator.BytecodeMethod.appendMethodSignatureSuffixFromDesc (13 samples)
    • 0.63% com.codename1.tools.translator.BytecodeMethod.isMethodUsedByNative (12 samples)
    • 0.63% java.lang.StringCoding.encode (12 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented Apr 26, 2026

Compared 90 screenshots: 90 matched.

Native Android coverage

  • 📊 Line coverage: 10.22% (5584/54613 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.03% (27360/340728), branch 3.70% (1208/32634), complexity 4.68% (1463/31268), method 8.22% (1202/14616), class 13.59% (267/1964)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 10.22% (5584/54613 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.03% (27360/340728), branch 3.70% (1208/32634), complexity 4.68% (1463/31268), method 8.22% (1202/14616), class 13.59% (267/1964)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 843.000 ms
Base64 CN1 encode 145.000 ms
Base64 encode ratio (CN1/native) 0.172x (82.8% faster)
Base64 native decode 869.000 ms
Base64 CN1 decode 187.000 ms
Base64 decode ratio (CN1/native) 0.215x (78.5% faster)
Image encode benchmark status skipped (SIMD unsupported)

@liannacasper liannacasper force-pushed the moving-initializr-to-new-js-port branch 3 times, most recently from 766a374 to 6c6c483 Compare April 30, 2026 14:29
shai-almog and others added 14 commits April 30, 2026 18:24
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>
shai-almog and others added 26 commits May 6, 2026 13:56
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>
After register-rewrite + brace-strip, many methods reduce to a
chain ``case M: STMTS, pc=N; break; ... case N: BODY`` where the
``pc=N`` reference appears in EXACTLY ONE place. The case label
is then pure dispatch overhead — we can move case N's body inline
at the source and drop the label.

The existing ``collapseUniqueImmediateCaseFallthrough`` only
handled IMMEDIATELY adjacent cases (and its post-rewrite pattern
no longer matched after brace-strip stripped the ``}`` between
break and case). The new ``inlineUniqueSourceCases`` pass handles
non-adjacent forward gotos and iterates to fixed point so linear
chains dissolve fully — three or four hops collapse into a single
case body, and every intermediate case label disappears.

Also:
* Extended ``parseCases`` to recognize post-brace-strip case
  shapes (``case N: STMTS`` with no surrounding braces). Bodies
  now extend until the next top-level ``case`` / ``default``.
* parseCases now also records the case-label start position as a
  4th tuple element (used by the inliner to know where to cut
  the case from).

Bails:
* try/catch methods (pc indices must match the runtime table).
* Backward gotos (target case is textually before the source —
  loops). Forward only.
* Cases whose body has a top-level ``let`` / ``const`` /
  ``function`` / ``class`` declaration (would collide with
  sibling-case scope when inlined).
* Ternary pc=cond?A:B sources (rewriting these to if/else is a
  follow-up).

Saves ~228 KiB raw on translated_app.js (5,080,035 → 4,851,943).
Cumulative session-3 win: 5,847,209 → 4,851,943 (~995 KiB / ~17%).
Lifecycle tests pass; interaction tests show the kill-switch
baseline failure pattern (Tests 1/2/4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the unique-source case folder to recognize ternary RHS
shapes:

  case M: STMTS, pc=COND?A:B; break;

When both A and B are unique-source AND both case bodies terminate
(return / throw / pc=...; break) AND neither body has a top-level
``let`` / ``const`` / ``function`` / ``class`` declaration, fold to:

  case M: STMTS; if(COND){bodyA}else{bodyB}

Removing both case A and case B labels.

Saves another ~25 KiB raw on translated_app.js (4,851,943 →
4,826,759). Cumulative session-3 win: 5,847,209 → 4,826,759
(~1,020 KiB / ~17.4%). Lifecycle tests pass; interaction tests
match baseline failure pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ocks

The ternary fold's ``bodyTerminates`` predicate only recognized
top-level ``return`` / ``throw`` / ``break``, missing the very
common shape

  case N: STMTS; { let v = ...; pc=Q; break; }

where the terminator lives inside a trailing nested ``{...}`` block
that the brace-strip pass left intact (for ``let v`` scoping). The
inner ``break`` exits the enclosing switch even from within a
nested block, so the case body still terminates from the switch's
perspective.

Refactored ``bodyTerminates`` to take a range and recurse into the
trailing block when present. Picks up the common
``S.q()``-pop-into-let / call / pc-set / break shape that
INVOKEINTERFACE / INVOKEVIRTUAL emit produces.

Saves another ~63 KiB raw on translated_app.js (4,826,759 →
4,763,749). Cumulative session-3 win: 5,847,209 → 4,763,749
(~1.06 MiB / ~18.5%). Lifecycle tests pass; interaction tests
match the kill-switch baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The translator emits

  function*<name>(T, A1, A2){let l0=T,l1=A1,l2=A2,l3,l4;...}

— params named after the post-rename ``__cn1This`` / ``__cn1Arg<N>``
short forms (T and AN), then a let prelude that copies them into
``l<N>`` locals matching the JVM local index. JS function
parameters are themselves local bindings, so we can name the
params ``l<N>`` directly and drop the copy entirely:

  function*<name>(l0, l1, l2){let l3,l4;...}

Saves ~5 chars per arg per method on the prelude side, costs ~1
char per arg on the param-list side (T → l0 widens by one). Net
~38 KiB on translated_app.js (4,763,749 → 4,727,367).

Bail conditions:
* synchronized methods (the ``let __cn1Monitor = T;`` line lives
  AFTER the locals prelude — that ``T`` would dangle if the param
  were renamed).
* Defensive scan: if any of the param identifiers still appears
  textually in the body after the prelude, bail. Catches odd emit
  shapes the explicit checks miss.
* Wrappers / methods without a recognizable ``let l0=...`` prelude.

Cumulative session-3 win: 5,847,209 → 4,727,367 (~1.07 MiB / 19.2%).
Lifecycle tests pass (3 consecutive runs); interaction tests match
the kill-switch baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… methods

After the inline + ternary fold passes collapse a method down to a
single ``case 0:`` whose body terminates and contains no ``pc=``
references, the surrounding ``for(;;) switch(pc) { case 0: BODY
default:return }`` wrapper just dispatches to BODY once and exits
the for-loop. Strip the wrapper:

  let l0=...,...,pc=0;
  for(;;)switch(pc){case 0:BODY default:return}
  →
  let l0=...,...;
  BODY

Bails when:
* try/catch (the wrapper is meaningful for the catch handler)
* synchronized methods (the try/finally around the wrapper)
* BODY contains another ``case`` label (multi-case, can't strip)
* BODY contains a ``pc=`` assignment (would dangle)
* BODY contains a ``break`` outside any nested ``switch`` /
  ``while`` / ``for`` / ``do`` (would dangle as a SyntaxError once
  the outer wrapper is gone). The translator's TABLESWITCH /
  LOOKUPSWITCH emit produces a trailing outer-switch ``break``
  AFTER an inner switch — this check catches that shape.

Saves ~4 KiB raw on translated_app.js (4,727,367 → 4,723,463). Modest
because only ~180 methods qualify, but the strip plus the existing
fold passes together gave ~1 MiB. Lifecycle tests pass; interaction
tests match baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovered via TeaVM gap analysis that the bulk of the size
difference comes from us using 2-char identifiers (l0, s0, pc) where
TeaVM uses 1-char (a, b, c). Top-frequency identifiers in our
translated_app.js:

   s0  111,781 × 2 chars = 218 KiB
   s1   69,146 × 2 chars = 135 KiB
   pc   51,766 × 2 chars = 101 KiB
   l0   40,335 × 2 chars =  79 KiB

We can't enable esbuild's --minify-identifiers because it renames
top-level too, and our $XX short-form names are cross-file
referenced from parparvm_runtime.js / port.js. So this pass does
the local-only rewrite ourselves, per method:

  1. Walk the function body once collecting:
     * usage frequency of every l<N> / s<N> / pc identifier,
     * the set of single-letter names already used by inner
       block-let temporaries (a, b, v, etc) — those become
       reserved.
  2. Build alias pool from a-zA-Z minus reserved minus JS keywords.
  3. Sort rename targets by usage frequency (descending) so the
     hottest local gets the shortest alias.
  4. Apply rename throughout the function body (string-literal
     aware).

Saves ~430 KiB raw on translated_app.js (4,727,367 → 4,292,637).
Cumulative session-3 win: 5,847,209 → 4,292,637 (~1.48 MiB / ~26.6%).
Lifecycle tests pass (3 consecutive); interaction tests match the
kill-switch baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a post-emit pass in JavascriptBundleWriter.writeTranslatedClasses
that rewrites the most-used pure-identifier double-quoted strings in
each translated_app[_NN].js chunk to short ``_qN`` const aliases at
the top of the file.

Initializr Initializr-js bundle:
- before: 4,292,637 bytes
- after:  4,139,005 bytes  (-150 KiB, ~3.6%)

Both Initializr and HelloCodenameOne lifecycle tests still pass.

Why pure-identifier-only matches: a body containing escape characters
could share textual overlap with a different JS string after an escape
sequence we don't decode, so restricting to ``[A-Za-z0-9_]+`` keeps
the byte-level substitution provably safe -- the literal "BODY" can
only appear inside another string by being followed by either a
closing delimiter or a non-identifier byte we'd notice.

Why a const prelude: esbuild minification only collapses identifiers
and whitespace, not string literals. A const alias declared at top of
chunk is in scope for every translated method and class registration,
with one-time binding cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoist pass added in f3dd52d only matched bodies of
``[A-Za-z_][A-Za-z0-9_]*`` shape. The translator's mangle scheme
also emits ``$``-prefixed names like ``$Lb`` / ``$XX`` inside quoted
strings (args to ``_O("$Lb")`` class lookups + dispatch-id args).
These share the same safety property as plain identifier bodies (no
escape sequences possible), so widen the alphabet to include ``$``.

Marginal gain on Initializr (-647 bytes; 4,139,005 -> 4,138,358) --
most ``$``-prefixed strings are short 3-char names, but worth keeping
the alphabet unified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the hoist pass landed in f3dd52d / 67129b8. When a name
already has a const alias (because its quoted occurrences justified
hoisting), also rewrite ``,KEY:VAL`` -> ``,[ALIAS]:VAL`` for the
unquoted obj-key occurrences inside class-table entries like
``_Z({m:{cn1_s_getName_R_java_lang_String:$cMI,...}})``.

Initializr Initializr-js bundle:
- before: 4,138,358 bytes
- after:  4,132,423 bytes  (-5,935 bytes)

Why ``,`` only and not ``{``: ``{ KEY: ... }`` is also valid as a
*block* containing a labeled statement, and the translator emits both
shapes. After ``,`` we're always inside a list context (function args,
array, obj literal); only obj literals accept ``KEY:`` shape, so
matching after ``,`` is unambiguous. This skips the first key of each
object literal but keeps every subsequent key, which is enough to
recover most of the byte savings on the ``_Z({m:{...}})`` registries
that dominate the obj-key uses.

Both Initializr and HelloCodenameOne lifecycle tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the inlineUniqueSourceCases fold pass with a second sweep:
when a case label has 2 or 3 incoming branches (all simple
``pc=N;break;`` -- no ternary RHS), all forward, and the body is
short enough that duplicating saves bytes vs keeping the case label,
inline the body at every source and drop the case.

Cost model per fold:
  saved = caseSpan + sum(srcLen) - incoming * bodyLen
where caseSpan is the full ``case N: BODY`` length and srcLen is the
``pc=N;break;`` site length (~11 bytes). For incoming=2 with a body
under ~10 chars (e.g. ``c=0,d=14``), each fold saves ~20-30 bytes.

Initializr Initializr-js bundle:
- before: 4,132,423 bytes
- after:  4,122,396 bytes  (-10,027 bytes)

Both Initializr and HelloCodenameOne lifecycle tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fold pass already gates each rewrite on
``incoming * bodyLen < caseSpan + sum(srcLen)`` so anything that
costs more bytes than it saves is rejected; widen the upper limit
from 3 to 6 incomings so the gate sees the larger N candidates.
The ceiling stays as a guardrail against pathological hub cases.

Initializr Initializr-js bundle: 4,122,396 -> 4,122,360 bytes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rame

Adds a deep integration test (scripts/test-blackbar-textfield.mjs) that
reproduces a regression where clicking the Initializr "MyAppName" text
field made the "Main Class" label above it stop rendering -- the canvas
region went transparent and the page background showed through.

Root cause: ``drainPendingDisplayFrame`` was unconditionally calling
``context.clearRect`` at the union of all dirty components' bounds. When
two non-adjacent components both queue a repaint -- here, the
``TextField`` (y=243..276) and the right-aligned "?" help button on the
row above (y=217..243) -- ``CodenameOneImplementation.paintDirty`` unions
their absolute bounds into a single crop rect (y=217..276) and calls
``flushGraphics`` once with that union. The actual paint ops only cover
each component's own clip, so the "Main Class" Label between the two
got cleared but never refilled, leaving alpha=0 pixels.

Fix: skip the clearRect when the crop is *not* the entire canvas.
Partial-frame drains rely on each component's own bg fill to overwrite
stale pixels in its own bounds; sibling components whose bounds happen
to fall inside the union but who are NOT in the dirty list keep their
previous pixels (which is the intended behaviour). Full-canvas drains
(form transitions) still clear, preserving the title-bar accumulation
fix the original clearRect was added for.

Verification:
- Reproducer test (test-blackbar-textfield.mjs): label-strip
  transparent fraction was 85.2% post-click (FAIL), now 0.0% (PASS).
- Both Initializr and HelloCodenameOne lifecycle tests still pass.
- After-click screenshot shows "Main Class" / "Package" labels
  preserved with the native edit overlay correctly attached at the
  field's bounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 7, 2026

Compared 90 screenshots: 90 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 308 seconds

Build and Run Timing

Metric Duration
Simulator Boot 91000 ms
Simulator Boot (Run) 0 ms
App Install 14000 ms
App Launch 5000 ms
Test Execution 216000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1519.000 ms
Base64 CN1 encode 1584.000 ms
Base64 encode ratio (CN1/native) 1.043x (4.3% slower)
Base64 native decode 1046.000 ms
Base64 CN1 decode 1269.000 ms
Base64 decode ratio (CN1/native) 1.213x (21.3% slower)
Base64 SIMD encode 415.000 ms
Base64 encode ratio (SIMD/native) 0.273x (72.7% faster)
Base64 encode ratio (SIMD/CN1) 0.262x (73.8% faster)
Base64 SIMD decode 471.000 ms
Base64 decode ratio (SIMD/native) 0.450x (55.0% faster)
Base64 decode ratio (SIMD/CN1) 0.371x (62.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 83.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.108x (89.2% faster)
Image applyMask (SIMD off) 129.000 ms
Image applyMask (SIMD on) 56.000 ms
Image applyMask ratio (SIMD on/off) 0.434x (56.6% faster)
Image modifyAlpha (SIMD off) 149.000 ms
Image modifyAlpha (SIMD on) 91.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.611x (38.9% faster)
Image modifyAlpha removeColor (SIMD off) 187.000 ms
Image modifyAlpha removeColor (SIMD on) 94.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.503x (49.7% faster)
Image PNG encode (SIMD off) 1425.000 ms
Image PNG encode (SIMD on) 967.000 ms
Image PNG encode ratio (SIMD on/off) 0.679x (32.1% faster)
Image JPEG encode 565.000 ms

Adds scripts/test-initializr-features.mjs which boots the bundle and
walks every interactive surface in the Initializr form, opening a
fresh page per scenario so leftover modal/menu state from one
scenario cannot pollute the next. Each sub-test asserts on canvas
pixels (transparent / opaque-white / opaque-dark fractions, color
deltas in known hot spots) and on worker liveness.

Scenarios covered:
  1. textfield: click MyAppName, expect "Main Class" label preserved
     (the d91a4f9 fix)
  2. dialog: click Hello-World, dialog body should fill with white bg
  3. side-menu: hamburger animation should not flicker through many
     distinct canvas states
  4. template-buttons: each radio click should swap the selection --
     previously-selected goes away from blue, clicked goes toward
     blue. Polls up to 15 s for the click effect to settle (heavy
     theme reloads can take several seconds).
  5. toggle-mashing: 60 rapid alternating clicks; worker must remain
     responsive afterwards.

Current observed failures (with d91a4f9 in place AND in baseline):
  - 04-template-kotlin: BAREBONES does NOT redraw to unselected when
    KOTLIN is clicked -- ButtonGroup.deselect repaint does not reach
    the canvas
  - 04-template-grub/tweet/barebones: subsequent clicks (after the
    first kotlin click) are dropped or not painted -- worker is
    blocked from processing further input for >15 s
  - 05-toggle-mashing: 60 rapid clicks leave the worker unresponsive

These all reproduce on the pre-fix baseline too, so they are NOT
regressions from d91a4f9. The d91a4f9 fix actually IMPROVES the
dialog body opacity (79% white vs 26% on baseline) and preserves the
"Main Class" textfield + help-icon layout that the baseline corrupts
on first template click. Tracked separately for follow-up fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
async function scenarioToggleMashing() {
const s = await bootScenario('05-toggle-mashing');
try {
const sigBefore = await canvasSig(s);
Root cause analysis (using scripts/test-kotlin-trace.mjs which hooks
the worker dispatch table):

Clicking a template button (KOTLIN, GRUB, TWEET) on the Initializr
form fired:
  Button.released
  -> Button.fireActionEvent
  -> Template.setTemplate
  -> Template.updateMode
  -> Template.createBarebonesPreviewForm
  -> Template.restoreThemeDefaults
  -> Resources.getThemeResourceNames -> leave
  -> Resources.getTheme -> leave
  -> UIManager.setThemeProps     <-- enters but never leaves

UIManager.setThemeProps drives an EDT-bound setThemePropsImpl that
runs buildTheme + LookAndFeel.refreshTheme(true) + a complete
createStyle sweep across every cached UIID. On the JS port worker
this never returned within 60 s, which manifested as:
  - "Kotlin button instantly freezes the UI"
  - Subsequent template clicks dropped (action listener thread is
    still spinning in setThemeProps so the dispatch loop never
    returns)
  - "UI freezes eventually when I press the toggle buttons too much"
    (toggle clicks also wind up in the same theme-reset path via
    options-changed -> refresh.run -> setTemplate -> ...
    -> restoreThemeDefaults)

Fix: only call restoreThemeDefaults when the *previous* template
load actually mutated the theme via custom CSS. When no custom CSS
is active (the default case for the published Initializr UI), the
global theme has not been touched, so resetting it is a no-op
that just happens to get stuck.

Track the last applied custom CSS in a new field; reset only when
transitioning out of a non-empty custom-CSS state. The
applyCustomCssToPreview path still handles the live re-apply when
the user IS editing custom CSS, so the user-facing CSS preview
behaviour is unchanged.

Verification (scripts/test-initializr-features.mjs):
  - 01-textfield ........ PASS (label preserved, d91a4f9 fix)
  - 02-dialog ........... PASS (79.2% white body)
  - 03-side-menu ........ PASS (no flicker)
  - 04-template clicks .. PASS (KOTLIN/GRUB/TWEET/BAREBONES each
                                swap selection; previously-selected
                                button transitions away from blue)
  - 05-toggle-mashing ... PASS (worker stays responsive after 60
                                rapid alternating clicks)

Pre-fix this commit + the d91a4f9 paint fix the same test
recorded 8 sub-test failures.

Also adds scripts/test-kotlin-trace.mjs as a diagnostic harness
(adapted from test-initializr-interaction.mjs) that surfaces the
exact CN1 method that hangs, so future EDT-stall investigations
have a ready-made trace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant