From 9b3a3871cd71df80271fff5e965afce0f877311e Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 04:43:30 -0700 Subject: [PATCH 01/12] Materialize Vector128 as a wasm v128 in the codegen ABI Vector128 is the one SIMD type the JIT recognizes as TYP_SIMD16 on wasm (the others are TARGET_ARM64/TARGET_XARCH-only or multi-field structs passed by reference), so treat it as the wasm v128 ABI primitive. The lowering now maps Vector128 to CORINFO_WASM_TYPE_V128 -> TYP_SIMD16, and the codegen paths that previously bailed to the interpreter (SIMD parameter, SIMD16 local field load, store-indirect, call argument, and local load) emit real v128 values instead of producing an invalid module. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 39 +---------------- src/coreclr/jit/targetwasm.cpp | 3 +- .../CompilerTypeSystemContext.Wasm.cs | 15 +++++++ .../tools/Common/JitInterface/CorInfoImpl.cs | 2 + .../tools/Common/JitInterface/WasmLowering.cs | 42 ++++++++++++++++++- .../TestCases/R2RTestSuites.cs | 36 ++++++++++++++++ .../TestCases/Webcil/WasmSimdModule.cs | 38 +++++++++++++++++ .../WasmInterpreterToR2RThunkNode.cs | 3 ++ 8 files changed, 137 insertions(+), 41 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 1c00c386259e20..9a8d017856071d 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -105,18 +105,6 @@ void CodeGen::genMarkLabelsForCodegen() // void CodeGen::genBeginFnProlog() { - // SIMD (Vector2/3/4, Vector128) parameters are lowered to i32 in the wasm signature, so any - // vector operation performed on them produces an invalid module (e.g. a v128/f64 op with - // an i32 operand). Bail such methods to the interpreter until SIMD parameters are - // properly supported in the wasm calling convention. - for (unsigned lclNum = 0; lclNum < m_compiler->info.compArgsCount; lclNum++) - { - if (varTypeIsSIMD(m_compiler->lvaGetDesc(lclNum)->TypeGet())) - { - NYI_WASM_SIMD("SIMD parameter"); - } - } - GetEmitter()->emitIns(INS_code_size); FuncInfoDsc* const func = m_compiler->funGetFunc(ROOT_FUNC_IDX); @@ -2585,11 +2573,6 @@ void CodeGen::genCodeForLclFld(GenTreeLclFld* tree) LclVarDsc* varDsc = m_compiler->lvaGetDesc(tree); var_types type = tree->TypeGet(); - if (type == TYP_SIMD16) - { - NYI_WASM_SIMD("SIMD16 local field load"); - } - GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); GetEmitter()->emitIns_S(ins_Load(type), emitTypeSize(tree), tree->GetLclNum(), tree->GetLclOffs()); WasmProduceReg(tree); @@ -2747,15 +2730,8 @@ void CodeGen::genCodeForStoreInd(GenTreeStoreInd* tree) } else // A normal store, not a WriteBarrier store { - var_types type = tree->TypeGet(); - if (type == TYP_SIMD16) - { - // Storing a SIMD16 value emits v128.store, but the data operand is not - // materialized as a v128 (it comes through as an i32), producing an invalid - // module. Bail until SIMD16 store is properly supported. - NYI_WASM_SIMD("SIMD16 store indirect"); - } - instruction ins = ins_Store(type); + var_types type = tree->TypeGet(); + instruction ins = ins_Store(type); // TODO-WASM: Memory barriers @@ -2854,13 +2830,6 @@ void CodeGen::genCallInstruction(GenTreeCall* call) assert(seg.IsPassedInRegister()); WasmValueType wvt = WasmRegToType(seg.GetRegister()); assert(wvt < WasmValueType::Count); - if (wvt == WasmValueType::V128) - { - // Passing a 16-byte SIMD value by value through a call is not yet correctly - // implemented: the argument is materialized as an i32 (by-ref) while the call - // signature requires v128, producing an invalid module. Bail for now. - NYI_WASM_SIMD("SIMD16 call argument"); - } typeStack.Push((CorInfoWasmType)emitter::GetWasmValueTypeCode(wvt)); } } @@ -3769,10 +3738,6 @@ void CodeGen::genLoadLocalIntoReg(regNumber targetReg, unsigned lclNum) { LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); var_types type = varDsc->GetRegisterType(); - if (type == TYP_SIMD16) - { - NYI_WASM_SIMD("SIMD16 local load"); - } GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetFramePointerRegIndex()); GetEmitter()->emitIns_S(ins_Load(type), emitTypeSize(type), lclNum, 0); diff --git a/src/coreclr/jit/targetwasm.cpp b/src/coreclr/jit/targetwasm.cpp index 0a9db2bf8cd5b6..94392952e15bf6 100644 --- a/src/coreclr/jit/targetwasm.cpp +++ b/src/coreclr/jit/targetwasm.cpp @@ -46,8 +46,7 @@ var_types WasmClassifier::ToJitType(CorInfoWasmType wasmType) case CORINFO_WASM_TYPE_F64: return TYP_DOUBLE; case CORINFO_WASM_TYPE_V128: - // TODO-WASM: Simd support - unreached(); + return TYP_SIMD16; default: unreached(); } diff --git a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs index f3a270d255af18..bd6ec1ef7b184a 100644 --- a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs +++ b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs @@ -12,6 +12,21 @@ public partial class CompilerTypeSystemContext private readonly object _structCacheLock = new object(); private readonly Dictionary _structsBySize = new Dictionary(); private volatile TypeDesc _cachedEmptyStruct; + private volatile TypeDesc _cachedV128Type; + + /// + /// Gets the first SIMD v128 type encountered during lowering, or null if none has been seen. + /// Used by RaiseSignature to produce a roundtrippable type for the 'V' encoding. + /// + public TypeDesc CachedV128Type => _cachedV128Type; + + /// + /// Caches a SIMD v128 type discovered during lowering. Only the first one is retained. + /// + public void CacheV128Type(TypeDesc type) + { + _cachedV128Type ??= type; + } /// /// Gets the first empty struct type encountered during lowering, or null if none has been seen. diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 37badc2c91ed76..f322f690685e4a 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3900,6 +3900,8 @@ private CorInfoWasmType getWasmLowering(CORINFO_CLASS_STRUCT_* structHnd) return CorInfoWasmType.CORINFO_WASM_TYPE_F32; case WasmValueType.F64: return CorInfoWasmType.CORINFO_WASM_TYPE_F64; + case WasmValueType.V128: + return CorInfoWasmType.CORINFO_WASM_TYPE_V128; default: ThrowHelper.ThrowInvalidProgramException(); return CorInfoWasmType.CORINFO_WASM_TYPE_I32; // unreachable diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index ddbb03db80bdff..e0c3372475df10 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -38,6 +38,14 @@ public static MethodSignature GetStringCtorActualSignature(MethodSignature signa public static TypeDesc LowerToAbiType(TypeDesc type) { + // Vector128 is the only SIMD type passed by value as a wasm v128. The other SIMD + // types (Vector2/3/4, Vector64, Vector, ...) are not yet handled by the wasm + // calling convention and fall through to the generic struct lowering below. + if (IsWasmV128Type(type)) + { + return type; + } + if (!(type.IsValueType && !type.IsPrimitive)) { return type; @@ -87,10 +95,30 @@ public static TypeDesc LowerToAbiType(TypeDesc type) } } + /// + /// Determines whether a type is , the + /// only SIMD vector type currently passed and returned by value as a wasm v128. The + /// JIT recognizes Vector128<T> as TYP_SIMD16 on wasm; the other SIMD + /// types (Vector2/3/4, Vector64/256/512<T>, Vector<T>, ...) are not yet handled + /// by the wasm calling convention and continue to use the generic struct ABI. + /// + public static bool IsWasmV128Type(TypeDesc type) + { + return type is MetadataType metadataType && + metadataType.IsIntrinsic && + metadataType.Namespace == "System.Runtime.Intrinsics"u8 && + metadataType.Name == "Vector128`1"u8; + } + public static WasmValueType LowerType(TypeDesc type) { WasmValueType pointerType = (type.Context.Target.PointerSize == 4) ? WasmValueType.I32 : WasmValueType.I64; + if (IsWasmV128Type(type)) + { + return WasmValueType.V128; + } + TypeDesc abiType = LowerToAbiType(type); if (abiType == null) @@ -165,7 +193,8 @@ public static WasmValueType LowerType(TypeDesc type) 'l' => context.GetWellKnownType(WellKnownType.Int64), 'f' => context.GetWellKnownType(WellKnownType.Single), 'd' => context.GetWellKnownType(WellKnownType.Double), - 'V' => throw new NotSupportedException("SIMD types are not supported in this version of the compiler"), + 'V' => ((CompilerTypeSystemContext)context).CachedV128Type + ?? throw new InvalidOperationException("Encountered 'V' in signature but no v128 type was cached during lowering"), _ => throw new InvalidOperationException($"Unknown signature char: {c}") }; @@ -347,7 +376,12 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag } else { - sigBuilder.Append(WasmValueTypeToSigChar(LowerType(loweredReturnType))); + WasmValueType returnWasmType = LowerType(loweredReturnType); + if (returnWasmType == WasmValueType.V128) + { + ((CompilerTypeSystemContext)returnType.Context).CacheV128Type(loweredReturnType); + } + sigBuilder.Append(WasmValueTypeToSigChar(returnWasmType)); } // Reserve space for potential implicit this, stack pointer parameter, portable entrypoint parameter, @@ -424,6 +458,10 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag else { WasmValueType paramWasmType = LowerType(paramType); + if (paramWasmType == WasmValueType.V128) + { + ((CompilerTypeSystemContext)paramType.Context).CacheV128Type(paramType); + } sigBuilder.Append(WasmValueTypeToSigChar(paramWasmType)); result.Add(paramWasmType); } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index aee0bc1e77cb30..83b70bd5d3a98a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -97,6 +97,42 @@ static void Validate(ReadyToRunReader reader) } } + [Fact] + public void WasmSimdModule() + { + var wasmSimdModule = new CompiledAssembly + { + AssemblyName = nameof(WasmSimdModule), + SourceResourceNames = ["Webcil/WasmSimdModule.cs"], + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(WasmSimdModule), + [ + new(nameof(WasmSimdModule), [new CrossgenAssembly(wasmSimdModule)]) + { + OutputFileExtension = ".wasm", + AdditionalArgs = + { + "--targetarch", + "wasm", + "--targetos", + "browser", + }, + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + var webcilReader = Assert.IsType(reader.CompositeReader); + Assert.True(webcilReader.IsWasmWrapped); + Assert.Equal(WasmMachine.Wasm32, reader.Machine); + Assert.True(R2RAssert.GetAllMethods(reader).Exists(method => + method.SignatureString.Contains("Echo", StringComparison.Ordinal))); + } + } + [Fact] public void RuntimeFunctionsSectionSizeExcludesSentinel() { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs new file mode 100644 index 00000000000000..4fbd65164aa100 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace Webcil; + +// Exercises the wasm v128 calling convention (SIMD passed/returned/stored by value) +// without relying on any SIMD arithmetic intrinsics, so only the ABI/materialization +// paths are covered. +public static class WasmSimdModule +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector128 Echo(Vector128 value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector128 ThroughLocal(Vector128 value) + { + Vector128 local = value; + return local; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Store(Vector128 value, ref Vector128 destination) + { + destination = value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector128 CallEcho(Vector128 value) + { + return Echo(value); + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs index 0a88512554eebc..7e597bf5218819 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -209,6 +209,9 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr case WasmValueType.F64: expressions.Add(F64.Load((ulong)interpOffsets[i])); break; + case WasmValueType.V128: + expressions.Add(V128.Load((ulong)interpOffsets[i])); + break; default: throw new Exception("Unexpected wasm type for interpreter-to-R2R arg"); } From 30f4bceac1af5c7d399facd3212ae323e31e8ffa Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 04:57:37 -0700 Subject: [PATCH 02/12] Address PR review feedback for the wasm v128 ABI Strengthen the WasmSimdModule test to assert that all four methods (Echo, ThroughLocal, Store, CallEcho) are present in the compiled output rather than just one, so a regression in any single ABI/materialization path is caught. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 83b70bd5d3a98a..a6f3f59b9d9706 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -128,8 +128,14 @@ static void Validate(ReadyToRunReader reader) var webcilReader = Assert.IsType(reader.CompositeReader); Assert.True(webcilReader.IsWasmWrapped); Assert.Equal(WasmMachine.Wasm32, reader.Machine); - Assert.True(R2RAssert.GetAllMethods(reader).Exists(method => - method.SignatureString.Contains("Echo", StringComparison.Ordinal))); + + List methods = R2RAssert.GetAllMethods(reader); + foreach (string name in new[] { "Echo", "ThroughLocal", "Store", "CallEcho" }) + { + Assert.True( + methods.Exists(method => method.SignatureString.Contains($".{name}(", StringComparison.Ordinal)), + $"Expected compiled method '{name}' in the wasm SIMD module."); + } } } From 02e071131cecfdae57ec2243551e347c834c123c Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 06:23:47 -0700 Subject: [PATCH 03/12] Reuse the already-lowered param type when computing its wasm value type GetSignature already computes loweredParamType via LowerToAbiType, so pass it to LowerType directly instead of re-lowering paramType, avoiding a redundant field walk for single-field structs and matching the return-type lowering pattern. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/tools/Common/JitInterface/WasmLowering.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index e0c3372475df10..b6957ac9c6bf67 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -457,7 +457,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag } else { - WasmValueType paramWasmType = LowerType(paramType); + WasmValueType paramWasmType = LowerType(loweredParamType); if (paramWasmType == WasmValueType.V128) { ((CompilerTypeSystemContext)paramType.Context).CacheV128Type(paramType); From f94e18d7d3b27ac1441d02a149da9f4ed979a795 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 07:07:43 -0700 Subject: [PATCH 04/12] Cache the lowered param type as the representative v128 type The V128 branch already has loweredParamType (guaranteed to be the Vector128 that lowered to 'V'), so cache it directly instead of the outer paramType. This keeps the cached representative consistent with the type that produced the 'V' encoding for single-field struct wrappers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/tools/Common/JitInterface/WasmLowering.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index b6957ac9c6bf67..2b870f24714a3d 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -460,7 +460,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag WasmValueType paramWasmType = LowerType(loweredParamType); if (paramWasmType == WasmValueType.V128) { - ((CompilerTypeSystemContext)paramType.Context).CacheV128Type(paramType); + ((CompilerTypeSystemContext)paramType.Context).CacheV128Type(loweredParamType); } sigBuilder.Append(WasmValueTypeToSigChar(paramWasmType)); result.Add(paramWasmType); From 5a9009155b31738df1d109352e8b16a6477d4dec Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 07:42:02 -0700 Subject: [PATCH 05/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/coreclr/tools/Common/JitInterface/WasmLowering.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 2b870f24714a3d..f9e66cc55d7045 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -102,7 +102,7 @@ public static TypeDesc LowerToAbiType(TypeDesc type) /// types (Vector2/3/4, Vector64/256/512<T>, Vector<T>, ...) are not yet handled /// by the wasm calling convention and continue to use the generic struct ABI. /// - public static bool IsWasmV128Type(TypeDesc type) + private static bool IsWasmV128Type(TypeDesc type) { return type is MetadataType metadataType && metadataType.IsIntrinsic && From b4afc1b098ba66754ad202bf71de18e5192ddcc6 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 07:51:42 -0700 Subject: [PATCH 06/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/coreclr/tools/Common/JitInterface/WasmLowering.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index f9e66cc55d7045..815c604ed9967b 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -104,10 +104,8 @@ public static TypeDesc LowerToAbiType(TypeDesc type) /// private static bool IsWasmV128Type(TypeDesc type) { - return type is MetadataType metadataType && - metadataType.IsIntrinsic && - metadataType.Namespace == "System.Runtime.Intrinsics"u8 && - metadataType.Name == "Vector128`1"u8; + return type.IsIntrinsic && + Internal.TypeSystem.Interop.InteropTypes.IsSystemRuntimeIntrinsicsVector128T(type.Context, type); } public static WasmValueType LowerType(TypeDesc type) From 3027e17015e7668ee75de3fbc99ecdcb692f47d6 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 08:35:03 -0700 Subject: [PATCH 07/12] Assert the wasm v128 valtype in the compiled SIMD signatures Decode each method's wasm function signature and verify the Vector128 parameter (and return, where applicable) materializes as v128 (0x7B) rather than a by-ref i32, so a regression to the generic struct ABI is caught. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index a6f3f59b9d9706..87ecbabb787de9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection.PortableExecutable; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; using ILCompiler.Reflection.ReadyToRun; @@ -130,12 +131,45 @@ static void Validate(ReadyToRunReader reader) Assert.Equal(WasmMachine.Wasm32, reader.Machine); List methods = R2RAssert.GetAllMethods(reader); - foreach (string name in new[] { "Echo", "ThroughLocal", "Store", "CallEcho" }) + + // Each method's compiled body must actually use the wasm v128 (0x7B) valtype for its + // Vector128 parameter, and for its return when it returns one. A regression that + // reverts to the by-ref i32 ABI would produce no v128 in the signature at all. + const byte WasmV128 = 0x7B; + + // (method name, expects v128 return). All four take a Vector128 by value; Store + // returns void (its 'ref Vector128' destination is an i32 pointer). + foreach ((string name, bool expectsV128Return) in + new[] { ("Echo", true), ("ThroughLocal", true), ("Store", false), ("CallEcho", true) }) { + ReadyToRunMethod method = Assert.Single( + methods, m => m.SignatureString.Contains($".{name}(", StringComparison.Ordinal)); + + WebcilImageReader.WasmFunctionInfo body = ResolveWasmBody(reader, webcilReader, method); + + Assert.True( + body.ParamTypes.Contains(WasmV128), + $"'{name}' should take a wasm v128 parameter; params were {Format(body.ParamTypes)}."); + Assert.True( - methods.Exists(method => method.SignatureString.Contains($".{name}(", StringComparison.Ordinal)), - $"Expected compiled method '{name}' in the wasm SIMD module."); + body.ResultTypes.Contains(WasmV128) == expectsV128Return, + $"'{name}' v128 return expectation was {expectsV128Return}; results were {Format(body.ResultTypes)}."); } + + static string Format(IReadOnlyList valTypes) => + $"[{string.Join(",", valTypes.Select(b => $"0x{b:X2}"))}]"; + } + + static WebcilImageReader.WasmFunctionInfo ResolveWasmBody( + ReadyToRunReader reader, WebcilImageReader webcilReader, ReadyToRunMethod method) + { + uint tableIndex = checked(reader.WasmMinFunctionTableIndex + (uint)method.EntryPointRuntimeFunctionId); + int functionIndex = webcilReader.GetFunctionIndexFromTableIndex(tableIndex); + Assert.True(functionIndex >= 0, $"Could not resolve wasm table index {tableIndex} to a function body."); + + WebcilImageReader.WasmFunctionInfo? body = webcilReader.GetWasmFunctionBody(functionIndex); + Assert.True(body is not null, $"Wasm function body {functionIndex} was not found."); + return body.Value; } } From 60f2b84a1344f0600a2d80a9aec7cad1c45c80a4 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 10:46:10 -0700 Subject: [PATCH 08/12] Only treat Vector128 as wasm v128 for supported base types The shared-generic Vector128<__Canon> instantiation was classified as the wasm v128 ABI primitive even though the JIT only types Vector128 as TYP_SIMD16 when T is a primitive numeric base type, so crossgen crashed materializing an i32/byref as a v128. Gate the classification on a shared IsSupportedVectorBaseType helper reused by the existing ARM64 vector-aggregate checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Common/Compiler/VectorFieldLayoutAlgorithm.cs | 11 ++++++++++- src/coreclr/tools/Common/JitInterface/WasmLowering.cs | 10 ++++++---- .../Compiler/VectorOfTFieldLayoutAlgorithm.cs | 2 +- .../Compiler/ReadyToRunCompilerContext.cs | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs b/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs index 74481988dadf25..21226021f29657 100644 --- a/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs +++ b/src/coreclr/tools/Common/Compiler/VectorFieldLayoutAlgorithm.cs @@ -147,7 +147,7 @@ public override bool ComputeIsUnsafeValueType(DefType type) public override ValueTypeShapeCharacteristics ComputeValueTypeShapeCharacteristics(DefType type) { if (type.Context.Target.Architecture == TargetArchitecture.ARM64 && - type.Instantiation[0].IsPrimitiveNumeric) + IsSupportedVectorBaseType(type.Instantiation[0])) { return type.InstanceFieldSize.AsInt switch { @@ -170,5 +170,14 @@ public static bool IsVectorType(DefType type) type.Name == "Vector256`1"u8 || type.Name == "Vector512`1"u8); } + + /// + /// Determines whether is supported as the base (element) + /// type of an intrinsic vector, mirroring the set the JIT recognizes as a SIMD base type. + /// + public static bool IsSupportedVectorBaseType(TypeDesc elementType) + { + return elementType.IsPrimitiveNumeric; + } } } diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 815c604ed9967b..74d52e80a7d37d 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -98,14 +98,16 @@ public static TypeDesc LowerToAbiType(TypeDesc type) /// /// Determines whether a type is , the /// only SIMD vector type currently passed and returned by value as a wasm v128. The - /// JIT recognizes Vector128<T> as TYP_SIMD16 on wasm; the other SIMD - /// types (Vector2/3/4, Vector64/256/512<T>, Vector<T>, ...) are not yet handled - /// by the wasm calling convention and continue to use the generic struct ABI. + /// JIT recognizes Vector128<T> as TYP_SIMD16 on wasm only when T is + /// a primitive numeric type; other SIMD types (Vector2/3/4, Vector64/256/512<T>, + /// Vector<T>, ...) and non-primitive instantiations (e.g. the shared __Canon form) + /// are not handled by the wasm calling convention and continue to use the generic struct ABI. /// private static bool IsWasmV128Type(TypeDesc type) { return type.IsIntrinsic && - Internal.TypeSystem.Interop.InteropTypes.IsSystemRuntimeIntrinsicsVector128T(type.Context, type); + Internal.TypeSystem.Interop.InteropTypes.IsSystemRuntimeIntrinsicsVector128T(type.Context, type) && + VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0]); } public static WasmValueType LowerType(TypeDesc type) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs index 0bb36a84800e56..1a6f8ae2cd8df4 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/VectorOfTFieldLayoutAlgorithm.cs @@ -78,7 +78,7 @@ public override bool ComputeContainsByRefs(DefType type) public override ValueTypeShapeCharacteristics ComputeValueTypeShapeCharacteristics(DefType type) { if (type.Context.Target.Architecture == TargetArchitecture.ARM64 && - type.Instantiation[0].IsPrimitiveNumeric) + VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0])) { return type.InstanceFieldSize.AsInt switch { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs index 568488eda1f9ec..edf32f30df371a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs @@ -329,7 +329,7 @@ public override ComputedStaticFieldLayout ComputeStaticFieldLayout(DefType type, public override ValueTypeShapeCharacteristics ComputeValueTypeShapeCharacteristics(DefType type) { if (type.Context.Target.Architecture == TargetArchitecture.ARM64 && - type.Instantiation[0].IsPrimitiveNumeric) + VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0])) { return type.InstanceFieldSize.AsInt switch { From 021d60d39473071e5b2401094d260fa52181424b Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 16 Jul 2026 11:59:34 -0700 Subject: [PATCH 09/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 87ecbabb787de9..e53967c39189c7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -147,10 +147,9 @@ static void Validate(ReadyToRunReader reader) WebcilImageReader.WasmFunctionInfo body = ResolveWasmBody(reader, webcilReader, method); - Assert.True( - body.ParamTypes.Contains(WasmV128), - $"'{name}' should take a wasm v128 parameter; params were {Format(body.ParamTypes)}."); - +Assert.True( + body.ParamTypes.Count(b => b == WasmV128) == 1, + $"'{name}' should have exactly one wasm v128 parameter; params were {Format(body.ParamTypes)}."); Assert.True( body.ResultTypes.Contains(WasmV128) == expectsV128Return, $"'{name}' v128 return expectation was {expectsV128Return}; results were {Format(body.ResultTypes)}."); From ed1cdbc29b56935ecad164a875c4c296c6b79b0a Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 09:50:08 -0700 Subject: [PATCH 10/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 01f8ae2d0e4832..5cc9213d151d40 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -170,9 +170,9 @@ static void Validate(ReadyToRunReader reader) WebcilImageReader.WasmFunctionInfo body = ResolveWasmBody(reader, webcilReader, method); -Assert.True( - body.ParamTypes.Count(b => b == WasmV128) == 1, - $"'{name}' should have exactly one wasm v128 parameter; params were {Format(body.ParamTypes)}."); + Assert.True( + body.ParamTypes.Count(b => b == WasmV128) == 1, + $"'{name}' should have exactly one wasm v128 parameter; params were {Format(body.ParamTypes)}."); Assert.True( body.ResultTypes.Contains(WasmV128) == expectsV128Return, $"'{name}' v128 return expectation was {expectsV128Return}; results were {Format(body.ResultTypes)}."); From ed1f8467a216cf05f8bf3293adb2d5566e6229a9 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 09:59:59 -0700 Subject: [PATCH 11/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/coreclr/tools/Common/JitInterface/WasmLowering.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 74d52e80a7d37d..2ef5b66dcdf030 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -107,6 +107,7 @@ private static bool IsWasmV128Type(TypeDesc type) { return type.IsIntrinsic && Internal.TypeSystem.Interop.InteropTypes.IsSystemRuntimeIntrinsicsVector128T(type.Context, type) && + type.Instantiation.Length == 1 && VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0]); } From 75e139dd8059c751e13b4676d8cbb55fecb62312 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 21 Jul 2026 12:59:30 -0700 Subject: [PATCH 12/12] Handle Vector and single-field v128 wrapper structs in the wasm ABI A 128-bit Vector uses the same v128 ABI as Vector128, and a single-field struct wrapping either lowers to the v128 primitive, matching emscripten's isSingleElementStruct handling. Multi-field aggregates keep the generic by-ref ABI since the wasm C ABI has no HFA/HVA concept. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tools/Common/JitInterface/WasmLowering.cs | 51 ++++++++++++++----- .../TestCases/R2RTestSuites.cs | 11 +++- .../TestCases/Webcil/WasmSimdModule.cs | 50 ++++++++++++++++++ 3 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 2ef5b66dcdf030..58447a357d4151 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -38,9 +38,7 @@ public static MethodSignature GetStringCtorActualSignature(MethodSignature signa public static TypeDesc LowerToAbiType(TypeDesc type) { - // Vector128 is the only SIMD type passed by value as a wasm v128. The other SIMD - // types (Vector2/3/4, Vector64, Vector, ...) are not yet handled by the wasm - // calling convention and fall through to the generic struct lowering below. + // Vector128 and a 128-bit Vector are wasm v128 ABI primitives passed by value. if (IsWasmV128Type(type)) { return type; @@ -73,6 +71,10 @@ public static TypeDesc LowerToAbiType(TypeDesc type) if (numIntroducedFields != 1) { + // Multi-field aggregates (including a homogeneous 2x v128) use the generic by-ref + // struct ABI; the wasm C ABI has no HFA/HVA concept. Only emscripten's opt-in + // experimental multivalue ABI expands these into per-field registers, which we + // don't target. return null; } @@ -86,6 +88,13 @@ public static TypeDesc LowerToAbiType(TypeDesc type) type = firstFieldElementType; + // A single-field wrapper struct around a v128 lowers to the v128 primitive, matching + // emscripten, which passes a struct wrapping a v128 as a v128. + if (IsWasmV128Type(type)) + { + return type; + } + if (type.IsValueType && !type.IsPrimitive) { continue; @@ -96,19 +105,35 @@ public static TypeDesc LowerToAbiType(TypeDesc type) } /// - /// Determines whether a type is , the - /// only SIMD vector type currently passed and returned by value as a wasm v128. The - /// JIT recognizes Vector128<T> as TYP_SIMD16 on wasm only when T is - /// a primitive numeric type; other SIMD types (Vector2/3/4, Vector64/256/512<T>, - /// Vector<T>, ...) and non-primitive instantiations (e.g. the shared __Canon form) - /// are not handled by the wasm calling convention and continue to use the generic struct ABI. + /// Determines whether a type is passed and returned by value as a wasm v128, matching + /// the SIMD types the JIT recognizes as TYP_SIMD16 on wasm. This is + /// and a 128-bit + /// , in both cases only when T is a supported + /// primitive numeric base type. Other SIMD types (Vector2/3/4, Vector64/256/512<T>, ...) + /// and non-primitive instantiations (e.g. the shared __Canon form) are not ABI + /// primitives and continue to use the generic struct ABI. /// private static bool IsWasmV128Type(TypeDesc type) { - return type.IsIntrinsic && - Internal.TypeSystem.Interop.InteropTypes.IsSystemRuntimeIntrinsicsVector128T(type.Context, type) && - type.Instantiation.Length == 1 && - VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0]); + if (!type.IsIntrinsic || + type.Instantiation.Length != 1 || + !VectorFieldLayoutAlgorithm.IsSupportedVectorBaseType(type.Instantiation[0])) + { + return false; + } + + // Vector128 is always a 16-byte v128. + if (Internal.TypeSystem.Interop.InteropTypes.IsSystemRuntimeIntrinsicsVector128T(type.Context, type)) + { + return true; + } + + // Vector is target-sized, so it is only a v128 when the target's maximum SIMD width is + // 128-bit (i.e. it is exactly 16 bytes). This matches the JIT recognizing it as TYP_SIMD16 + // via getVectorTByteLength() and keeps the ABI correct should wasm later gain wider vectors. + return type is DefType vectorOfT && + VectorOfTFieldLayoutAlgorithm.IsVectorOfTType(vectorOfT) && + type.GetElementSize().AsInt == 16; } public static WasmValueType LowerType(TypeDesc type) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 5cc9213d151d40..09e98fcc6a088b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -160,10 +160,17 @@ static void Validate(ReadyToRunReader reader) // reverts to the by-ref i32 ABI would produce no v128 in the signature at all. const byte WasmV128 = 0x7B; - // (method name, expects v128 return). All four take a Vector128 by value; Store + // (method name, expects v128 return). All take a v128-classified value by value (a + // Vector128, a 128-bit Vector, or a single-field struct wrapping one); Store // returns void (its 'ref Vector128' destination is an i32 pointer). foreach ((string name, bool expectsV128Return) in - new[] { ("Echo", true), ("ThroughLocal", true), ("Store", false), ("CallEcho", true) }) + new[] + { + ("Echo", true), ("ThroughLocal", true), ("Store", false), ("CallEcho", true), + ("EchoVectorT", true), ("CallEchoVectorT", true), + ("EchoWrapped", true), ("CallEchoWrapped", true), + ("EchoWrappedVectorT", true), ("CallEchoWrappedVectorT", true), + }) { ReadyToRunMethod method = Assert.Single( methods, m => m.SignatureString.Contains($".{name}(", StringComparison.Ordinal)); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs index 4fbd65164aa100..77518d9ee5336d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmSimdModule.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; @@ -35,4 +36,53 @@ public static Vector128 CallEcho(Vector128 value) { return Echo(value); } + + // Vector is 16 bytes on wasm (128-bit vectors), so it uses the same v128 ABI as Vector128. + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector EchoVectorT(Vector value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector CallEchoVectorT(Vector value) + { + return EchoVectorT(value); + } + + // A single-field struct wrapping a v128 is itself passed/returned as a v128, matching emscripten. + public struct WrappedVector128 + { + public Vector128 Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVector128 EchoWrapped(WrappedVector128 value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVector128 CallEchoWrapped(WrappedVector128 value) + { + return EchoWrapped(value); + } + + // The same unwrapping applies to a struct wrapping a 128-bit Vector. + public struct WrappedVectorT + { + public Vector Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVectorT EchoWrappedVectorT(WrappedVectorT value) + { + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static WrappedVectorT CallEchoWrappedVectorT(WrappedVectorT value) + { + return EchoWrappedVectorT(value); + } }