diff --git a/wasm-tools/src/main/java/run/endive/tools/wasm/Validate.java b/wasm-tools/src/main/java/run/endive/tools/wasm/Validate.java index 59e2863a..09ca5f0f 100644 --- a/wasm-tools/src/main/java/run/endive/tools/wasm/Validate.java +++ b/wasm-tools/src/main/java/run/endive/tools/wasm/Validate.java @@ -8,6 +8,8 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import run.endive.log.Logger; import run.endive.log.SystemLogger; @@ -21,8 +23,6 @@ public final class Validate { - private Validate() {} - private static final Logger logger = new SystemLogger() { @Override @@ -32,6 +32,16 @@ public boolean isLoggable(Logger.Level level) { }; private static final WasmModule MODULE = WasmToolsModule.load(); + private final List features; + + private Validate(List features) { + this.features = features; + } + + public static Builder builder() { + return new Builder(); + } + public static void validate(File file) { try (var is = new FileInputStream(file)) { validate(is); @@ -49,16 +59,49 @@ public static void validate(String wat) { } public static void validate(InputStream is) { + doValidate(is, Collections.emptyList()); + } + + public void validateModule(File file) { + try (var is = new FileInputStream(file)) { + validateModule(is); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void validateModule(String wat) { + try (var is = new ByteArrayInputStream(wat.getBytes(StandardCharsets.UTF_8))) { + validateModule(is); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void validateModule(InputStream is) { + doValidate(is, features); + } + + private static void doValidate(InputStream is, List features) { try (var stdinStream = new ByteArrayInputStream(is.readAllBytes()); var stdoutStream = new ByteArrayOutputStream(); var stderrStream = new ByteArrayOutputStream()) { + List args = new ArrayList<>(); + args.add("wasm-tools"); + args.add("validate"); + if (!features.isEmpty()) { + args.add("--features"); + args.add(String.join(",", features)); + } + args.add("-"); + var options = WasiOptions.builder() .withStdin(stdinStream, false) .withStdout(stdoutStream, false) .withStderr(stderrStream, false) - .withArguments(List.of("wasm-tools", "validate", "-")) + .withArguments(args) .build(); logger.info("Running command: " + String.join(" ", options.arguments())); @@ -86,4 +129,26 @@ public static void validate(InputStream is) { throw new UncheckedIOException(e); } } + + public static final class Builder { + private final List features = new ArrayList<>(); + + private Builder() {} + + public Builder withFeatures(WasmFeature... features) { + for (WasmFeature f : features) { + this.features.add(f.flag()); + } + return this; + } + + public Builder withoutFeature(WasmFeature feature) { + this.features.add(feature.negatedFlag()); + return this; + } + + public Validate build() { + return new Validate(Collections.unmodifiableList(new ArrayList<>(features))); + } + } } diff --git a/wasm-tools/src/main/java/run/endive/tools/wasm/WasmFeature.java b/wasm-tools/src/main/java/run/endive/tools/wasm/WasmFeature.java new file mode 100644 index 00000000..8b85e71e --- /dev/null +++ b/wasm-tools/src/main/java/run/endive/tools/wasm/WasmFeature.java @@ -0,0 +1,76 @@ +package run.endive.tools.wasm; + +/** + * WebAssembly features and feature groups for use with {@link Validate}. + * + *

Feature groups ({@link #MVP}, {@link #WASM1}, {@link #WASM2}, {@link #WASM3}, {@link #LIME1}) + * reset the feature set to a predefined baseline. Individual features can then be enabled or + * disabled on top. Order matters: groups reset, then individual features toggle. + * + *

The flag strings correspond to the {@code wasm-tools validate --features} CLI flags from + * wasm-tools v1.240.0. + */ +public enum WasmFeature { + + // Feature groups + MVP("mvp"), + WASM1("wasm1"), + WASM2("wasm2"), + WASM3("wasm3"), + LIME1("lime1"), + + // Meta-flag + ALL("all"), + + // Individual features + MUTABLE_GLOBAL("mutable-global"), + SATURATING_FLOAT_TO_INT("saturating-float-to-int"), + SIGN_EXTENSION("sign-extension"), + REFERENCE_TYPES("reference-types"), + MULTI_VALUE("multi-value"), + BULK_MEMORY("bulk-memory"), + SIMD("simd"), + RELAXED_SIMD("relaxed-simd"), + THREADS("threads"), + SHARED_EVERYTHING_THREADS("shared-everything-threads"), + TAIL_CALL("tail-call"), + FLOATS("floats"), + MULTI_MEMORY("multi-memory"), + EXCEPTIONS("exceptions"), + MEMORY64("memory64"), + EXTENDED_CONST("extended-const"), + COMPONENT_MODEL("component-model"), + FUNCTION_REFERENCES("function-references"), + MEMORY_CONTROL("memory-control"), + GC("gc"), + CUSTOM_PAGE_SIZES("custom-page-sizes"), + LEGACY_EXCEPTIONS("legacy-exceptions"), + GC_TYPES("gc-types"), + STACK_SWITCHING("stack-switching"), + WIDE_ARITHMETIC("wide-arithmetic"), + CM_VALUES("cm-values"), + CM_NESTED_NAMES("cm-nested-names"), + CM_ASYNC("cm-async"), + CM_ASYNC_STACKFUL("cm-async-stackful"), + CM_ASYNC_BUILTINS("cm-async-builtins"), + CM_THREADING("cm-threading"), + CM_ERROR_CONTEXT("cm-error-context"), + CM_FIXED_SIZE_LIST("cm-fixed-size-list"), + CM_GC("cm-gc"), + CALL_INDIRECT_OVERLONG("call-indirect-overlong"), + BULK_MEMORY_OPT("bulk-memory-opt"); + + private final String flag; + + WasmFeature(String flag) { + this.flag = flag; + } + + public String flag() { + return flag; + } + + public String negatedFlag() { + return "-" + flag; + } +} diff --git a/wasm-tools/src/test/java/run/endive/tools/wasm/WasmToolsTest.java b/wasm-tools/src/test/java/run/endive/tools/wasm/WasmToolsTest.java index e80d958f..c4466200 100644 --- a/wasm-tools/src/test/java/run/endive/tools/wasm/WasmToolsTest.java +++ b/wasm-tools/src/test/java/run/endive/tools/wasm/WasmToolsTest.java @@ -100,4 +100,47 @@ public void shouldValidateWat() { exitException.getMessage().contains("failed to validate"), "found: " + exitException.getMessage() + " doesn't contains the expected result"); } + + @Test + public void shouldValidateSimpleModuleWithWasm1() { + Validate.builder() + .withFeatures(WasmFeature.WASM1) + .build() + .validateModule( + "(module (func (export \"add\")" + + " (param i32) (param i32) (result i32)" + + " (i32.add (local.get 0) (local.get 1))))"); + } + + @Test + public void shouldRejectSimdModuleWithWasm1() { + var validator = Validate.builder().withFeatures(WasmFeature.WASM1).build(); + assertThrows( + WatParseException.class, + () -> + validator.validateModule( + "(module (func (result v128) (v128.const i32x4 0 0 0 0)))")); + } + + @Test + public void shouldAcceptSimdModuleWithWasm2() { + Validate.builder() + .withFeatures(WasmFeature.WASM2) + .build() + .validateModule("(module (func (result v128) (v128.const i32x4 0 0 0 0)))"); + } + + @Test + public void shouldRejectSimdModuleWhenDisabled() { + var validator = + Validate.builder() + .withFeatures(WasmFeature.WASM2) + .withoutFeature(WasmFeature.SIMD) + .build(); + assertThrows( + WatParseException.class, + () -> + validator.validateModule( + "(module (func (result v128) (v128.const i32x4 0 0 0 0)))")); + } }