From e7e4dc916d784a72d536d959f96c4bb9dd177555 Mon Sep 17 00:00:00 2001 From: James Roome Date: Wed, 1 Jul 2026 17:32:22 +0530 Subject: [PATCH 1/3] Fix for `ArrayIndexOutOfBoundsException` in `Frame.merge` ("incompatible stack heights") --- .../compiler/internal/BranchTarget.java | 62 +++++++++++ .../compiler/internal/WasmAnalyzer.java | 104 +++++++++++++----- .../run/endive/testing/TrySaveStackTest.java | 16 +++ .../compiled/try_save_stack.wat.wasm | Bin 254 -> 305 bytes .../src/main/resources/wat/try_save_stack.wat | 24 ++++ 5 files changed, 179 insertions(+), 27 deletions(-) create mode 100644 compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java diff --git a/compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java b/compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java new file mode 100644 index 000000000..7fe03ccc3 --- /dev/null +++ b/compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java @@ -0,0 +1,62 @@ +package run.endive.compiler.internal; + +import java.util.List; +import java.util.function.Function; +import run.endive.wasm.types.AnnotatedInstruction; +import run.endive.wasm.types.FunctionBody; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.Instruction; +import run.endive.wasm.types.OpCode; +import run.endive.wasm.types.ValType; + +/** + * A resolved branch target: its direction, the enclosing scope it targets, and that scope's block + * type. Shared by ordinary-branch and try_table catch-handler unwinding in {@link WasmAnalyzer}. + */ +final class BranchTarget { + + final boolean forward; + final Instruction scope; + final FunctionType blockType; + + private BranchTarget(boolean forward, Instruction scope, FunctionType blockType) { + this.forward = forward; + this.scope = scope; + this.blockType = blockType; + } + + /** The values kept on top: block results (forward) or loop params (backward). */ + List keepTypes() { + return forward ? blockType.returns() : blockType.params(); + } + + /** + * Resolves a branch to {@code label} from {@code fromIns}. + * + * @param body the function body containing the branch and its target + * @param fromIns the branch instruction + * @param label the target label, as an index into {@code body}'s instructions + * @param functionType the enclosing function's type, used when the target is its implicit block + * @param blockTypeOf maps a scope to its block type (i.e. {@code WasmAnalyzer::blockType}), + * which needs the module's type section + */ + static BranchTarget resolve( + FunctionBody body, + AnnotatedInstruction fromIns, + int label, + FunctionType functionType, + Function blockTypeOf) { + boolean forward = true; + var target = body.instructions().get(label); + if (target.address() <= fromIns.address()) { + target = body.instructions().get(label - 1); + forward = false; + } + var scope = target.scope(); + if (scope.opcode() == OpCode.END) { + // special scope for the function's implicit block + return new BranchTarget(forward, TypeStack.FUNCTION_SCOPE, functionType); + } + return new BranchTarget(forward, scope, blockTypeOf.apply(scope)); + } +} diff --git a/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java b/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java index cf13e2af9..2d0006e7e 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java +++ b/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java @@ -168,6 +168,8 @@ public static class TryCatchBlock { // types of values below the try scope that need to be saved/restored int savedStackSlotBase; List savedStackTypes; + // per-catch DROP_KEEP applied before branching to the catch target (null if none) + CompilerInstruction[] catchUnwinds; // set to true when TRY_CATCH_BLOCK was emitted (i.e., block is reachable) boolean registered; @@ -428,6 +430,22 @@ public AnalysisResult analyze(int funcId) { tryCatchBlock.savedStackSlotBase = saveSlotBase; tryCatchBlock.savedStackTypes = new ArrayList<>(belowTypes); + // A catch branches like a br, so precompute the unwind that + // drops values below each catch target (see catchUnwind). + var catches = ins.catches(); + tryCatchBlock.catchUnwinds = new CompilerInstruction[catches.size()]; + for (int ci = 0; ci < catches.size(); ci++) { + tryCatchBlock.catchUnwinds[ci] = + catchUnwind( + functionType, + body, + ins, + catches.get(ci).resolvedLabel(), + belowCount, + tryCatchBlock.savedStackTypes, + stack); + } + // operands: [saveSlotBase, belowCount, type_ids...] long[] saveOperands = new long[2 + allTypes.size()]; saveOperands[0] = saveSlotBase; @@ -939,6 +957,10 @@ private static void analyzeTryCatchEnd( result.add(new CompilerInstruction(CompilerOpCode.CATCH_REGISTER_EXCEPTION)); break; } + // unwind values below the catch target before branching to it + if (tryCatchBlock.catchUnwinds != null && tryCatchBlock.catchUnwinds[i] != null) { + result.add(tryCatchBlock.catchUnwinds[i]); + } result.add( new CompilerInstruction(CompilerOpCode.GOTO, catchCondition.resolvedLabel())); result.add(new CompilerInstruction(CompilerOpCode.LABEL, afterCatchLabel)); @@ -1671,41 +1693,21 @@ private Optional unwindStack( int label, TypeStack stack) { - boolean forward = true; - - var target = body.instructions().get(label); - if (target.address() <= ins.address()) { - target = body.instructions().get(label - 1); - forward = false; - } - var scope = target.scope(); - - FunctionType blockType; - if (scope.opcode() == OpCode.END) { - // special scope for the function's implicit block - scope = FUNCTION_SCOPE; - blockType = functionType; - } else { - blockType = blockType(scope); - } - - var types = forward ? blockType.returns() : blockType.params(); - int keep = types.size(); + var target = BranchTarget.resolve(body, ins, label, functionType, this::blockType); + int keep = target.keepTypes().size(); // scope may have been exited already (unreachable code after unconditional branch) - var scopeSize = stack.scopeStackSize(scope); + var scopeSize = stack.scopeStackSize(target.scope); if (scopeSize == null) { return Optional.empty(); } - // for a backward jump, the initial loop parameters are dropped + // for a backward jump the initial loop parameters are dropped; for a forward jump + // the return values are kept rather than dropped int drop = stack.types().size() - scopeSize; - - // do not drop the return values for a forward jump - if (forward) { - drop -= types.size(); + if (target.forward) { + drop -= keep; } - if (drop <= 0) { return Optional.empty(); } @@ -1723,6 +1725,54 @@ private Optional unwindStack( new CompilerInstruction(CompilerOpCode.DROP_KEEP, operands.build().toArray())); } + /** + * The DROP_KEEP a try_table catch handler applies before branching to {@code label}: like a + * {@code br}, it drops values below the target scope (see {@link #unwindStack}). {@code + * savedStackTypes} are the {@code belowCount} restored below-try values, bottom-to-top. + */ + private CompilerInstruction catchUnwind( + FunctionType functionType, + FunctionBody body, + AnnotatedInstruction tryIns, + int label, + int belowCount, + List savedStackTypes, + TypeStack stack) { + + var target = BranchTarget.resolve(body, tryIns, label, functionType, this::blockType); + + // kept values are the catch results, matching the target label arity + var keepTypes = target.keepTypes(); + int keep = keepTypes.size(); + + var scopeSize = stack.scopeStackSize(target.scope); + if (scopeSize == null) { + // target scope already exited (unreachable code) + return null; + } + + // reconstructed stack is [belowCount below-try values, keep caught values]; + // drop down to the target scope, like unwindStack + int drop = (belowCount + keep) - scopeSize; + if (target.forward) { + drop -= keep; + } + if (drop <= 0) { + return null; + } + + // operands: [drop, drop_types..., keep_types...] (dropped = top `drop` of below-try) + var operands = LongStream.builder(); + operands.add(drop); + for (ValType t : savedStackTypes.subList(belowCount - drop, belowCount)) { + operands.add(t.id()); + } + for (ValType t : keepTypes) { + operands.add(t.id()); + } + return new CompilerInstruction(CompilerOpCode.DROP_KEEP, operands.build().toArray()); + } + private FunctionType blockType(Instruction ins) { var typeId = ins.operand(0); if (typeId == 0x40) { diff --git a/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java b/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java index 2a2d7e581..c9be09b52 100644 --- a/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java +++ b/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java @@ -53,4 +53,20 @@ public void nestedTryValues(Function machine var instance = machineInject.apply(Instance.builder(MODULE)).build(); assertEquals(6, instance.export("nested-try-values").apply()[0]); } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void catchDropsValueAboveTarget( + Function machineInject) { + var instance = machineInject.apply(Instance.builder(MODULE)).build(); + assertEquals(7, instance.export("catch-drops-value-above-target").apply()[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void catchKeepsValueBelowTarget( + Function machineInject) { + var instance = machineInject.apply(Instance.builder(MODULE)).build(); + assertEquals(1007, instance.export("catch-keeps-value-below-target").apply()[0]); + } } diff --git a/wasm-corpus/src/main/resources/compiled/try_save_stack.wat.wasm b/wasm-corpus/src/main/resources/compiled/try_save_stack.wat.wasm index 018fa702174af2fe5dd9f5b16197862b0c517bc8..bb3b6c2750f7036d3c7e2ffb468fdd28aca078c9 100644 GIT binary patch delta 169 zcmeyzxRGgsh#)%~10x9VGBYwTus1NWPE^v2l1ol3NzTwsDatP>)-6lSDNWT)Ov*1y z)h$UZN>43eV1_ElPECa?NJ`DgFNY~$5zI?1E=f(%Eh(ym7*Nc>%GJflHZjReM}&c? g-tjSmyaUii21j-Q25ts!ZV3j*7wnkg6Q`*I0Cu}98UO$Q delta 121 zcmdnU^pA0Z2p=m810y3NFEb+p1G^pbL{&|3!MxPslGGI4lA=o8vc#Oy)M5r^t{9ey zE6gVIFseE-*t6s%=B6@oGcs_b dropped when the catch branches + (try_table (catch $e $t) + (call $do_throw (i32.const 7)) + ) + (unreachable) + ) ;; $t yields the caught 7 + ) + + ;; Like above, but a value below the target label survives the unwind. + (func (export "catch-keeps-value-below-target") (result i32) + (i32.const 1000) ;; below $t -> survives + (block $t (result i32) + (i32.const 99) ;; above $t -> dropped + (try_table (catch $e $t) + (call $do_throw (i32.const 7)) + ) + (unreachable) + ) + (i32.add) ;; 1000 + 7 = 1007 + ) + ;; Nested try_table with values below both levels (func (export "nested-try-values") (result i32) (i32.const 1) ;; below outer try From b88cd615709461eeaf6f03cdd7f6b5e567927a6c Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 1 Jul 2026 18:34:01 +0100 Subject: [PATCH 2/3] Inline branch target resolution, remove BranchTarget class --- .../compiler/internal/BranchTarget.java | 62 ------------------- .../compiler/internal/WasmAnalyzer.java | 59 ++++++++++++++---- 2 files changed, 46 insertions(+), 75 deletions(-) delete mode 100644 compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java diff --git a/compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java b/compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java deleted file mode 100644 index 7fe03ccc3..000000000 --- a/compiler/src/main/java/run/endive/compiler/internal/BranchTarget.java +++ /dev/null @@ -1,62 +0,0 @@ -package run.endive.compiler.internal; - -import java.util.List; -import java.util.function.Function; -import run.endive.wasm.types.AnnotatedInstruction; -import run.endive.wasm.types.FunctionBody; -import run.endive.wasm.types.FunctionType; -import run.endive.wasm.types.Instruction; -import run.endive.wasm.types.OpCode; -import run.endive.wasm.types.ValType; - -/** - * A resolved branch target: its direction, the enclosing scope it targets, and that scope's block - * type. Shared by ordinary-branch and try_table catch-handler unwinding in {@link WasmAnalyzer}. - */ -final class BranchTarget { - - final boolean forward; - final Instruction scope; - final FunctionType blockType; - - private BranchTarget(boolean forward, Instruction scope, FunctionType blockType) { - this.forward = forward; - this.scope = scope; - this.blockType = blockType; - } - - /** The values kept on top: block results (forward) or loop params (backward). */ - List keepTypes() { - return forward ? blockType.returns() : blockType.params(); - } - - /** - * Resolves a branch to {@code label} from {@code fromIns}. - * - * @param body the function body containing the branch and its target - * @param fromIns the branch instruction - * @param label the target label, as an index into {@code body}'s instructions - * @param functionType the enclosing function's type, used when the target is its implicit block - * @param blockTypeOf maps a scope to its block type (i.e. {@code WasmAnalyzer::blockType}), - * which needs the module's type section - */ - static BranchTarget resolve( - FunctionBody body, - AnnotatedInstruction fromIns, - int label, - FunctionType functionType, - Function blockTypeOf) { - boolean forward = true; - var target = body.instructions().get(label); - if (target.address() <= fromIns.address()) { - target = body.instructions().get(label - 1); - forward = false; - } - var scope = target.scope(); - if (scope.opcode() == OpCode.END) { - // special scope for the function's implicit block - return new BranchTarget(forward, TypeStack.FUNCTION_SCOPE, functionType); - } - return new BranchTarget(forward, scope, blockTypeOf.apply(scope)); - } -} diff --git a/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java b/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java index 2d0006e7e..b31968e37 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java +++ b/compiler/src/main/java/run/endive/compiler/internal/WasmAnalyzer.java @@ -1693,21 +1693,41 @@ private Optional unwindStack( int label, TypeStack stack) { - var target = BranchTarget.resolve(body, ins, label, functionType, this::blockType); - int keep = target.keepTypes().size(); + boolean forward = true; + + var target = body.instructions().get(label); + if (target.address() <= ins.address()) { + target = body.instructions().get(label - 1); + forward = false; + } + var scope = target.scope(); + + FunctionType blockType; + if (scope.opcode() == OpCode.END) { + // special scope for the function's implicit block + scope = FUNCTION_SCOPE; + blockType = functionType; + } else { + blockType = blockType(scope); + } + + var types = forward ? blockType.returns() : blockType.params(); + int keep = types.size(); // scope may have been exited already (unreachable code after unconditional branch) - var scopeSize = stack.scopeStackSize(target.scope); + var scopeSize = stack.scopeStackSize(scope); if (scopeSize == null) { return Optional.empty(); } - // for a backward jump the initial loop parameters are dropped; for a forward jump - // the return values are kept rather than dropped + // for a backward jump, the initial loop parameters are dropped int drop = stack.types().size() - scopeSize; - if (target.forward) { - drop -= keep; + + // do not drop the return values for a forward jump + if (forward) { + drop -= types.size(); } + if (drop <= 0) { return Optional.empty(); } @@ -1739,22 +1759,35 @@ private CompilerInstruction catchUnwind( List savedStackTypes, TypeStack stack) { - var target = BranchTarget.resolve(body, tryIns, label, functionType, this::blockType); + boolean forward = true; + + var target = body.instructions().get(label); + if (target.address() <= tryIns.address()) { + target = body.instructions().get(label - 1); + forward = false; + } + var scope = target.scope(); + + FunctionType blockType; + if (scope.opcode() == OpCode.END) { + scope = FUNCTION_SCOPE; + blockType = functionType; + } else { + blockType = blockType(scope); + } - // kept values are the catch results, matching the target label arity - var keepTypes = target.keepTypes(); + var keepTypes = forward ? blockType.returns() : blockType.params(); int keep = keepTypes.size(); - var scopeSize = stack.scopeStackSize(target.scope); + var scopeSize = stack.scopeStackSize(scope); if (scopeSize == null) { - // target scope already exited (unreachable code) return null; } // reconstructed stack is [belowCount below-try values, keep caught values]; // drop down to the target scope, like unwindStack int drop = (belowCount + keep) - scopeSize; - if (target.forward) { + if (forward) { drop -= keep; } if (drop <= 0) { From 1f00a23b4c037149ed7431dcb04169f2e8a769af Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 1 Jul 2026 18:34:05 +0100 Subject: [PATCH 3/3] Add backward-catch tests for try_table --- .../run/endive/testing/TrySaveStackTest.java | 15 ++++++ .../compiled/try_save_stack.wat.wasm | Bin 305 -> 567 bytes .../src/main/resources/wat/try_save_stack.wat | 47 ++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java b/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java index c9be09b52..2fca6e830 100644 --- a/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java +++ b/machine-tests/src/test/java/run/endive/testing/TrySaveStackTest.java @@ -54,6 +54,21 @@ public void nestedTryValues(Function machine assertEquals(6, instance.export("nested-try-values").apply()[0]); } + @ParameterizedTest + @MethodSource("machineImplementations") + public void catchBackwardToLoop(Function machineInject) { + var instance = machineInject.apply(Instance.builder(MODULE)).build(); + assertEquals(42, instance.export("catch-backward-to-loop").apply()[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void catchBackwardToLoopDrop( + Function machineInject) { + var instance = machineInject.apply(Instance.builder(MODULE)).build(); + assertEquals(42, instance.export("catch-backward-to-loop-drop").apply()[0]); + } + @ParameterizedTest @MethodSource("machineImplementations") public void catchDropsValueAboveTarget( diff --git a/wasm-corpus/src/main/resources/compiled/try_save_stack.wat.wasm b/wasm-corpus/src/main/resources/compiled/try_save_stack.wat.wasm index bb3b6c2750f7036d3c7e2ffb468fdd28aca078c9..9a38462602bb6529566b2b6c2a46f33134e42bb9 100644 GIT binary patch literal 567 zcmZ{h?MlNi6o$`9`sIROFhmiw0}&hqZ(;fGJ-Dn3&aP`lny&0uFUwo-9{j1Gmtnfr zB7Wq@oAAERIXNL9ixmO@9?&V)Fa@mLjy2tj0Nb@s9S}ZYc({>SsrXD~y5iPs0f)BI zeCHP5Y;uuEn=JU;=ylP?T+Z}HaVyP(vOqgvsZKu*%_P+l>X`;02=qVu*kZK@3QJIcXj&;v-dc4-Xz9D7K6Za^i@A@>u>(U#CzD?)R9-NydR46C)$qJ zDudF?(kesz$2?btHM#l(mDP8t delta 77 zcmdnavXM!ZA+b1@k%57Mk&`Kbv7RA;fw7*MosEGJ1bCT&GVBeEtP^#zB?R+Qi%U{d dbW4gVb;}ZSN>hs&Sh>0w*(N?qnLL|O6#y#!5kUX| diff --git a/wasm-corpus/src/main/resources/wat/try_save_stack.wat b/wasm-corpus/src/main/resources/wat/try_save_stack.wat index 7548401f4..0c3cbbb4d 100644 --- a/wasm-corpus/src/main/resources/wat/try_save_stack.wat +++ b/wasm-corpus/src/main/resources/wat/try_save_stack.wat @@ -59,6 +59,53 @@ (i32.add) ;; 1000 + 7 = 1007 ) + ;; Catch branches backward to a loop. + (func (export "catch-backward-to-loop") (result i32) + (local $count i32) + (local $result i32) + (i32.const 0) + (loop $L (param i32) (result i32) + (local.set $result) + (local.get $count) + (if (then + (local.get $result) + (return) + )) + (local.get $count) + (i32.const 1) + (i32.add) + (local.set $count) + (try_table (catch $e $L) + (call $do_throw (i32.const 42)) + ) + (unreachable) + ) + ) + + ;; Catch branches backward to a loop, dropping an intermediate value. + (func (export "catch-backward-to-loop-drop") (result i32) + (local $count i32) + (local $result i32) + (i32.const 0) + (loop $L (param i32) (result i32) + (local.set $result) + (local.get $count) + (if (then + (local.get $result) + (return) + )) + (local.get $count) + (i32.const 1) + (i32.add) + (local.set $count) + (i32.const 999) ;; above $L -> must be dropped by catch unwind + (try_table (catch $e $L) + (call $do_throw (i32.const 42)) + ) + (unreachable) + ) + ) + ;; Nested try_table with values below both levels (func (export "nested-try-values") (result i32) (i32.const 1) ;; below outer try