Skip to content

Commit 6adfa4f

Browse files
shai-almogclaude
andcommitted
js-port: merge throwing instructions into one case in handler-free methods
Per-suspension-boundary emission step: in methods with no exception wrapper (~91% of methods), a throwing instruction (method call, field/ array access, NEW, ...) no longer forces its own case + pc=N+1; break; tail. That break+redispatch existed only so exception unwind could resume at the right frame pc; with no handler wrapper a thrown error propagates straight out of the generator and yield* resumes inline via delegation, so it's pure overhead. - computeJumpTargets takes forceThrowingTargets; only adds the post-throw i+1 target when the method has an exception wrapper. - mergeThrowing (gated !hasTryCatch, kill switch parparvm.js.merge.throwing.off) extends the safe-strip to throwing ops. Modest on its own (~0.21MB; case scaffolding is only ~7% of the bundle -- the bulk is per-bytecode statement emission, addressed by the expression-folding work that follows) but a correct foundational step. node --check clean; CI JS screenshot suite is the functional gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5259c7d commit 6adfa4f

1 file changed

Lines changed: 27 additions & 4 deletions

File tree

vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2205,8 +2205,17 @@ private static void appendMethodImpl(StringBuilder out, StringBuilder regs, Byte
22052205
// shape. Flip via the JVM system property
22062206
// ``parparvm.js.merge.off``.
22072207
boolean mergeCases = System.getProperty("parparvm.js.merge.off") == null;
2208+
// When the method has no exception-dispatch wrapper (no try/catch),
2209+
// throwing instructions (method calls etc.) need neither a pc update
2210+
// nor a case boundary -- a thrown error propagates straight out of the
2211+
// generator and yield* resumes inline. So we can merge them into the
2212+
// same case, collapsing the per-call ``pc=N+1; break;`` + ``case N+1:``
2213+
// pairs that dominate the emitted size. Gated off by
2214+
// ``parparvm.js.merge.throwing.off`` as a kill switch.
2215+
boolean mergeThrowing = mergeCases && !hasTryCatch
2216+
&& System.getProperty("parparvm.js.merge.throwing.off") == null;
22082217
java.util.Set<Integer> jumpTargets = mergeCases
2209-
? computeJumpTargets(instructions, labelToIndex)
2218+
? computeJumpTargets(instructions, labelToIndex, !mergeThrowing)
22102219
: java.util.Collections.<Integer>emptySet();
22112220
boolean blockOpen = false;
22122221
// True when the previous emission was a bare ``case N:`` label
@@ -2271,7 +2280,10 @@ private static void appendMethodImpl(StringBuilder out, StringBuilder regs, Byte
22712280
// access), conflating its pc with the next is harmless.
22722281
boolean nextIsNewBlock = i + 1 >= instructions.size() || jumpTargets.contains(i + 1);
22732282
boolean isTerminal = isTerminatingInstruction(instruction);
2274-
boolean strip = !isTerminal && !nextIsNewBlock && isNonThrowingInstruction(instruction);
2283+
// In a wrapper-free method (mergeThrowing) a throwing op needs no
2284+
// pc/break either -- merge it straight-line like a non-throwing op.
2285+
boolean strip = !isTerminal && !nextIsNewBlock
2286+
&& (isNonThrowingInstruction(instruction) || mergeThrowing);
22752287
if (isTarget || pendingBareLabel) {
22762288
if (blockOpen) {
22772289
out.append(" }\n");
@@ -3737,7 +3749,8 @@ private static boolean isPcSkippableNoOp(Instruction instruction) {
37373749
* label is purely decorative and a preceding ``pc = i; break;``
37383750
* may be omitted.
37393751
*/
3740-
private static java.util.Set<Integer> computeJumpTargets(List<Instruction> instructions, Map<Label, Integer> labelToIndex) {
3752+
private static java.util.Set<Integer> computeJumpTargets(List<Instruction> instructions, Map<Label, Integer> labelToIndex,
3753+
boolean forceThrowingTargets) {
37413754
java.util.Set<Integer> targets = new java.util.HashSet<Integer>();
37423755
for (int i = 0; i < instructions.size(); i++) {
37433756
Instruction instr = instructions.get(i);
@@ -3808,7 +3821,17 @@ private static java.util.Set<Integer> computeJumpTargets(List<Instruction> instr
38083821
// and drops the instruction there — producing a switch
38093822
// whose body targets a missing case label and silently
38103823
// falls through to ``default:return``.
3811-
if (!(instr instanceof Jump) && !(instr instanceof SwitchInstruction)
3824+
//
3825+
// This pc-jump exists ONLY so exception unwind can resume at
3826+
// the right pc (findExceptionHandler reads the frame pc). In a
3827+
// method with NO exception-dispatch wrapper a thrown error just
3828+
// propagates out of the generator and the break+re-dispatch is
3829+
// pure overhead, so the caller passes forceThrowingTargets=false
3830+
// and the throwing op merges straight-line into the same case
3831+
// (its tail is safe-stripped). yield* method calls resume inline
3832+
// via generator delegation, so no case label is needed there.
3833+
if (forceThrowingTargets
3834+
&& !(instr instanceof Jump) && !(instr instanceof SwitchInstruction)
38123835
&& !(instr instanceof LabelInstruction) && !(instr instanceof LineNumber)
38133836
&& !(instr instanceof LocalVariable) && !(instr instanceof TryCatch)
38143837
&& !isTerminatingInstruction(instr) && !isNonThrowingInstruction(instr)

0 commit comments

Comments
 (0)