Skip to content

Commit

Permalink
Merge pull request #37 from kelnishi/wasm-3.0
Browse files Browse the repository at this point in the history
Wasm 3.0
  • Loading branch information
kelnishi authored Jan 14, 2025
2 parents 9365a29 + b65d393 commit bc4bb8e
Show file tree
Hide file tree
Showing 87 changed files with 2,097 additions and 610 deletions.
3 changes: 2 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[submodule "Spec.Test/spec"]
path = Spec.Test/spec
url = [email protected]:WebAssembly/gc.git
url = [email protected]:WebAssembly/spec.git
branch = wasm-3.0
[submodule "Spec.Test/wasi"]
path = Spec.Test/wasi
url = [email protected]:WebAssembly/wasi-testsuite.git
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.7.0]
- wasm-3.0 spec support
- exnref/tag support
- memory64 support
- multi-memory support (enabled)

## [0.6.0]
- wasm-gc extension
- function-references extension
Expand Down
2 changes: 1 addition & 1 deletion Feature.Detect/DetectFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void Detect(FeatureJson.FeatureJson file)
}
catch (Exception e)
{
Assert.Fail($"{file.Name} support not detected.\n{e}");
Assert.Fail($"{file.Name} ({file.Module}) support not detected.\n{e}");
}

}
Expand Down
2 changes: 1 addition & 1 deletion Feature.Detect/proposals
Submodule proposals updated 2 files
+3 −2 README.md
+10 −10 finished-proposals.md
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ WACS supports the latest standardized webassembly feature extensions including *
- **Unity Compatibility**: Compatible with **Unity 2021.3+** including AOT/IL2CPP modes for iOS.
- **Pure C# Implementation**: Written in C# 9.0/.NET Standard 2.1. (No unsafe code)
- **No Complex Dependencies**: Uses [FluentValidation](https://github.com/FluentValidation/FluentValidation) and [Microsoft.Extensions.ObjectPool](https://www.nuget.org/packages/Microsoft.Extensions.ObjectPool) as its only dependencies.
- **WebAssembly+GC Spec Compliance**: Passes the [WebAssembly GC spec test suite](https://github.com/WebAssembly/gc/tree/main/test/core).
- **WebAssembly 3.0 Spec Compliance**: Passes the [WebAssembly 3.0](https://webassembly.github.io/spec/versions/core/WebAssembly-3.0-draft.pdf) spec [test suite](https://github.com/WebAssembly/spec/tree/wasm-3.0).
- **Magical Interop**: Host bindings are validated with reflection, no boilerplate code required.
- **Async Tasks**: [JSPI](https://github.com/WebAssembly/js-promise-integration)-like non-blocking calls for async functions.
- **WASI:** Wacs.WASIp1 provides a [wasi\_snapshot\_preview1](https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md) implementation.
Expand All @@ -42,7 +42,7 @@ Because WebAssembly is memory-safe and can be ahead-of-time validated, WACS make
UGC, DLC, or plugin systems that include executable logic.

## WebAssembly Feature Extensions
WACS is based on the [WebAssembly Core 2 + gc spec](https://webassembly.github.io/gc/core/bikeshed/index.html) and passes the associated test suite.
WACS is based on the [WebAssembly Core 3 draft spec](https://webassembly.github.io/spec/versions/core/WebAssembly-3.0-draft.pdf) and passes the associated [test suite](https://github.com/WebAssembly/spec/tree/wasm-3.0).

Support for all standardized extensions is listed below.

Expand All @@ -66,9 +66,9 @@ Harnessed results from [wasm-feature-detect](https://github.com/GoogleChromeLabs
|[Tail call](https://github.com/webassembly/tail-call)|tail_call||
|[Typed Function References](https://github.com/WebAssembly/function-references)|function-references||
|Phase 4|
|[Exception handling](https://github.com/WebAssembly/exception-handling)|exceptions||
|[Exception handling](https://github.com/WebAssembly/exception-handling)|exceptions||
|[JS String Builtins](https://github.com/WebAssembly/js-string-builtins)|||
|[Memory64](https://github.com/WebAssembly/memory64)|memory64||
|[Memory64](https://github.com/WebAssembly/memory64)|memory64||
|[Threads](https://github.com/webassembly/threads)|threads||
|Phase 3|
|[JS Promise Integration](https://github.com/WebAssembly/js-promise-integration)|jspi|<span title="Browser idiom, but conceptually supported">✳️</span>|
Expand Down
4 changes: 3 additions & 1 deletion Spec.Test/SpecTestEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public void BindToRuntime(WasmRuntime runtime)
runtime.BindHostGlobal((module, "global_f64"), new GlobalType(ValType.F64, Mutability.Immutable),
new Value(ValType.F64, 666.6));

runtime.BindHostTable((module, "table"), new TableType(ValType.FuncRef,new Limits(10,20)),
runtime.BindHostTable((module, "table"), new TableType(ValType.FuncRef, new Limits(AddrType.I32, 10,20)),
new Value(ValType.FuncRef));
runtime.BindHostTable((module, "table64"), new TableType(ValType.FuncRef, new Limits(AddrType.I64, 10,20)),
new Value(ValType.FuncRef));

runtime.BindHostMemory((module, "memory"), new MemoryType(minimum:1, maximum:2));
Expand Down
7 changes: 7 additions & 0 deletions Spec.Test/WastJson/Argument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class Argument
[JsonPropertyName("value")]
public object? Value { get; set; }

[JsonPropertyName("values")]
public List<Argument>? Values { get; set; }

public Value AsValue =>
Type switch
Expand All @@ -57,9 +59,14 @@ public class Argument
"arrayref" => new Value(ValType.Array, Value?.ToString()??"null"),
"eqref" => new Value(ValType.Eq, Value?.ToString()??"null"),
"i31ref" => new Value(ValType.I31, Value?.ToString()??"null"),
"exnref" => new Value(ValType.Exn, Value?.ToString()??"null"),
"nullexnref" => new Value(ValType.NoExn, Value?.ToString()??"null"),
"ref" => new Value(ValType.Ref, Value?.ToString()??"null"),
_ => throw new ArgumentException($"Cannot parse value {Value} of type {Type}")
};

public List<Value> AsValues =>
Values?.Select(v => v.AsValue).ToList() ?? new List<Value>();

private Value ParseV128(object? value)
{
Expand Down
6 changes: 6 additions & 0 deletions Spec.Test/WastJson/CommandType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public enum CommandType
[EnumMember(Value = "module")]
Module,

[EnumMember(Value = "module_definition")]
ModuleDefinition,

[EnumMember(Value = "register")]
Register,

Expand All @@ -36,6 +39,9 @@ public enum CommandType

[EnumMember(Value = "assert_trap")]
AssertTrap,

[EnumMember(Value = "assert_exception")]
AssertException,

[EnumMember(Value = "assert_exhaustion")]
AssertExhaustion,
Expand Down
163 changes: 139 additions & 24 deletions Spec.Test/WastJson/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime,

public class ModuleCommand : ICommand
{
private SpecTestEnv _env = new SpecTestEnv();
[JsonPropertyName("filename")] public string? Filename { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }

Expand All @@ -71,6 +70,76 @@ public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime,
public override string ToString() => $"Load Module {{ Line = {Line}, Filename = {Filename} }}";
}

public class ModuleDefinition : ICommand
{
[JsonPropertyName("filename")] public string? Filename { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("module_type")] public string? ModuleType { get; set; }

public CommandType Type => CommandType.ModuleDefinition;

[JsonPropertyName("line")] public int Line { get; set; }

public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime, ref Module? module)
{
List<Exception> errors = new();

if (ModuleType == "text")
{
errors.Add(new Exception(
$"Module Definition line {Line}: Skipping module_definition. No WAT parsing."));
return errors;
}

if (Filename == null)
throw new ArgumentException("Json missing `filename` field");

if (string.IsNullOrEmpty(Name))
throw new ArgumentException("Json missing `name` field");

var filepath = Path.Combine(testDefinition.Path, Filename);
using var fileStream = new FileStream(filepath, FileMode.Open);
module = BinaryModuleParser.ParseWasm(fileStream);
module.SetName(Name);

return errors;
}
}

public class ModuleInstanceCommand : ICommand
{
[JsonPropertyName("module")] public string? Module { get; set; }

[JsonPropertyName("instance")] public string? Instance { get; set; }

public CommandType Type => CommandType.ModuleInstance;
[JsonPropertyName("line")] public int Line { get; set; }

public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime, ref Module? module)
{
List<Exception> errors = new();
if (Instance == null)
throw new ArgumentException("Json missing `instance` field");

if (module == null)
throw new ArgumentException("Module not loaded");

if (module.Name != Module)
throw new ArgumentException($"Module name mismatch: {module.Name} != {Module}");

if (string.IsNullOrEmpty(Instance))
throw new ArgumentException("Json missing `instance` field");

var modInst = runtime.InstantiateModule(module);

modInst.Name = Instance;

return errors;
}

public override string ToString() => $"Module Instance {{ Line = {Line}, Module = {Module} }}";
}

public class RegisterCommand : ICommand
{
[JsonPropertyName("name")] public string? Name { get; set; }
Expand Down Expand Up @@ -119,6 +188,22 @@ public class AssertReturnCommand : ICommand
[JsonPropertyName("expected")] public List<Argument> Expected { get; set; } = new();
public CommandType Type => CommandType.AssertReturn;
[JsonPropertyName("line")] public int Line { get; set; }

static bool CompareValues(Value actual, Value expected)
{
//HACK: null ref comparison
if (expected.IsNullRef)
{
if (!actual.IsNullRef && !actual.Type.Matches(expected.Type, null))
return false;
}
else if (!actual.Equals(expected))
{
return false;
}

return true;
}

public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime, ref Module? module)
{
Expand All @@ -133,19 +218,24 @@ public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime,
throw new TestException(
$"Test failed {this} \"{invokeAction.Field}\": Expected [{string.Join(" ", Expected.Select(e => e.AsValue))}], but got [{string.Join(" ", result)}]");

foreach (var (actual, expected) in result.Zip(Expected, (a, e) => (a, e.AsValue)))
foreach (var (actual, arg) in result.Zip(Expected, (a, e) => (a, e)))
{
//HACK: null ref comparison
if (expected.IsNullRef)
{
if (!actual.IsNullRef && !actual.Type.Matches(expected.Type, null))
if (arg.Type == "either")
{
if (!arg.AsValues.Any(v => CompareValues(actual, v)))
{
throw new TestException(
$"Test failed {this} \"{invokeAction.Field}\": Expected [{string.Join(" ", Expected.Select(e => e.AsValue))}], but got [{string.Join(" ", result)}]");
$"Test failed {this} \"{invokeAction.Field}\": \nExpected \n either:[{string.Join("]\n or:[", Expected.SelectMany(e => e.AsValues))}], \nbut got:[{string.Join(" ", result)}]");
}
}
else if (!actual.Equals(expected))
else
{
throw new TestException(
$"Test failed {this} \"{invokeAction.Field}\": Expected [{string.Join(" ", Expected.Select(e => e.AsValue))}], but got [{string.Join(" ", result)}]");
var expected = arg.AsValue;
if (!CompareValues(actual, expected))
{
throw new TestException(
$"Test failed {this} \"{invokeAction.Field}\": \nExpected [{string.Join(" ", Expected.Select(e => e.AsValue))}],\n but got [{string.Join(" ", result)}]");
}
}
}
break;
Expand Down Expand Up @@ -192,6 +282,43 @@ public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime,

public override string ToString() => $"Assert Trap {{ Line = {Line}, Action = {Action}, Text = {Text} }}";
}

public class AssertExceptionCommand : ICommand
{
[JsonPropertyName("action")] public IAction? Action { get; set; }
[JsonPropertyName("text")] public string? Text { get; set; }
public CommandType Type => CommandType.AssertException;
[JsonPropertyName("line")] public int Line { get; set; }

public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime, ref Module? module)
{
List<Exception> errors = new();

switch (Action)
{
case InvokeAction invokeAction:
bool didThrow = false;
string throwMessage = "";
try
{
var result = invokeAction.Invoke(ref runtime, ref module);
}
catch (UnhandledWasmException e)
{
didThrow = true;
throwMessage = e.Message;
}

if (!didThrow)
throw new TestException($"Test failed {this} \"{throwMessage}\"");
break;
}

return errors;
}

public override string ToString() => $"Assert Trap {{ Line = {Line}, Action = {Action}, Text = {Text} }}";
}

public class AssertExhaustionCommand : ICommand
{
Expand Down Expand Up @@ -586,20 +713,6 @@ public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime,
public override string ToString() => $"Assert Exclude From Must {{ Line = {Line}, Module = {Module} }}";
}

public class ModuleInstanceCommand : ICommand
{
[JsonPropertyName("module")] public string? Module { get; set; }
public CommandType Type => CommandType.ModuleInstance;
[JsonPropertyName("line")] public int Line { get; set; }

public List<Exception> RunTest(WastJson testDefinition, ref WasmRuntime runtime, ref Module? module)
{
throw new InvalidDataException($"Test command not setup:{this} from {testDefinition.TestName}");
}

public override string ToString() => $"Module Instance {{ Line = {Line}, Module = {Module} }}";
}

public class ModuleExclusiveCommand : ICommand
{
[JsonPropertyName("module")] public string? Module { get; set; }
Expand Down Expand Up @@ -659,6 +772,7 @@ public class CommandJsonConverter : JsonConverter<ICommand>
CommandType.Action => JsonSerializer.Deserialize<ActionCommand>(root.GetRawText(), options),
CommandType.AssertReturn => JsonSerializer.Deserialize<AssertReturnCommand>(root.GetRawText(), options),
CommandType.AssertTrap => JsonSerializer.Deserialize<AssertTrapCommand>(root.GetRawText(), options),
CommandType.AssertException => JsonSerializer.Deserialize<AssertExceptionCommand>(root.GetRawText(), options),
CommandType.AssertExhaustion => JsonSerializer.Deserialize<AssertExhaustionCommand>(root.GetRawText(), options),
CommandType.AssertInvalid => JsonSerializer.Deserialize<AssertInvalidCommand>(root.GetRawText(), options),
CommandType.AssertMalformed => JsonSerializer.Deserialize<AssertMalformedCommand>(root.GetRawText(), options),
Expand All @@ -674,6 +788,7 @@ public class CommandJsonConverter : JsonConverter<ICommand>
CommandType.AssertTerminated => JsonSerializer.Deserialize<AssertTerminatedCommand>(root.GetRawText(), options),
CommandType.AssertUndefined => JsonSerializer.Deserialize<AssertUndefinedCommand>(root.GetRawText(), options),
CommandType.AssertExcludeFromMust => JsonSerializer.Deserialize<AssertExcludeFromMustCommand>(root.GetRawText(), options),
CommandType.ModuleDefinition => JsonSerializer.Deserialize<ModuleDefinition>(root.GetRawText(), options),
CommandType.ModuleInstance => JsonSerializer.Deserialize<ModuleInstanceCommand>(root.GetRawText(), options),
CommandType.ModuleExclusive => JsonSerializer.Deserialize<ModuleExclusiveCommand>(root.GetRawText(), options),
CommandType.Pump => JsonSerializer.Deserialize<PumpCommand>(root.GetRawText(), options),
Expand Down
1 change: 0 additions & 1 deletion Spec.Test/WastJsonTestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ static WastJsonTestData()
}

private static string JsonDirectory => Path.Combine(AppContext.BaseDirectory, Configuration["JsonDirectory"] ?? "");
public static bool RunTranspilerTests => Configuration["RunTranspilerTests"] == "True";

public static string SingleTest => Configuration["Single"] ?? "";
public static HashSet<string> SkipWasts =>
Expand Down
8 changes: 0 additions & 8 deletions Spec.Test/WastTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ public void RunWast(WastJson.WastJson file)
SpecTestEnv env = new SpecTestEnv();
WasmRuntime runtime = new();
env.BindToRuntime(runtime);

//Make multiple memories fail validation
ModuleValidator.ValidateMultipleMemories = false;
runtime.TranspileModules = false;

Module? module = null;
Expand All @@ -70,16 +67,11 @@ public void RunWast(WastJson.WastJson file)
[ClassData(typeof(WastJsonTestData))]
public void RunWastTranspiled(WastJson.WastJson file)
{
if (!WastJsonTestData.RunTranspilerTests)
throw SkipException.ForSkip("Skipping transpiled test");

_output.WriteLine($"Running test:{file.TestName}");
SpecTestEnv env = new SpecTestEnv();
WasmRuntime runtime = new();
env.BindToRuntime(runtime);

//Make multiple memories fail validation
ModuleValidator.ValidateMultipleMemories = false;
runtime.TranspileModules = true;

Module? module = null;
Expand Down
2 changes: 1 addition & 1 deletion Spec.Test/spec
Submodule spec updated 316 files
3 changes: 2 additions & 1 deletion Spec.Test/testsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"RunTranspilerTests": true,
"Single": null,
"SkipWasts": [
"comments.wast"
"comments.wast",
"annotations.wast"
]
}
Loading

0 comments on commit bc4bb8e

Please sign in to comment.