diff --git a/aot-tests/pom.xml b/aot-tests/pom.xml index 1c7b876a3..16f652297 100644 --- a/aot-tests/pom.xml +++ b/aot-tests/pom.xml @@ -152,8 +152,9 @@ utf8-import-module.wast utf8-invalid-encoding.wast + - SpecV1TcReturnCallTest.test19,SpecV1TcReturnCallTest.test24,SpecV1TcReturnCallTest.test25,SpecV1TcReturnCallTest.test30,SpecV1TcReturnCallTest.test31,SpecV1TcReturnCallIndirectTest.test40,SpecV1TcReturnCallIndirectTest.test41,SpecV1TcReturnCallIndirectTest.test46,SpecV1TcReturnCallIndirectTest.test47 + SpecV1BinaryTest.test40,SpecV1TcReturnCallTest.test19,SpecV1TcReturnCallTest.test24,SpecV1TcReturnCallTest.test25,SpecV1TcReturnCallTest.test30,SpecV1TcReturnCallTest.test31,SpecV1TcReturnCallIndirectTest.test40,SpecV1TcReturnCallIndirectTest.test41,SpecV1TcReturnCallIndirectTest.test46,SpecV1TcReturnCallIndirectTest.test47 diff --git a/runtime-tests/pom.xml b/runtime-tests/pom.xml index 6a6759684..1879f940a 100644 --- a/runtime-tests/pom.xml +++ b/runtime-tests/pom.xml @@ -115,7 +115,11 @@ proposals/exception-handling/binary.wast proposals/exception-handling/exports.wast proposals/exception-handling/imports.wast + proposals/exception-handling/ref_null.wast proposals/exception-handling/tag.wast + proposals/exception-handling/throw.wast + proposals/exception-handling/throw_ref.wast + proposals/exception-handling/try_table.wast proposals/tail-call/return_call.wast proposals/tail-call/return_call_indirect.wast ref_func.wast @@ -149,7 +153,9 @@ utf8-import-module.wast utf8-invalid-encoding.wast - + + + SpecV1BinaryTest.test40,SpecV1EhBinaryTest.test40 @@ -328,7 +334,11 @@ proposals/exception-handling/binary.wast proposals/exception-handling/exports.wast proposals/exception-handling/imports.wast + proposals/exception-handling/ref_null.wast proposals/exception-handling/tag.wast + proposals/exception-handling/throw.wast + proposals/exception-handling/throw_ref.wast + proposals/exception-handling/try_table.wast proposals/tail-call/return_call.wast proposals/tail-call/return_call_indirect.wast ref_func.wast @@ -419,7 +429,9 @@ utf8-import-module.wast utf8-invalid-encoding.wast - + + + SpecV1BinaryTest.test40,SpecV1EhBinaryTest.test40 diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/CtrlFrame.java b/runtime/src/main/java/com/dylibso/chicory/runtime/CtrlFrame.java index 55e701ec0..0a67a2d30 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/CtrlFrame.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/CtrlFrame.java @@ -12,10 +12,19 @@ final class CtrlFrame { // the height of the stack before entering the current Control Flow instruction public final int height; + // the program counter of a TRY_TABLE block + // TODO: do we have a better way of doing this? + public final int pc; + public CtrlFrame(OpCode opCode, int startValues, int endValues, int height) { + this(opCode, startValues, endValues, height, 0); + } + + public CtrlFrame(OpCode opCode, int startValues, int endValues, int height, int pc) { this.opCode = opCode; this.startValues = startValues; this.endValues = endValues; this.height = height; + this.pc = pc; } } diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java b/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java index 3176e69ec..b52d6ea1f 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java @@ -65,6 +65,8 @@ public class Instance { private final ExecutionListener listener; private final Exports fluentExports; + private final Map exnRefs; + Instance( WasmModule module, Global[] globalInitializers, @@ -106,6 +108,8 @@ public class Instance { this.listener = listener; this.fluentExports = new Exports(this); + this.exnRefs = new HashMap<>(); + if (initialize) { initialize(start); } @@ -317,6 +321,19 @@ public TagInstance tag(int idx) { return tags[idx - imports.tagCount()]; } + public int tagCount() { + return tags.length; + } + + public int registerException(WasmException ex) { + exnRefs.put(ex.tagIdx(), ex); + return ex.tagIdx(); + } + + public WasmException exn(int idx) { + return exnRefs.get(idx); + } + public Machine getMachine() { return machine; } diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java index 5e9b1dcb6..6c7bcdec0 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java @@ -7,6 +7,7 @@ import com.dylibso.chicory.wasm.ChicoryException; import com.dylibso.chicory.wasm.types.AnnotatedInstruction; +import com.dylibso.chicory.wasm.types.CatchOpCode; import com.dylibso.chicory.wasm.types.FunctionType; import com.dylibso.chicory.wasm.types.Instruction; import com.dylibso.chicory.wasm.types.OpCode; @@ -96,13 +97,18 @@ private long[] call( callStack.push(stackFrame); var imprt = instance.imports().function(funcId); - var results = imprt.handle().apply(instance, args); - // a host function can return null or an array of ints - // which we will push onto the stack - if (results != null) { - for (var result : results) { - stack.push(result); + + try { + var results = imprt.handle().apply(instance, args); + // a host function can return null or an array of ints + // which we will push onto the stack + if (results != null) { + for (var result : results) { + stack.push(result); + } } + } catch (WasmException e) { + THROW_REF(instance, instance.registerException(e), stack, stackFrame, callStack); } } @@ -162,6 +168,9 @@ protected void eval(MStack stack, Instance instance, Deque callStack case BLOCK: BLOCK(frame, stack, instance, instruction); break; + case TRY_TABLE: + TRY_TABLE(frame, stack, instance, instruction, frame.currentPc()); + break; case IF: IF(frame, stack, instance, instruction); break; @@ -207,6 +216,24 @@ protected void eval(MStack stack, Instance instance, Deque callStack // swap in place the current frame frame = RETURN_CALL_INDIRECT(stack, instance, callStack, operands, frame); break; + case THROW: + { + int tagNumber = (int) operands.get(0); + var tag = instance.tag(tagNumber); + var type = instance.type(tag.tagType().typeIdx()); + + var args = extractArgsForParams(stack, type.params()); + var exception = new WasmException(instance, tagNumber, args); + var exceptionIdx = instance.registerException(exception); + frame = THROW_REF(instance, exceptionIdx, stack, frame, callStack); + break; + } + case THROW_REF: + { + var exceptionIdx = (int) stack.pop(); + frame = THROW_REF(instance, exceptionIdx, stack, frame, callStack); + break; + } case CALL_INDIRECT: CALL_INDIRECT(stack, instance, callStack, operands); break; @@ -2035,6 +2062,93 @@ private static int numberOfValuesToReturn(Instance instance, AnnotatedInstructio return sizeOf(instance.type(typeId).returns()); } + private static StackFrame THROW_REF( + Instance instance, + int exceptionIdx, + MStack stack, + StackFrame frame, + Deque callStack) { + var exception = instance.exn(exceptionIdx); + boolean found = false; + while (!found) { + while (frame.ctrlStackSize() > 0) { + var ctrlFrame = frame.popCtrl(); + if (ctrlFrame.opCode != OpCode.TRY_TABLE) { + continue; + } + + frame.jumpTo(ctrlFrame.pc); + var tryInst = frame.loadCurrentInstruction(); + + var catches = CatchOpCode.decode(tryInst.operands()); + for (int i = 0; i < catches.size() && !found; i++) { + var currentCatch = catches.get(i); + + // verify import compatibility + var compatibleImport = false; + if ((currentCatch.opcode() == CatchOpCode.CATCH + || currentCatch.opcode() == CatchOpCode.CATCH_REF)) { + var currentCatchTag = instance.tag(currentCatch.tag()); + var exceptionTag = exception.instance().tag(exception.tagIdx()); + + // if it's an import we verify the compatibility + if (currentCatch.tag() < instance.imports().tagCount() + && currentCatchTag.type().paramsMatch(exceptionTag.type()) + && currentCatchTag.type().returnsMatch(exceptionTag.type())) { + compatibleImport = true; + } else if (exceptionTag != currentCatchTag) { + // if it's not an import the tag should be the same + continue; + } + } + + switch (currentCatch.opcode()) { + case CATCH: + if (currentCatch.tag() == exception.tagIdx() || compatibleImport) { + found = true; + for (var arg : exception.args()) { + stack.push(arg); + } + } + break; + case CATCH_REF: + if (currentCatch.tag() == exception.tagIdx() || compatibleImport) { + found = true; + for (var arg : exception.args()) { + stack.push(arg); + } + stack.push(exceptionIdx); + } + break; + case CATCH_ALL: + found = true; + break; + case CATCH_ALL_REF: + found = true; + stack.push(exceptionIdx); + break; + } + + if (found) { + var resolvedLabel = tryInst.labelTable().get(i); + // BR l + ctrlJump(frame, stack, currentCatch.label()); + frame.jumpTo(resolvedLabel); + return frame; + } + } + } + if (!found) { + if (callStack.isEmpty()) { + throw exception; + } else { + frame = callStack.pop(); + } + } + } + throw new RuntimeException("unreacheable"); + } + private static void BLOCK( StackFrame frame, MStack stack, Instance instance, AnnotatedInstruction instruction) { var paramsSize = numberOfParams(instance, instruction); @@ -2042,6 +2156,18 @@ private static void BLOCK( frame.pushCtrl(instruction.opcode(), paramsSize, returnsSize, stack.size() - paramsSize); } + private static void TRY_TABLE( + StackFrame frame, + MStack stack, + Instance instance, + AnnotatedInstruction instruction, + int pc) { + var paramsSize = numberOfParams(instance, instruction); + var returnsSize = numberOfValuesToReturn(instance, instruction); + frame.pushCtrl( + instruction.opcode(), paramsSize, returnsSize, stack.size() - paramsSize, pc); + } + private static void IF( StackFrame frame, MStack stack, Instance instance, AnnotatedInstruction instruction) { var predValue = stack.pop(); diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java b/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java index b7e494050..8291062b6 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java @@ -136,6 +136,10 @@ AnnotatedInstruction loadCurrentInstruction() { return currentInstruction; } + int currentPc() { + return pc - 1; + } + boolean isLastBlock() { return currentInstruction.depth() == 0; } @@ -152,6 +156,14 @@ void pushCtrl(OpCode opcode, int startValues, int returnValues, int height) { ctrlStack.add(new CtrlFrame(opcode, startValues, returnValues, height)); } + void pushCtrl(OpCode opcode, int startValues, int returnValues, int height, int pc) { + ctrlStack.add(new CtrlFrame(opcode, startValues, returnValues, height, pc)); + } + + int ctrlStackSize() { + return ctrlStack.size(); + } + CtrlFrame popCtrl() { var ctrlFrame = ctrlStack.remove(ctrlStack.size() - 1); return ctrlFrame; diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/WasmException.java b/runtime/src/main/java/com/dylibso/chicory/runtime/WasmException.java new file mode 100644 index 000000000..c49479780 --- /dev/null +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/WasmException.java @@ -0,0 +1,26 @@ +package com.dylibso.chicory.runtime; + +public class WasmException extends RuntimeException { + private final int tagIdx; + private final long[] args; + private final Instance instance; + + public WasmException(Instance instance, int tagIdx, long[] args) { + this.instance = instance; + this.tagIdx = tagIdx; + this.args = args.clone(); + this.setStackTrace(new StackTraceElement[0]); + } + + public Instance instance() { + return instance; + } + + public int tagIdx() { + return tagIdx; + } + + public long[] args() { + return args; + } +} diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java index 5ce827e53..6a92508a2 100644 --- a/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java @@ -11,6 +11,7 @@ import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.UninstantiableException; import com.dylibso.chicory.wasm.WasmModule; +import com.dylibso.chicory.wasm.types.CatchOpCode; import com.dylibso.chicory.wasm.types.MemoryLimits; import com.dylibso.chicory.wasm.types.Table; import com.dylibso.chicory.wasm.types.TableLimits; @@ -541,4 +542,18 @@ public void shouldResolveMultipleAliasesByTypeForAllImports() { assertEquals(0, instance.imports().tag(0).tag().tagType().typeIdx()); assertEquals(1, instance.imports().tag(1).tag().tagType().typeIdx()); } + + @Test + public void correctlyReturnAllLabels() { + // Arrange + var operands = new long[] {64, 2, 0, 0, 0, 0, 0, 0}; + + // Act + var result = CatchOpCode.allLabels(operands); + + // Assert + assertEquals(2, result.size()); + assertEquals(0, result.get(0)); + assertEquals(0, result.get(1)); + } } diff --git a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/JavaTestGen.java b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/JavaTestGen.java index 657429450..9b91c37ac 100644 --- a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/JavaTestGen.java +++ b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/JavaTestGen.java @@ -83,6 +83,7 @@ public CompilationUnit generate(String name, Wast wast, String wasmClasspath) { // runtime imports cu.addImport("com.dylibso.chicory.wasm.ChicoryException"); + cu.addImport("com.dylibso.chicory.runtime.WasmException"); cu.addImport("com.dylibso.chicory.runtime.ExportFunction"); cu.addImport("com.dylibso.chicory.runtime.Instance"); @@ -189,6 +190,7 @@ public CompilationUnit generate(String name, Wast wast, String wasmClasspath) { case ASSERT_RETURN: case ASSERT_TRAP: case ASSERT_EXHAUSTION: + case ASSERT_EXCEPTION: { method = createTestMethod( @@ -290,6 +292,7 @@ private boolean getExcluded(CommandType typ, String name) { return excludedUnlinkableWasts.contains(name + ".wast"); case ASSERT_EXHAUSTION: case ASSERT_TRAP: + case ASSERT_EXCEPTION: return false; default: throw new IllegalArgumentException(typ + "not implemented"); @@ -309,6 +312,8 @@ private String getExceptionType(CommandType typ) { case ASSERT_TRAP: case ASSERT_EXHAUSTION: return "ChicoryException"; + case ASSERT_EXCEPTION: + return "WasmException"; default: throw new IllegalArgumentException(typ + "not implemented"); } @@ -365,6 +370,7 @@ private Optional generateFieldExport( private List generateAssert(String varName, Command cmd) { assert (cmd.type() == CommandType.ASSERT_RETURN || cmd.type() == CommandType.ASSERT_TRAP + || cmd.type() == CommandType.ASSERT_EXCEPTION || cmd.type() == CommandType.ASSERT_EXHAUSTION); assert (cmd.expected() != null); assert (cmd.expected().length > 0); @@ -388,7 +394,9 @@ private List generateAssert(String varName, Command cmd) { ? ".apply(ArgsAdapter.builder()" + adaptedArgs + ".build()" + ")" : ".getValue()"; - if (cmd.type() == CommandType.ASSERT_TRAP || cmd.type() == CommandType.ASSERT_EXHAUSTION) { + if (cmd.type() == CommandType.ASSERT_TRAP + || cmd.type() == CommandType.ASSERT_EXHAUSTION + || cmd.type() == CommandType.ASSERT_EXCEPTION) { var assertDecl = new NameExpr( "var exception =" diff --git a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/CommandType.java b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/CommandType.java index fb76680d9..bd0da3298 100644 --- a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/CommandType.java +++ b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/CommandType.java @@ -20,6 +20,8 @@ public enum CommandType { ASSERT_EXHAUSTION("assert_exhaustion"), @JsonProperty("assert_unlinkable") ASSERT_UNLINKABLE("assert_unlinkable"), + @JsonProperty("assert_exception") + ASSERT_EXCEPTION("assert_exception"), @JsonProperty("action") ACTION("action"), @JsonProperty("register") diff --git a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValue.java b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValue.java index d401d4258..21c9437cf 100644 --- a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValue.java +++ b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValue.java @@ -34,6 +34,7 @@ public String toResultValue(String result) { case F64: return "Double.longBitsToDouble(" + result + "), 0.0"; case EXTERN_REF: + case EXN_REF: case FUNC_REF: if (result.equals("null")) { return "Value.REF_NULL_VALUE"; @@ -154,6 +155,7 @@ public String toExpectedValue() { return "null"; } case EXTERN_REF: + case EXN_REF: case FUNC_REF: if (value[0].equals("null")) { return "Value.REF_NULL_VALUE"; @@ -258,6 +260,7 @@ public String toArgsValue() { return "null"; } case EXTERN_REF: + case EXN_REF: case FUNC_REF: if (value[0].toString().equals("null")) { return "Value.REF_NULL_VALUE"; diff --git a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValueType.java b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValueType.java index 24475334a..38485afb3 100644 --- a/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValueType.java +++ b/test-gen-lib/src/main/java/com/dylibso/chicory/testgen/wast/WasmValueType.java @@ -17,7 +17,9 @@ public enum WasmValueType { @JsonProperty("externref") EXTERN_REF("externref"), @JsonProperty("funcref") - FUNC_REF("funcref"); + FUNC_REF("funcref"), + @JsonProperty("exnref") + EXN_REF("exnref"); private final String value; diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java b/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java index 313f3499a..c45b95fb9 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java @@ -16,6 +16,7 @@ import com.dylibso.chicory.wasm.types.ActiveDataSegment; import com.dylibso.chicory.wasm.types.ActiveElement; import com.dylibso.chicory.wasm.types.AnnotatedInstruction; +import com.dylibso.chicory.wasm.types.CatchOpCode; import com.dylibso.chicory.wasm.types.CodeSection; import com.dylibso.chicory.wasm.types.CustomSection; import com.dylibso.chicory.wasm.types.DataCountSection; @@ -828,6 +829,7 @@ private static CodeSection parseCodeSection(ByteBuffer buffer) { case BLOCK: case LOOP: case IF: + case TRY_TABLE: { depth++; instruction.withDepth(depth); @@ -926,6 +928,32 @@ private static CodeSection parseCodeSection(ByteBuffer buffer) { instruction.withLabelTable(labelTable); break; } + case TRY_TABLE: + { + // labels computation + var allLabels = CatchOpCode.allLabels(baseInstruction.operands()); + var labelTable = new ArrayList(); + for (var idx = 0; idx < allLabels.size(); idx++) { + labelTable.add(null); + var offset = allLabels.get(idx); + ControlTree reference = currentControlFlow; + while (offset > 0) { + if (reference == null) { + throw new InvalidException("unknown label"); + } + reference = reference.parent(); + offset--; + } + int finalIdx = idx; + reference.addCallback(end -> labelTable.set(finalIdx, end)); + } + instruction.withLabelTable(labelTable); + + // block start + currentControlFlow = + currentControlFlow.spawn(instructions.size(), instruction); + break; + } case END: { currentControlFlow.setFinalInstructionNumber( @@ -1072,6 +1100,27 @@ private static Instruction parseInstruction(ByteBuffer buffer) { } break; } + case VEC_CATCH: + { + var n = readVarUInt32(buffer); + operands.add(n); + for (var j = 0; j < n; j++) { + var catchOp = readByte(buffer); + operands.add(0L | catchOp); + var catchOpcode = CatchOpCode.byOpCode(catchOp); + switch (catchOpcode) { + case CATCH: + case CATCH_REF: + operands.add(readVarUInt32(buffer)); // tag + // intentional fall-through + case CATCH_ALL: + case CATCH_ALL_REF: + operands.add(readVarUInt32(buffer)); // label + break; + } + } + break; + } case V128: { byte[] bytes = new byte[16]; diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java b/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java index f74e02f2b..d73d41fce 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java @@ -8,6 +8,7 @@ import com.dylibso.chicory.wasm.types.ActiveDataSegment; import com.dylibso.chicory.wasm.types.ActiveElement; import com.dylibso.chicory.wasm.types.AnnotatedInstruction; +import com.dylibso.chicory.wasm.types.CatchOpCode; import com.dylibso.chicory.wasm.types.DeclarativeElement; import com.dylibso.chicory.wasm.types.Element; import com.dylibso.chicory.wasm.types.ExternalType; @@ -20,6 +21,8 @@ import com.dylibso.chicory.wasm.types.MutabilityType; import com.dylibso.chicory.wasm.types.OpCode; import com.dylibso.chicory.wasm.types.TableImport; +import com.dylibso.chicory.wasm.types.TagImport; +import com.dylibso.chicory.wasm.types.TagSection; import com.dylibso.chicory.wasm.types.TagType; import com.dylibso.chicory.wasm.types.Value; import com.dylibso.chicory.wasm.types.ValueType; @@ -27,6 +30,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.stream.Stream; @@ -84,6 +88,7 @@ public CtrlFrame( private final List globalImports; private final List functionImports; private final List tableImports; + private final List tagImports; private final int memoryImports; private final Set declaredFunctions; @@ -119,6 +124,13 @@ public CtrlFrame( .flatMap(element -> element.initializers().stream()) .flatMap(this::declaredFunctions) .collect(toSet()); + + this.tagImports = + module.importSection().stream() + .filter(TagImport.class::isInstance) + .map(TagImport.class::cast) + .map(TagImport::tagType) + .collect(toList()); } private Stream declaredFunctions(List init) { @@ -144,7 +156,7 @@ private ValueType popVal() { if (valueTypeStack.size() == frame.height) { errors.add( new InvalidException( - "type mismatch, popVal(), stack reached limit at " + frame.height)); + "type mismatch: instruction requires [i32] but stack has []")); return ValueType.UNKNOWN; } return valueTypeStack.remove(valueTypeStack.size() - 1); @@ -155,10 +167,11 @@ private ValueType popVal(ValueType expected) { if (actual != expected && actual != ValueType.UNKNOWN && expected != ValueType.UNKNOWN) { errors.add( new InvalidException( - "type mismatch, popVal(expected), expected: " - + expected - + " but got: " - + actual)); + "type mismatch: instruction requires [" + + expected.toString().toLowerCase(Locale.ROOT) + + "] but stack has [" + + actual.toString().toLowerCase(Locale.ROOT) + + "]")); } return actual; } @@ -312,6 +325,16 @@ private ValueType getTableType(int idx) { return module.tableSection().getTable(idx - tableImports.size()).elementType(); } + private TagType getTagType(int idx) { + if (idx < 0 || idx >= tagImports.size() + module.tagSection().get().tagCount()) { + throw new InvalidException("unknown tag " + idx); + } + if (idx < tagImports.size()) { + return tagImports.get(idx); + } + return module.tagSection().get().getTag(idx - tagImports.size()); + } + private Element getElement(int idx) { if (idx < 0 || idx >= module.elementSection().elementCount()) { throw new InvalidException("unknown elem segment " + idx); @@ -445,7 +468,9 @@ private void validateConstantExpression( { exprType = ValueType.refTypeForId((int) instruction.operand(0)); constInstrCount++; - if (exprType != ValueType.ExternRef && exprType != ValueType.FuncRef) { + if (exprType != ValueType.ExternRef + && exprType != ValueType.FuncRef + && exprType != ValueType.ExnRef) { throw new IllegalStateException( "Unexpected wrong type for ref.null instruction"); } @@ -530,6 +555,79 @@ void validateFunction(int funcIdx, FunctionBody body, FunctionType functionType) case UNREACHABLE: unreachable(); break; + case TRY_TABLE: + { + var t1 = getParams(op); + var t2 = getReturns(op); + popVals(t1); + // and now the catches + var catches = CatchOpCode.decode(op.operands()); + + for (int idx = 0; idx < catches.size(); idx++) { + var currentCatch = catches.get(idx); + if (ctrlFrameStack.size() < currentCatch.label()) { + throw new InvalidException("something something"); + } + // push_ctrl(catch, [], label_types(ctrls[handler.label])) + // using THROW instead of CATCH ... doesn't matter as it's removed right + // after + pushCtrl( + OpCode.THROW, + List.of(), + labelTypes(getCtrl(currentCatch.label()))); + switch (currentCatch.opcode()) { + case CATCH: + { + var tagType = + module.typeSection() + .getType( + getTagType(currentCatch.tag()) + .typeIdx()); + pushVals(tagType.params()); + break; + } + case CATCH_REF: + { + var tagType = + module.typeSection() + .getType( + getTagType(currentCatch.tag()) + .typeIdx()); + pushVals(tagType.params()); + pushVal(ValueType.ExnRef); + break; + } + case CATCH_ALL: + break; + case CATCH_ALL_REF: + pushVal(ValueType.ExnRef); + break; + } + popCtrl(); + } + pushCtrl(op.opcode(), t1, t2); + break; + } + case THROW: + { + var tagNumber = (int) op.operand(0); + if ((tagImports.size() + + module.tagSection().map(TagSection::tagCount).orElse(0)) + <= tagNumber) { + throw new InvalidException("unknown tag " + tagNumber); + } + var type = module.typeSection().getType(getTagType(tagNumber).typeIdx()); + popVals(type.params()); + assert (type.returns().size() == 0); + unreachable(); + break; + } + case THROW_REF: + { + popVal(ValueType.ExnRef); + unreachable(); + break; + } case IF: popVal(ValueType.I32); // fallthrough @@ -735,6 +833,9 @@ void validateFunction(int funcIdx, FunctionBody body, FunctionType functionType) switch (op.opcode()) { case NOP: case UNREACHABLE: + case THROW: + case THROW_REF: + case TRY_TABLE: case LOOP: case BLOCK: case IF: diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/AnnotatedInstruction.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/AnnotatedInstruction.java index 428724d90..b0747ea19 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/types/AnnotatedInstruction.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/AnnotatedInstruction.java @@ -138,6 +138,7 @@ public AnnotatedInstruction build() { case LOOP: case END: case IF: + case TRY_TABLE: assert (scope.isPresent()); break; default: @@ -164,6 +165,7 @@ public AnnotatedInstruction build() { } switch (base.opcode()) { case BR_TABLE: + case TRY_TABLE: if (labelTable.isEmpty()) { throw new InvalidException("unknown label table" + base); } diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/CatchOpCode.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/CatchOpCode.java new file mode 100644 index 000000000..b0489004b --- /dev/null +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/CatchOpCode.java @@ -0,0 +1,99 @@ +package com.dylibso.chicory.wasm.types; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public enum CatchOpCode { + CATCH(0x00), + CATCH_REF(0x01), + CATCH_ALL(0x02), + CATCH_ALL_REF(0x03); + + private static final int OP_CODES_SIZE = 4; + + // trick: the enum constructor cannot access its own static fields + // but can access another class + private static final class CatchOpCodes { + private CatchOpCodes() {} + + private static final CatchOpCode[] byOpCode = new CatchOpCode[OP_CODES_SIZE]; + } + + private final int opcode; + + CatchOpCode(int opcode) { + this.opcode = opcode; + CatchOpCodes.byOpCode[opcode] = this; + } + + public int opcode() { + return opcode; + } + + public static CatchOpCode byOpCode(int opcode) { + return CatchOpCodes.byOpCode[opcode]; + } + + public static final class Catch { + private final CatchOpCode opcode; + private final int tag; + private final int label; + + private Catch(CatchOpCode opcode, int label) { + this(opcode, -1, label); + assert (opcode == CATCH_ALL || opcode == CATCH_ALL_REF); + } + + private Catch(CatchOpCode opcode, int tag, int label) { + assert (tag == -1 || opcode == CATCH || opcode == CATCH_REF); + this.opcode = opcode; + this.tag = tag; + this.label = label; + } + + public CatchOpCode opcode() { + return opcode; + } + + public int tag() { + return tag; + } + + public int label() { + return label; + } + } + + @SuppressWarnings("ModifiedControlVariable") + public static List decode(long[] operands) { + var length = operands[1]; + var result = new ArrayList(); + for (int i = 2; i < operands.length; i++) { + var catchEnum = CatchOpCode.byOpCode((int) operands[i++]); + switch (catchEnum) { + case CATCH: + case CATCH_REF: + { + var tag = (int) operands[i++]; + var label = (int) operands[i]; + result.add(new Catch(catchEnum, tag, label)); + break; + } + case CATCH_ALL: + case CATCH_ALL_REF: + { + var label = (int) operands[i]; + result.add(new Catch(catchEnum, label)); + break; + } + } + } + assert (result.size() == length); + return result; + } + + public static List allLabels(long[] operands) { + return decode(operands).stream().map(Catch::label).collect(Collectors.toList()); + } +} diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/FunctionType.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/FunctionType.java index 41ea661d8..5c0dc366e 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/types/FunctionType.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/FunctionType.java @@ -51,6 +51,7 @@ public int hashCode() { static { EnumMap map = new EnumMap<>(ValueType.class); map.put(ValueType.ExternRef, new FunctionType(List.of(), List.of(ValueType.ExternRef))); + map.put(ValueType.ExnRef, new FunctionType(List.of(), List.of(ValueType.ExnRef))); map.put(ValueType.FuncRef, new FunctionType(List.of(), List.of(ValueType.FuncRef))); map.put(ValueType.V128, new FunctionType(List.of(), List.of(ValueType.V128))); map.put(ValueType.F64, new FunctionType(List.of(), List.of(ValueType.F64))); @@ -60,6 +61,7 @@ public int hashCode() { returning = map; map = new EnumMap<>(ValueType.class); map.put(ValueType.ExternRef, new FunctionType(List.of(ValueType.ExternRef), List.of())); + map.put(ValueType.ExnRef, new FunctionType(List.of(ValueType.ExnRef), List.of())); map.put(ValueType.FuncRef, new FunctionType(List.of(ValueType.FuncRef), List.of())); map.put(ValueType.V128, new FunctionType(List.of(ValueType.V128), List.of())); map.put(ValueType.F64, new FunctionType(List.of(ValueType.F64), List.of())); diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/OpCode.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/OpCode.java index d9c1f641d..fc7d2d59d 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/types/OpCode.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/OpCode.java @@ -7,6 +7,7 @@ import static com.dylibso.chicory.wasm.types.WasmEncoding.VARSINT32; import static com.dylibso.chicory.wasm.types.WasmEncoding.VARSINT64; import static com.dylibso.chicory.wasm.types.WasmEncoding.VARUINT; +import static com.dylibso.chicory.wasm.types.WasmEncoding.VEC_CATCH; import static com.dylibso.chicory.wasm.types.WasmEncoding.VEC_VARUINT; import java.util.List; @@ -18,6 +19,8 @@ public enum OpCode { LOOP(0x03, List.of(VARUINT)), IF(0x04, List.of(VARUINT)), ELSE(0x05), + THROW(0x08, List.of(VARUINT)), + THROW_REF(0x0A), END(0x0B), BR(0x0C, List.of(VARUINT)), BR_IF(0x0D, List.of(VARUINT)), @@ -31,6 +34,7 @@ public enum OpCode { DROP(0x1A), SELECT(0x1B), SELECT_T(0x1C, List.of(VEC_VARUINT)), + TRY_TABLE(0x1F, List.of(VARUINT, VEC_CATCH)), LOCAL_GET(0x20, List.of(VARUINT)), LOCAL_SET(0x21, List.of(VARUINT)), LOCAL_TEE(0x22, List.of(VARUINT)), diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/Value.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/Value.java index bfbf042df..26f716d30 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/types/Value.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/Value.java @@ -274,6 +274,7 @@ public static long zero(ValueType valueType) { case I64: case F64: return 0L; + case ExnRef: case FuncRef: case ExternRef: return REF_NULL_VALUE; diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/ValueType.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/ValueType.java index 303d261f0..234e76c7e 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/types/ValueType.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/ValueType.java @@ -1,5 +1,7 @@ package com.dylibso.chicory.wasm.types; +import static com.dylibso.chicory.wasm.types.ValueType.ID.ExnRef; + import com.dylibso.chicory.wasm.MalformedException; import java.util.List; @@ -14,6 +16,7 @@ public enum ValueType { I32(ID.I32), V128(ID.V128), FuncRef(ID.FuncRef), + ExnRef(ID.ExnRef), ExternRef(ID.ExternRef); private final int id; @@ -96,6 +99,7 @@ public boolean isFloatingPoint() { public boolean isReference() { switch (this) { case FuncRef: + case ExnRef: case ExternRef: return true; default: @@ -108,13 +112,14 @@ public boolean isReference() { */ public static boolean isValid(int typeId) { switch (typeId) { - case ID.F64: case ID.ExternRef: + case ID.ExnRef: case ID.FuncRef: case ID.V128: case ID.I32: case ID.I64: case ID.F32: + case ID.F64: return true; default: return false; @@ -140,6 +145,8 @@ public static ValueType forId(int id) { return V128; case ID.FuncRef: return FuncRef; + case ID.ExnRef: + return ExnRef; case ID.ExternRef: return ExternRef; default: @@ -158,6 +165,8 @@ public static ValueType refTypeForId(int id) { return FuncRef; case ID.ExternRef: return ExternRef; + case ID.ExnRef: + return ExnRef; default: throw new MalformedException("malformed reference type " + id); } @@ -184,6 +193,8 @@ static final class ID { private ID() {} static final int ExternRef = 0x6f; + // From the Exception Handling proposal + static final int ExnRef = 0x69; // -0x17 static final int FuncRef = 0x70; static final int V128 = 0x7b; static final int F64 = 0x7c; diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/WasmEncoding.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/WasmEncoding.java index 754a9c151..56040b364 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/types/WasmEncoding.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/WasmEncoding.java @@ -7,6 +7,7 @@ public enum WasmEncoding { FLOAT32, FLOAT64, VEC_VARUINT, + VEC_CATCH, BYTE, V128 }