diff --git a/web-image/README.md b/web-image/README.md index eb2f3f11db2c..d92ca706686f 100644 --- a/web-image/README.md +++ b/web-image/README.md @@ -1,14 +1,44 @@ # Web Image +Web Image is an experimental backend for Native Image that produces a +WebAssembly module from Java code. + +## Prerequisites + +Being part of Native Image, see the [Quick Start Guide](../docs/reference-manual/native-image/contribute/DevelopingNativeImage.md) +for all the prerequisites for building Native Image. + +In addition, Web Image also uses `wasm-as` from +[binaryen](https://github.com/WebAssembly/binaryen) as its assembler. +Version 119 of `wasm-as` has to be available on your `PATH`. + +
+Installation + +Binaryen provides pre-built releases for all major platforms on +[GitHub](https://github.com/WebAssembly/binaryen/releases). + +On MacOS, it is recommended to install through [Homebrew](https://brew.sh/), as +the pre-built binaries are not signed and may be quarantined by MacOS. + +```bash +brew install binaryen +``` +
+ ## Building +To build Web Image, run the `mx build` command in the `web-image` suite: + ```bash +cd /path/to/graal +cd web-image mx build ``` ## Usage -Use this as you would regular the regular `native-image` tool, but with an +Use this as you would use the regular `native-image` tool, but with an additional `--tool:svm-wasm` flag to enable the WebAssembly backend: ```bash @@ -16,18 +46,31 @@ mx native-image --tool:svm-wasm -cp ... HelloWorld ``` This produces `helloworld.js` and `helloworld.js.wasm` in your working -directory. The JavaScript file is a wrapper that loads and runs the WebAssembly -code and can be run with [Node.js](https://nodejs.org/en) 22 or later: +directory. The `--tool:svm-wasm` flag should be the first argument if possible. +If any experimental options specific to the Wasm backend are used, they can +only be added after the `--tool:svm-wasm` flag. -The `--tool:svm-wasm` flag should be the first argument if possible. If any -experimental options specific to the Wasm backend are used, they can only be -added after the `--tool:svm-wasm` flag. +The JavaScript file is a wrapper that loads and runs the WebAssembly +code and can be run with [Node.js](https://nodejs.org/en) 22 or later: ```bash -$ node helloworld.js +# --experimental-wasm-exnref is only required for Node versions before 25 +$ node --experimental-wasm-exnref helloworld.js Hello World ``` +## WebAssembly Features + +The WebAssembly code generated by Web Image makes use of various WebAssembly +features from WebAssembly 3.0: + +- [Garbage Collection](https://github.com/WebAssembly/gc) +- [Exception Handling](https://github.com/WebAssembly/exception-handling/blob/master/proposals/exception-handling/Exceptions.md) +- [Typed Function References](https://github.com/WebAssembly/function-references/blob/main/proposals/function-references/Overview.md) + +Support for [WebAssembly 2.0](https://www.w3.org/TR/wasm-core-2/#release-20) is +generally assumed. + ## Contributors - Aleksandar Prokopec diff --git a/web-image/mx.web-image/mx_web_image.py b/web-image/mx.web-image/mx_web_image.py index 9dd03341c882..1d5494faa705 100644 --- a/web-image/mx.web-image/mx_web_image.py +++ b/web-image/mx.web-image/mx_web_image.py @@ -49,7 +49,7 @@ _suite = mx.suite("web-image") -_web_image_js_engine_name = os.getenv("NODE_EXE", "node") +_web_image_js_engine_name = [os.getenv("NODE_EXE", "node"), "--experimental-wasm-exnref"] # Name of GraalVm component defining the web-image macro web_image_component = "web-image" @@ -63,7 +63,7 @@ "web-image:WEBIMAGE_CLOSURE_SUPPORT", "web-image:WEBIMAGE_GOOGLE_CLOSURE", ] -# Hosted options defined in the web-image-enterprise suite +# Hosted options defined in the web-image suite # This list has to be kept in sync with the code (the 'webimageoptions' gate tag checks this) # See also WebImageConfiguration.hosted_options web_image_hosted_options = [ @@ -92,6 +92,7 @@ "GrowthTriggerThreshold=", "HeapGrowthFactor=", "ImageHeapObjectsPerFunction=", + "LegacyExceptions", "JSComments=", "JSRuntime=", "LogFilter=", @@ -489,7 +490,7 @@ def __init__(self): def apply(self, config): vm_args, main_class, main_class_args = config - vm_args += ["-Dwebimage.test.js=" + _web_image_js_engine_name] + vm_args += ["-Dwebimage.test.js=" + ",".join(_web_image_js_engine_name)] vm_args += ["-Dwebimage.test.launcher=" + vm_web_image_path()] vm_args += ["-Dwebimage.test.flags=" + ",".join(get_launcher_flags(WebImageConfiguration.test_cases))] # If any of the arguments contains spaces and double quotes, on Windows it will add its own quotes around diff --git a/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestOptions.java b/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestOptions.java index 8b72fd6c6741..94a3b0165849 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestOptions.java +++ b/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestOptions.java @@ -31,7 +31,7 @@ import jdk.graal.compiler.debug.GraalError; public class WebImageTestOptions { - public static final String JS_CMD = System.getProperty("webimage.test.js"); + public static final List JS_CMD = Arrays.asList(System.getProperty("webimage.test.js", ",").split(",")); private static final String LAUNCHER = System.getProperty("webimage.test.launcher"); /** diff --git a/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestUtil.java b/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestUtil.java index b47ed66dbcba..c61c5da8222b 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestUtil.java +++ b/web-image/src/com.oracle.svm.hosted.webimage.test/src/com/oracle/svm/hosted/webimage/test/util/WebImageTestUtil.java @@ -191,8 +191,7 @@ public static RunResult executeTestProgram(Class c, String[] args, int expect } public static RunResult runJS(String cmd, String[] arguments, int expectExitCode) { - List invokeCmd = new ArrayList<>(); - invokeCmd.add(WebImageTestOptions.JS_CMD); + List invokeCmd = new ArrayList<>(WebImageTestOptions.JS_CMD); invokeCmd.add(cmd); if (arguments != null) { diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java index b95214ea3680..759d00d663c6 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java @@ -54,7 +54,6 @@ import com.oracle.svm.hosted.webimage.options.WebImageOptions.CompilerBackend; import com.oracle.svm.hosted.webimage.util.BenchmarkLogger; import com.oracle.svm.hosted.webimage.wasm.WebImageWasmLMJavaMainSupport; -import com.oracle.svm.hosted.webimage.wasm.codegen.BinaryenCompat; import com.oracle.svm.hosted.webimage.wasmgc.WebImageWasmGCJavaMainSupport; import com.oracle.svm.webimage.WebImageJSJavaMainSupport; import com.oracle.svm.webimage.WebImageJavaMainSupport; @@ -169,11 +168,6 @@ public int build(ImageClassLoader classLoader) { // For the Wasm backends, turn off closure compiler optionProvider.getHostedValues().put(WebImageOptions.ClosureCompiler, false); - if (backend == CompilerBackend.WASMGC && !optionProvider.getHostedValues().containsKey(BinaryenCompat.Options.UseBinaryen)) { - // For WasmGC backend, use binaryen by default - optionProvider.getHostedValues().put(BinaryenCompat.Options.UseBinaryen, true); - } - if (!optionProvider.getHostedValues().containsKey(WebImageOptions.NamingConvention)) { // The naming convention does not affect the binary image (unless debug information // is embedded) and the REDUCED mode makes the text file a lot easier to read diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/reconstruction/stackifier/StackifierData.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/reconstruction/stackifier/StackifierData.java index a721d8653e1b..40e87474f139 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/reconstruction/stackifier/StackifierData.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/codegen/reconstruction/stackifier/StackifierData.java @@ -58,6 +58,8 @@ public class StackifierData implements ReconstructionData { */ private HIRBlock[] blocks; + private ControlFlowGraph cfg; + /** * Mapping from a basic block to its index in {@link #blocks}. */ @@ -92,6 +94,10 @@ public HIRBlock[] getBlocks() { return blocks; } + public ControlFlowGraph getCfg() { + return cfg; + } + public EconomicMap getEnclosingScope() { return enclosingScope; } @@ -134,6 +140,7 @@ public void setLabeledBlockEnd(EconomicMap labeledBlockE public void setSortedBlocks(HIRBlock[] sortedBlocks, ControlFlowGraph cfg) { this.blocks = sortedBlocks; + this.cfg = cfg; this.blockIndexSortOrder = new BlockMap<>(cfg); for (int i = 0; i < sortedBlocks.length; ++i) { this.blockIndexSortOrder.put(sortedBlocks[i], i); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WebImageWasmOptions.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WebImageWasmOptions.java index b01b54d47575..981f84bb1d79 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WebImageWasmOptions.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/WebImageWasmOptions.java @@ -49,6 +49,9 @@ public class WebImageWasmOptions { "Has no effect on code size, the binary format does not have comments.")// public static final EnumOptionKey WasmComments = new EnumOptionKey<>(CommentVerbosity.NORMAL); + @Option(help = "Enable the legacy exception proposal using try-catch instead of try_table") // + public static final HostedOptionKey LegacyExceptions = new HostedOptionKey<>(false); + @Option(help = "Assemble the Wasm binary file with debug names.")// public static final HostedOptionKey DebugNames = new HostedOptionKey<>(false) { diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/Instruction.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/Instruction.java index 45473ba4baab..081dcc3c7c97 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/Instruction.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/Instruction.java @@ -148,14 +148,33 @@ public abstract static class WasmBlock extends Instruction { * If set to null, the block has no named identifier */ protected final WasmId.Label label; + protected final WasmValType result; - public WasmBlock(WasmId.Label label) { + protected WasmBlock(WasmId.Label label, WasmValType result) { this.label = label; + this.result = result; } public WasmId.Label getLabel() { return label; } + + public WasmValType getResult() { + return result; + } + + public boolean hasResult() { + return result != null; + } + + @Override + protected String toInnerString() { + if (hasResult()) { + return "() -> " + result; + } else { + return "() -> ()"; + } + } } /** @@ -165,7 +184,11 @@ public static final class Block extends WasmBlock { public final Instructions instructions = new Instructions(); public Block(WasmId.Label label) { - super(label); + this(label, null); + } + + public Block(WasmId.Label label, WasmValType result) { + super(label, result); } } @@ -176,7 +199,7 @@ public static final class Loop extends WasmBlock { public final Instructions instructions = new Instructions(); public Loop(WasmId.Label label) { - super(label); + super(label, null); } } @@ -190,7 +213,7 @@ public static final class If extends WasmBlock { public final Instruction condition; public If(WasmId.Label label, Instruction condition) { - super(label); + super(label, null); this.condition = condition; } @@ -200,10 +223,50 @@ public boolean hasElse() { } /** - * The WASM try block from the exception handling proposal. + * The WASM try_table from the exception handling proposal. *

* Ref: https://github.com/WebAssembly/exception-handling */ + public static final class TryTable extends WasmBlock { + /** + * A catch clause for a certain tag. + *

+ * When an exception is caught, the block branches to the label of the appropriate clause. + */ + public static final class Catch { + public final WasmId.Tag tag; + public final WasmId.Label label; + + private Catch(WasmId.Tag tag, WasmId.Label label) { + this.tag = tag; + this.label = label; + } + + @Override + public String toString() { + return "Catch{tag=" + tag + ", label=" + label + '}'; + } + } + + public final Instructions instructions = new Instructions(); + public final List catchBlocks = new ArrayList<>(); + + public TryTable(WasmId.Label label) { + super(label, null); + } + + public void addCatch(WasmId.Tag tag, WasmId.Label catchLabel) { + var catchBlock = new Catch(tag, catchLabel); + catchBlocks.add(catchBlock); + } + } + + /** + * The WASM try block from the legacy exception handling proposal. + *

+ * Ref: + * https://github.com/WebAssembly/exception-handling/blob/master/proposals/exception-handling/legacy/Exceptions.md + */ public static final class Try extends WasmBlock { /** @@ -222,7 +285,7 @@ private Catch(WasmId.Tag tag) { public final List catchBlocks = new ArrayList<>(); public Try(WasmId.Label label) { - super(label); + super(label, null); } public Instructions addCatch(WasmId.Tag tag) { @@ -546,6 +609,11 @@ public static Const forWord(WordBase word) { // TODO GR-42105 Use forInt return forLong(word.rawValue()); } + + @Override + protected String toInnerString() { + return literal.type + ", " + literal.asText(); + } } /** diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java index d03c6604e506..e984ae8009d6 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmPrinter.java @@ -710,14 +710,22 @@ public void visitTable(Table t) { parenClose(); } - @Override - @SuppressWarnings("try") - public void visitBlock(Instruction.Block block) { - print("block"); + private void printBlockPrefix(String name, Instruction.WasmBlock block) { + print(name); space(); printId(block.getLabel()); + if (block.hasResult()) { + space(); + printResult(block.getResult()); + } space(); printComment(block.getComment()); + } + + @Override + @SuppressWarnings("try") + public void visitBlock(Instruction.Block block) { + printBlockPrefix("block", block); try (var ignored = new Indenter()) { super.visitBlock(block); } @@ -727,11 +735,7 @@ public void visitBlock(Instruction.Block block) { @Override @SuppressWarnings("try") public void visitLoop(Instruction.Loop loop) { - print("loop"); - space(); - printId(loop.getLabel()); - space(); - printComment(loop.getComment()); + printBlockPrefix("loop", loop); try (var ignored = new Indenter()) { super.visitLoop(loop); } @@ -741,11 +745,7 @@ public void visitLoop(Instruction.Loop loop) { @Override @SuppressWarnings("try") public void visitIf(Instruction.If ifBlock) { - print("if"); - space(); - printId(ifBlock.getLabel()); - space(); - printComment(ifBlock.getComment()); + printBlockPrefix("if", ifBlock); try (var ignored = new Indenter()) { visitInstruction(ifBlock.condition); @@ -771,14 +771,30 @@ public void visitIf(Instruction.If ifBlock) { newline(); } + @Override + @SuppressWarnings("try") + public void visitTryTable(Instruction.TryTable tryBlock) { + printBlockPrefix("try_table", tryBlock); + + try (var ignored = new Indenter()) { + for (Instruction.TryTable.Catch catchBlock : tryBlock.catchBlocks) { + newline(); + parenOpen("catch"); + space(); + printId(catchBlock.tag); + space(); + printId(catchBlock.label); + parenClose(); + } + super.visitInstructions(tryBlock.instructions); + } + newline(); + } + @Override @SuppressWarnings("try") public void visitTry(Instruction.Try tryBlock) { - print("try"); - space(); - printId(tryBlock.getLabel()); - space(); - printComment(tryBlock.getComment()); + printBlockPrefix("try", tryBlock); try (var ignored = new Indenter()) { newline(); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmValidator.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmValidator.java index 8341863d0d88..d182f4b418e6 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmValidator.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmValidator.java @@ -64,7 +64,7 @@ *

    *
  • Makes some simplifying assumptions (which the AST currently satisfies): *
      - *
    • Any labeled block has no input or output types.
    • + *
    • Any labeled block has no input and at most one output value.
    • *
    • No vector or reference types exist
    • *
    • Functions only have a single return value
    • *
    @@ -78,10 +78,24 @@ public class WasmValidator extends WasmVisitor { static class CtrlFrame { final WasmId.Label label; + final WasmValType returnType; + /** + * Whether the end of this frame (block) is unreachable. There might still be reachable code + * in the frame, but if this is true, execution can never reach the end of the block. + */ boolean unreachable = false; + /** + * Whether any instructions target this block. + */ + boolean targeted = false; - CtrlFrame(WasmId.Label label) { + CtrlFrame(WasmId.Label label, WasmValType returnType) { this.label = label; + this.returnType = returnType; + } + + public WasmValType getReturnType() { + return returnType; } } @@ -343,7 +357,7 @@ private void errorIf(boolean condition, String msg) { } } - private static boolean assertIdsEqual(WasmId first, WasmId second) { + private static boolean idsEqual(WasmId first, WasmId second) { return Objects.equals(first, second); } @@ -351,13 +365,22 @@ private CtrlFrame topFrame() { return Objects.requireNonNull(ctrls.peek()); } + /** + * Whether the current frame is marked as unreachable. + * + * @see #markUnreachable() + */ + private boolean isUnreachable() { + return topFrame().unreachable; + } + private void pushVal(WasmValType t) { + errorIf(isUnreachable(), "Tried to push " + t + " when unreachable"); vals.push(t); } private WasmValType popVal() { - CtrlFrame top = topFrame(); - if (vals.isEmpty() && top.unreachable) { + if (isUnreachable()) { return null; } @@ -383,6 +406,10 @@ private RuntimeException typeMismatch(WasmValType[] expected, Deque * @param expected The top of the stack must match this (the last element is at the very top). */ private void popVals(WasmValType... expected) { + if (isUnreachable()) { + return; + } + int numTypes = expected.length; if (vals.size() < numTypes) { throw typeMismatch(expected); @@ -416,12 +443,20 @@ private Global getAndAssertGlobalExists(WasmId.Global global) { } private void pushCtrl(WasmId.Label label) { + pushCtrl(label, null); + } + + private void pushCtrl(WasmId.Label label, WasmValType resultType) { assertStackEmpty(); if (label != null) { // No name conflicts. assertIdUniqueName(label, ctrls.stream().map(frame -> frame.label).filter(Objects::nonNull).collect(Collectors.toList())); } - ctrls.push(new CtrlFrame(label)); + ctrls.push(new CtrlFrame(label, resultType)); + } + + private void pushBlockCtrl(Instruction.WasmBlock block) { + pushCtrl(block.getLabel(), block.getResult()); } private void popCtrl(WasmId.Label expectedLabel) { @@ -430,9 +465,37 @@ private void popCtrl(WasmId.Label expectedLabel) { assertStackEmpty(); errorIf(frame.label != expectedLabel, "Expected control frame " + expectedLabel + " but got " + frame.label); ctrls.pop(); + /* + * If the end of the frame was unreachable and no instruction targeted it, it means that it + * is impossible to reach the instruction right after it and we have to mark the outer block + * as unreachable as well. + */ + if (!frame.targeted && frame.unreachable && !ctrls.isEmpty()) { + markUnreachable(); + } + + } + + private void popBlockCtrl(Instruction.WasmBlock block) { + if (block.hasResult()) { + popVals(block.getResult()); + } + popCtrl(block.getLabel()); + if (block.hasResult()) { + pushVal(block.getResult()); + } } - private void unreachable() { + /** + * Marks the top frame as unreachable. + *

    + * An unreachable frame satisfies all requirements on the operand stack (i.e. + * {@link #popVals(WasmValType...) never errors.}). + *

    + * Call this method after visiting any instruction at which execution stops and never returns + * (e.g. {@code br}). + */ + private void markUnreachable() { CtrlFrame top = topFrame(); vals.clear(); top.unreachable = true; @@ -451,8 +514,15 @@ private void applyTypeUse(TypeUse typeUse) { typeUse.results.forEach(this::pushVal); } - private void assertLabelExists(WasmId.Label label) { - errorIf(ctrls.stream().noneMatch(frame -> assertIdsEqual(label, frame.label)), "Label " + label + " does not exist."); + private CtrlFrame markLabelTargeted(WasmId.Label label) { + CtrlFrame frame = ctrls.stream().filter(f -> idsEqual(label, f.label)).findFirst().orElseThrow(() -> error("Label " + label + " does not exist.")); + frame.targeted = true; + return frame; + } + + private void markLabelTargetedWithReturnType(WasmId.Label label, WasmValType returnType) { + CtrlFrame frame = markLabelTargeted(label); + errorIf(!Objects.equals(frame.getReturnType(), returnType), "Label " + label + " has return type " + frame.getReturnType() + " but " + returnType + " was expected"); } /** @@ -566,35 +636,79 @@ public void visitInstruction(Instruction inst) { @Override public void visitBlock(Instruction.Block block) { - pushCtrl(block.getLabel()); + pushBlockCtrl(block); super.visitBlock(block); - popCtrl(block.getLabel()); + popBlockCtrl(block); } @Override public void visitLoop(Instruction.Loop loop) { - pushCtrl(loop.getLabel()); + pushBlockCtrl(loop); super.visitLoop(loop); - popCtrl(loop.getLabel()); + popBlockCtrl(loop); } @Override public void visitIf(Instruction.If ifBlock) { visitInstruction(ifBlock.condition); popVals(i32); - pushCtrl(ifBlock.getLabel()); + pushBlockCtrl(ifBlock); + + /* + * Propagating the information about unreachability upward here requires some more work. We + * only want to mark the end of the if-block as unreachable if both the end of the then- and + * else-branches are unreachable. + */ + boolean thenBlockUnreachable = false; + boolean elseBlockUnreachable = false; + + // Control frame around both branches to intercept the unreachable state + pushCtrl(null); + + // Control frame around the then-branch + pushCtrl(null); visitInstructions(ifBlock.thenInstructions); + if (topFrame().unreachable) { + thenBlockUnreachable = true; + topFrame().unreachable = false; + } + popCtrl(null); + if (ifBlock.hasElse()) { - popCtrl(ifBlock.getLabel()); - pushCtrl(ifBlock.getLabel()); + pushCtrl(null); visitInstructions(ifBlock.elseInstructions); + if (topFrame().unreachable) { + elseBlockUnreachable = true; + topFrame().unreachable = false; + } + popCtrl(null); + } + if (thenBlockUnreachable && elseBlockUnreachable) { + // This will mark the parent block as unreachable once we pop this control frame. + markUnreachable(); } - popCtrl(ifBlock.getLabel()); + popCtrl(null); + popBlockCtrl(ifBlock); + } + + @Override + public void visitTryTable(Instruction.TryTable tryBlock) { + tryBlock.catchBlocks.forEach(this::assertCatchValid); + pushBlockCtrl(tryBlock); + visitInstructions(tryBlock.instructions); + popBlockCtrl(tryBlock); + } + + private void assertCatchValid(Instruction.TryTable.Catch catchClause) { + errorIf(!ctxt.hasTag(catchClause.tag), "No matching tag for catch clause: " + catchClause); + List catchParams = catchClause.tag.typeUse.params; + errorIf(catchParams.size() != 1, "Can only support catch clause tags with a single param, got" + catchParams.size()); + markLabelTargetedWithReturnType(catchClause.label, catchParams.getFirst()); } @Override public void visitTry(Instruction.Try tryBlock) { - pushCtrl(tryBlock.getLabel()); + pushBlockCtrl(tryBlock); visitInstructions(tryBlock.instructions); for (Instruction.Try.Catch catchBlock : tryBlock.catchBlocks) { @@ -605,12 +719,12 @@ public void visitTry(Instruction.Try tryBlock) { popCtrl(null); } - popCtrl(tryBlock.getLabel()); + popBlockCtrl(tryBlock); } @Override public void visitUnreachable(Instruction.Unreachable unreachable) { - unreachable(); + markUnreachable(); super.visitUnreachable(unreachable); } @@ -632,10 +746,10 @@ public void visitBreak(Instruction.Break inst) { WasmId.Label targetLabel = inst.getTarget(); - assertLabelExists(targetLabel); + markLabelTargeted(targetLabel); if (inst.condition == null) { - unreachable(); + markUnreachable(); } else { popVals(i32); } @@ -648,13 +762,13 @@ public void visitBreakTable(Instruction.BreakTable inst) { popVals(i32); WasmId.Label defaultLabel = inst.getDefaultTarget(); - assertLabelExists(defaultLabel); + markLabelTargeted(defaultLabel); for (int i = 0; i < inst.numTargets(); i++) { - assertLabelExists(inst.getTarget(i)); + markLabelTargeted(inst.getTarget(i)); } - unreachable(); + markUnreachable(); } @Override @@ -743,6 +857,7 @@ public void visitThrow(Instruction.Throw inst) { errorIf(!ctxt.hasTag(inst.tag), "No matching tag for throw: " + inst); applyTypeUse(inst.tag.typeUse); + markUnreachable(); } @Override diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmVisitor.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmVisitor.java index 835794ef7885..a338a2acab6e 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmVisitor.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/ast/visitors/WasmVisitor.java @@ -278,6 +278,10 @@ public void visitInstruction(Instruction inst) { visitBlockInstr(i); visitIf(i); } + case Instruction.TryTable i -> { + visitBlockInstr(i); + visitTryTable(i); + } case Instruction.Try i -> { visitBlockInstr(i); visitTry(i); @@ -337,8 +341,11 @@ public void visitInstruction(Instruction inst) { } } - public void visitBlockInstr(@SuppressWarnings("unused") Instruction.WasmBlock block) { + public void visitBlockInstr(Instruction.WasmBlock block) { visitId(block.getLabel()); + if (block.hasResult()) { + visitType(block.getResult()); + } } public void visitBlock(Instruction.Block block) { @@ -357,6 +364,14 @@ public void visitIf(Instruction.If ifBlock) { } } + public void visitTryTable(Instruction.TryTable tryBlock) { + for (Instruction.TryTable.Catch catchBlock : tryBlock.catchBlocks) { + visitId(catchBlock.tag); + visitId(catchBlock.label); + } + visitInstructions(tryBlock.instructions); + } + public void visitTry(Instruction.Try tryBlock) { for (Instruction.Try.Catch catchBlock : tryBlock.catchBlocks) { visitId(catchBlock.tag); diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/BinaryenCompat.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/BinaryenCompat.java index 34488f0fb8b6..8fbf4aa40ace 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/BinaryenCompat.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/BinaryenCompat.java @@ -27,11 +27,17 @@ import java.util.Objects; +import org.graalvm.collections.UnmodifiableEconomicMap; + import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.hosted.webimage.options.WebImageOptions; +import com.oracle.svm.hosted.webimage.wasm.WebImageWasmOptions; import com.oracle.svm.hosted.webimage.wasm.ast.visitors.WasmPrinter; import com.oracle.svm.webimage.wasm.types.WasmValType; import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionKey; +import jdk.graal.compiler.options.OptionValues; import jdk.vm.ci.code.site.Reference; /** @@ -43,7 +49,23 @@ public class BinaryenCompat { public static class Options { @Option(help = "Use Binaryen (wasm-as) to assemble the final Wasm binary")// - public static final HostedOptionKey UseBinaryen = new HostedOptionKey<>(false); + public static final HostedOptionKey UseBinaryen = new HostedOptionKey<>(false) { + @Override + public Boolean getValue(OptionValues values) { + assert checkDescriptorExists(); + return getValueOrDefault(values.getMap()); + } + + @Override + public Boolean getValueOrDefault(UnmodifiableEconomicMap, Object> values) { + if (values.containsKey(this)) { + return (Boolean) values.get(this); + } + + // Binaryen is the default for WasmGC or when the new exception handling is used. + return WebImageOptions.getBackend() == WebImageOptions.CompilerBackend.WASMGC || !WebImageWasmOptions.LegacyExceptions.getValueOrDefault(values); + } + }; } public static boolean usesBinaryen() { diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java index 97c9b5996af3..649305869f8d 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmAssembler.java @@ -189,6 +189,16 @@ private static RunResult runCommand(Path executable, List args) throws I } } + protected String getDetails() { + return "'%s' is part of the %s project (%s). At least version %s is required.".formatted(getExecutable(), getProjectName(), getURL(), getMinimumVersion()); + } + + protected abstract String getProjectName(); + + protected abstract String getURL(); + + protected abstract String getMinimumVersion(); + /** * The standard name of the assembler executable. */ @@ -243,6 +253,7 @@ private UserError.UserException rethrowWithInfo(UserError.UserException err) { if (!pathOption.hasBeenSet()) { messages.add("A custom path to the " + getExecutable() + " executable can be set with the " + SubstrateOptionsParser.commandArgument(getPathOption(), "") + " command-line option"); } + messages.add(getDetails()); messages.add("To prevent native-toolchain checking provide command-line option " + SubstrateOptionsParser.commandArgument(SubstrateOptions.CheckToolchain, "-")); return UserError.abort(messages); } @@ -394,6 +405,21 @@ protected Wat2Wasm(Path tempDirectory) { super(tempDirectory); } + @Override + protected String getProjectName() { + return "wabt"; + } + + @Override + protected String getURL() { + return "https://github.com/WebAssembly/wabt"; + } + + @Override + protected String getMinimumVersion() { + return "1.0.32"; + } + @Override protected String getExecutable() { return "wat2wasm"; @@ -439,6 +465,21 @@ protected Binaryen(Path tempDirectory) { super(tempDirectory); } + @Override + protected String getProjectName() { + return "Binaryen"; + } + + @Override + protected String getURL() { + return "https://github.com/WebAssembly/binaryen"; + } + + @Override + protected String getMinimumVersion() { + return "119"; + } + @Override protected String getExecutable() { return "wasm-as"; diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmIRWalker.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmIRWalker.java index 27286491f3f6..d292ccf02e18 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmIRWalker.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WasmIRWalker.java @@ -35,6 +35,8 @@ import java.util.Map; import java.util.stream.Stream; +import com.oracle.svm.hosted.webimage.wasm.WebImageWasmOptions; +import com.oracle.svm.hosted.webimage.wasm.ast.id.WebImageWasmIds; import org.graalvm.collections.Pair; import com.oracle.svm.core.graal.nodes.ReadExceptionObjectNode; @@ -184,13 +186,16 @@ protected void predeclareVariables(StructuredGraph graph) { * * @param currentBlock block from which to jump from * @param successor target of the jump + * @return Whether a jump was generated */ - private void generateForwardJump(HIRBlock currentBlock, HIRBlock successor) { + private boolean generateForwardJump(HIRBlock currentBlock, HIRBlock successor) { WasmId.Label id = getForwardJumpTarget(currentBlock, successor); if (id != null) { masm.genInst(new Break(id), "forward jump"); + return true; } + return false; } private WasmId.Label getForwardJumpTarget(HIRBlock currentBlock, HIRBlock successor) { @@ -278,6 +283,85 @@ protected void lowerLoopEnd(LoopEndNode loopEnd) { /** * Lower a WithExceptionNode. * + * @param currentBlock basic block that ends with {@link WithExceptionNode} + * @param lastNode the {@link WithExceptionNode} + * @see #lowerWithExceptionExnRef(HIRBlock, WithExceptionNode) + * @see #lowerWithExceptionLegacy(HIRBlock, WithExceptionNode) + */ + @Override + protected void lowerWithException(HIRBlock currentBlock, WithExceptionNode lastNode) { + assert currentBlock.getEndNode() == lastNode : currentBlock.toString(Verbosity.Name); + if (WebImageWasmOptions.LegacyExceptions.getValue()) { + lowerWithExceptionLegacy(currentBlock, lastNode); + } else { + lowerWithExceptionExnRef(currentBlock, lastNode); + } + } + + /** + * Lower a WithExceptionNode using the exnref exception handling proposal. + * + *

    +     * {@code
    +     * (block $exnBlock (result $throwable)
    +     *     (try_table (catch $exc_tag $exnBlock)
    +     *         WithExceptionNode();
    +     *         br Successor
    +     *     )
    +     * )
    +     * (local.set $exc_var)
    +     * ExceptionEdge();
    +     * }
    +     * 
    + * + * The {@link WithExceptionNode} is wrapped in a {@code try_table} block, which is surrounded by + * a block that's the target of the catch scope. After the block around the {@code try_table} + * instruction, the thrown value is already on the stack, we store it in a dedicated local + * variable ({@link WebImageWasmNodeLowerer#exceptionObjectVariable}) so that it can be read + * later by {@link ReadExceptionObjectNode}. The + * {@link com.oracle.svm.hosted.webimage.wasm.phases.WasmLabeledBlockGeneration} ensures that + * the regular successor always requires a forward jump. + * + * @see #lowerWithException(HIRBlock, WithExceptionNode) + */ + protected void lowerWithExceptionExnRef(HIRBlock currentBlock, WithExceptionNode lastNode) { + WebImageWasmIds.InternalLabel exceptionHandlerLabel = masm.idFactory.newInternalLabel("exn" + currentBlock.getId()); + Instruction.Block exceptionTargetBlock = new Instruction.Block(exceptionHandlerLabel, masm.getWasmProviders().util().getThrowableType()); + masm.genInst(exceptionTargetBlock); + + masm.childScope(exceptionTargetBlock.instructions, exceptionTargetBlock); + Instruction.TryTable tryBlock = new Instruction.TryTable(null); + tryBlock.addCatch(masm.getKnownIds().getJavaThrowableTag(), exceptionHandlerLabel); + masm.genInst(tryBlock, lastNode); + + masm.childScope(tryBlock.instructions, tryBlock); + lowerNode(lastNode); + HIRBlock normSucc = cfg.blockFor(lastNode.next()); + boolean didJump = generateForwardJump(currentBlock, normSucc); + GraalError.guarantee(didJump, "No jump was inserted after a WithExceptionNode"); + masm.parentScope(tryBlock); + + masm.parentScope(exceptionTargetBlock); + + masm.genInst(masm.nodeLowerer().exceptionObjectVariable.setter(new Instruction.Nop()), "Store exception object"); + masm.lowerCatchPreamble(); + + CatchScopeContainer scopeEntry = (CatchScopeContainer) stackifierData.getScopeEntry(lastNode); + Scope catchScope = scopeEntry.getCatchScope(); + if (catchScope != null) { + lowerBlocks(catchScope.getSortedBlocks(stackifierData)); + // Just a sanity check + masm.genInst(new Unreachable(), "End of catch block is unreachable, it must break out"); + } else { + HIRBlock excpSucc = cfg.blockFor(lastNode.exceptionEdge()); + boolean didJumpAfterCatch = generateForwardJump(currentBlock, excpSucc); + GraalError.guarantee(didJumpAfterCatch, "No jump was inserted in catch block"); + } + } + + /** + * Lower a WithExceptionNode using the legacy exception handling proposal. + * *
          * {@code
          * (try
    @@ -301,13 +385,9 @@ protected void lowerLoopEnd(LoopEndNode loopEnd) {
          * ({@link WebImageWasmNodeLowerer#exceptionObjectVariable}) so that it can be read later by
          * {@link ReadExceptionObjectNode}.
          *
    -     * @param currentBlock basic block that ends with {@link WithExceptionNode}
    -     * @param lastNode the {@link WithExceptionNode}
    +     * @see #lowerWithException(HIRBlock, WithExceptionNode)
          */
    -    @Override
    -    protected void lowerWithException(HIRBlock currentBlock, WithExceptionNode lastNode) {
    -        assert currentBlock.getEndNode() == lastNode : currentBlock.toString(Verbosity.Name);
    -
    +    protected void lowerWithExceptionLegacy(HIRBlock currentBlock, WithExceptionNode lastNode) {
             Instruction.Try tryBlock = new Instruction.Try(null);
             masm.genInst(tryBlock, lastNode);
     
    diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WebImageWasmCodeGen.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WebImageWasmCodeGen.java
    index 4d65d92f802f..cfc100d0ffa2 100644
    --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WebImageWasmCodeGen.java
    +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/WebImageWasmCodeGen.java
    @@ -185,10 +185,6 @@ protected void emitCode() {
             module.constructActiveDataSegments();
             ((WebImageWasmHeapBreakdownProvider) HeapBreakdownProvider.singleton()).setActualTotalHeapSize((int) getFullImageHeapSize());
     
    -        if (WebImageOptions.DebugOptions.VerificationPhases.getValue(options)) {
    -            validateModule();
    -        }
    -
             emitJSCode();
     
             try (Writer writer = Files.newBufferedWriter(watFile)) {
    @@ -197,6 +193,10 @@ protected void emitCode() {
                 throw new RuntimeException(e);
             }
     
    +        if (WebImageOptions.DebugOptions.VerificationPhases.getValue(options)) {
    +            validateModule();
    +        }
    +
             assembleWasmFile(watFile, wasmFile);
         }
     
    diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wasm-as.wast b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wasm-as.wast
    index eb165423646a..5211d57c0014 100644
    --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wasm-as.wast
    +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wasm-as.wast
    @@ -23,19 +23,23 @@
     
       (start $main)
       (func $main
    -    (try
    -      (do (call $throwB))
    -      (catch $tag0
    -        (drop (call $checkException))
    -      )
    +    (block $catchBlock (result (ref $A))
    +        (try_table
    +          (catch $tag0 $catchBlock)
    +          (call $throwB)
    +          (unreachable)
    +        )
         )
    +    (drop (call $checkException))
     
    -    (try
    -      (do (call $throwC))
    -      (catch $tag0
    -        (drop (call $checkException))
    -      )
    +    (block $catchBlock (result (ref $A))
    +        (try_table
    +          (catch $tag0 $catchBlock)
    +          (call $throwC)
    +          (unreachable)
    +        )
         )
    +    (drop (call $checkException))
       )
     
       (func $checkException (param $p0 (ref $A)) (result (ref null $A))
    diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wat2wasm.wast b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wat2wasm.wast
    index af6d54a3ee8c..10a1a507eea0 100644
    --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wat2wasm.wast
    +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/codegen/verify-wat2wasm.wast
    @@ -1,7 +1,7 @@
     (;
       Wasm text file to verify the wat2wasm assembler works correctly.
     
    -  Uses features from the exception handling proposal
    +  Uses features from the legacy exception handling proposal
     ;)
     (module
       (tag $tag0 (param i32))
    diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/phases/WasmLabeledBlockGeneration.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/phases/WasmLabeledBlockGeneration.java
    index 7f8893a22d02..babc3534735d 100644
    --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/phases/WasmLabeledBlockGeneration.java
    +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/wasm/phases/WasmLabeledBlockGeneration.java
    @@ -28,6 +28,8 @@
     import com.oracle.svm.hosted.webimage.codegen.reconstruction.stackifier.LabeledBlockGeneration;
     import com.oracle.svm.hosted.webimage.codegen.reconstruction.stackifier.StackifierData;
     
    +import com.oracle.svm.hosted.webimage.wasm.WebImageWasmOptions;
    +import jdk.graal.compiler.nodes.WithExceptionNode;
     import jdk.graal.compiler.nodes.cfg.ControlFlowGraph;
     import jdk.graal.compiler.nodes.cfg.HIRBlock;
     
    @@ -53,6 +55,19 @@ public boolean isLabeledBlockNeeded(HIRBlock block, HIRBlock successor) {
                 return true;
             }
     
    +        if (!WebImageWasmOptions.LegacyExceptions.getValue() && block.getEndNode() instanceof WithExceptionNode withExceptionNode) {
    +            HIRBlock normSucc = stackifierData.getCfg().blockFor(withExceptionNode.next());
    +            if (normSucc.equals(successor)) {
    +                /*
    +                 * With the new exception handling, we need an explicit labeled block when going
    +                 * from the WithExceptionNode to its regular successor because in the Wasm code, the
    +                 * successor does not appear directly after, the catch block does, and we would then
    +                 * fall through to that.
    +                 */
    +                return true;
    +            }
    +        }
    +
             return false;
         }
     }