diff --git a/internal/engine/compiler/compiler.go b/internal/engine/compiler/compiler.go index 550e5efa..9cdf8b29 100644 --- a/internal/engine/compiler/compiler.go +++ b/internal/engine/compiler/compiler.go @@ -43,7 +43,7 @@ type compiler interface { // compileDrop adds instructions to perform wazeroir.OperationDrop. compileDrop(o *wazeroir.OperationDrop) error // compileSelect adds instructions to perform wazeroir.OperationSelect. - compileSelect() error + compileSelect(o *wazeroir.OperationSelect) error // compilePick adds instructions to perform wazeroir.OperationPick. compilePick(o *wazeroir.OperationPick) error // compileAdd adds instructions to perform wazeroir.OperationAdd. diff --git a/internal/engine/compiler/compiler_stack_test.go b/internal/engine/compiler/compiler_stack_test.go index 2fb2eb3d..261e1cb8 100644 --- a/internal/engine/compiler/compiler_stack_test.go +++ b/internal/engine/compiler/compiler_stack_test.go @@ -569,7 +569,7 @@ func TestCompiler_compileSelect(t *testing.T) { } // Now emit code for select. - err = compiler.compileSelect() + err = compiler.compileSelect(&wazeroir.OperationSelect{}) require.NoError(t, err) // x1 should be top of the stack. diff --git a/internal/engine/compiler/compiler_vec_test.go b/internal/engine/compiler/compiler_vec_test.go index e05a75cc..3ce70c6c 100644 --- a/internal/engine/compiler/compiler_vec_test.go +++ b/internal/engine/compiler/compiler_vec_test.go @@ -7280,3 +7280,58 @@ func TestCompiler_compileV128ITruncSatFromF(t *testing.T) { }) } } + +// TestCompiler_compileSelect_v128 is for select instructions on vector values. +func TestCompiler_compileSelect_v128(t *testing.T) { + const x1Lo, x1Hi = uint64(0x1), uint64(0x2) + const x2Lo, x2Hi = uint64(0x3), uint64(0x4) + + for _, selector := range []uint32{0, 1} { + env := newCompilerEnvironment() + compiler := env.requireNewCompiler(t, newCompiler, + &wazeroir.CompilationResult{HasMemory: true, Signature: &wasm.FunctionType{}}) + + err := compiler.compilePreamble() + require.NoError(t, err) + + err = compiler.compileV128Const(&wazeroir.OperationV128Const{ + Lo: x1Lo, + Hi: x1Hi, + }) + require.NoError(t, err) + + err = compiler.compileV128Const(&wazeroir.OperationV128Const{ + Lo: x2Lo, + Hi: x2Hi, + }) + require.NoError(t, err) + + err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: selector}) + require.NoError(t, err) + + err = compiler.compileSelect(&wazeroir.OperationSelect{IsTargetVector: true}) + require.NoError(t, err) + + require.Equal(t, uint64(2), compiler.runtimeValueLocationStack().sp) + require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters)) + + err = compiler.compileReturnFunction() + require.NoError(t, err) + + // Generate and run the code under test. + code, _, err := compiler.compile() + require.NoError(t, err) + env.exec(code) + + require.Equal(t, nativeCallStatusCodeReturned, env.callEngine().statusCode) + + lo, hi := env.stackTopAsV128() + if selector == 0 { + require.Equal(t, x2Lo, lo) + require.Equal(t, x2Hi, hi) + } else { + require.Equal(t, x1Lo, lo) + require.Equal(t, x1Hi, hi) + } + } +} diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go index 11481c5a..c06a89b0 100644 --- a/internal/engine/compiler/engine.go +++ b/internal/engine/compiler/engine.go @@ -884,7 +884,7 @@ func compileWasmFunction(_ wasm.Features, ir *wazeroir.CompilationResult) (*code case *wazeroir.OperationDrop: err = compiler.compileDrop(o) case *wazeroir.OperationSelect: - err = compiler.compileSelect() + err = compiler.compileSelect(o) case *wazeroir.OperationPick: err = compiler.compilePick(o) case *wazeroir.OperationSwap: diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go index 0369a2cb..ab8de745 100644 --- a/internal/engine/compiler/impl_amd64.go +++ b/internal/engine/compiler/impl_amd64.go @@ -871,16 +871,53 @@ func (c *amd64Compiler) emitDropRange(r *wazeroir.InclusiveRange) error { return nil } +// compileSelectV128Impl implements compileSelect for vector values. +func (c *amd64Compiler) compileSelectV128Impl(selectorReg asm.Register) error { + x2 := c.locationStack.popV128() + if err := c.compileEnsureOnRegister(x2); err != nil { + return err + } + + x1 := c.locationStack.popV128() + if err := c.compileEnsureOnRegister(x1); err != nil { + return err + } + + // Compare the conditional value with zero. + c.assembler.CompileRegisterToConst(amd64.CMPQ, selectorReg, 0) + + // Set the jump if the top value is not zero. + jmpIfNotZero := c.assembler.CompileJump(amd64.JNE) + + // In this branch, we select the value of x2, so we move the value into x1.register so that + // we can have the result in x1.register regardless of the selection. + c.assembler.CompileRegisterToRegister(amd64.MOVDQU, x2.register, x1.register) + + // Else, we don't need to adjust value, just need to jump to the next instruction. + c.assembler.SetJumpTargetOnNext(jmpIfNotZero) + + // As noted, the result exists in x1.register regardless of the selector. + c.pushVectorRuntimeValueLocationOnRegister(x1.register) + // Plus, x2.register is no longer used. + c.locationStack.markRegisterUnused(x2.register) + c.locationStack.markRegisterUnused(selectorReg) + return nil +} + // compileSelect implements compiler.compileSelect for the amd64 architecture. // // The emitted native code depends on whether the values are on // the physical registers or memory stack, or maybe conditional register. -func (c *amd64Compiler) compileSelect() error { +func (c *amd64Compiler) compileSelect(o *wazeroir.OperationSelect) error { cv := c.locationStack.pop() if err := c.compileEnsureOnRegister(cv); err != nil { return err } + if o.IsTargetVector { + return c.compileSelectV128Impl(cv.register) + } + x2 := c.locationStack.pop() // We do not consume x1 here, but modify the value according to // the conditional value "c" above. diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go index 587f7ad4..e5c7956b 100644 --- a/internal/engine/compiler/impl_arm64.go +++ b/internal/engine/compiler/impl_arm64.go @@ -1286,13 +1286,45 @@ func (c *arm64Compiler) compileDropRange(r *wazeroir.InclusiveRange) error { return nil } +func (c *arm64Compiler) compileSelectV128Impl(selectorRegister asm.Register) error { + x2 := c.locationStack.popV128() + if err := c.compileEnsureOnRegister(x2); err != nil { + return err + } + + x1 := c.locationStack.popV128() + if err := c.compileEnsureOnRegister(x1); err != nil { + return err + } + + c.assembler.CompileTwoRegistersToNone(arm64.CMPW, arm64.RegRZR, selectorRegister) + brIfNotZero := c.assembler.CompileJump(arm64.BCONDNE) + + // In this branch, we select the value of x2, so we move the value into x1.register so that + // we can have the result in x1.register regardless of the selection. + c.assembler.CompileTwoVectorRegistersToVectorRegister(arm64.VORR, + x2.register, x2.register, x1.register, arm64.VectorArrangement16B) + + c.assembler.SetJumpTargetOnNext(brIfNotZero) + + // As noted, the result exists in x1.register regardless of the selector. + c.pushVectorRuntimeValueLocationOnRegister(x1.register) + // Plus, x2.register is no longer used. + c.markRegisterUnused(x2.register) + return nil +} + // compileSelect implements compiler.compileSelect for the arm64 architecture. -func (c *arm64Compiler) compileSelect() error { +func (c *arm64Compiler) compileSelect(o *wazeroir.OperationSelect) error { cv, err := c.popValueOnRegister() if err != nil { return err } + if o.IsTargetVector { + return c.compileSelectV128Impl(cv.register) + } + c.markRegisterUsed(cv.register) x1, x2, err := c.popTwoValuesOnRegisters() diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go index 8d447602..80b45978 100644 --- a/internal/engine/interpreter/interpreter.go +++ b/internal/engine/interpreter/interpreter.go @@ -388,6 +388,7 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) { op.rs = make([]*wazeroir.InclusiveRange, 1) op.rs[0] = o.Depth case *wazeroir.OperationSelect: + op.b3 = o.IsTargetVector case *wazeroir.OperationPick: op.us = make([]uint64, 1) op.us[0] = uint64(o.Depth) @@ -899,10 +900,19 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont frame.pc++ case wazeroir.OperationKindSelect: c := ce.popValue() - v2 := ce.popValue() - if c == 0 { - _ = ce.popValue() - ce.pushValue(v2) + if op.b3 { // Target is vector. + x2Hi, x2Lo := ce.popValue(), ce.popValue() + if c == 0 { + _, _ = ce.popValue(), ce.popValue() // discard the x1's lo and hi bits. + ce.pushValue(x2Lo) + ce.pushValue(x2Hi) + } + } else { + v2 := ce.popValue() + if c == 0 { + _ = ce.popValue() + ce.pushValue(v2) + } } frame.pc++ case wazeroir.OperationKindPick: diff --git a/internal/integration_test/fuzzcases/fuzzcases_test.go b/internal/integration_test/fuzzcases/fuzzcases_test.go index 87c95015..ab6b7737 100644 --- a/internal/integration_test/fuzzcases/fuzzcases_test.go +++ b/internal/integration_test/fuzzcases/fuzzcases_test.go @@ -15,6 +15,8 @@ var ctx = context.Background() var ( //go:embed testdata/695.wasm case695 []byte + //go:embed testdata/696.wasm + case696 []byte ) func newRuntimeCompiler() wazero.Runtime { @@ -52,3 +54,36 @@ func Test695(t *testing.T) { }) } } + +func Test696(t *testing.T) { + if !platform.CompilerSupported() { + return + } + + functionNames := [4]string{ + "select with 0 / after calling dummy", + "select with 0", + "typed select with 1 / after calling dummy", + "typed select with 1", + } + + for _, tc := range []struct { + name string + r wazero.Runtime + }{ + {name: "compiler", r: newRuntimeCompiler()}, + {name: "interpreter", r: newRuntimeInterpreter()}, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + defer tc.r.Close(ctx) + module, err := tc.r.InstantiateModuleFromBinary(ctx, case696) + require.NoError(t, err) + + for _, name := range functionNames { + _, err := module.ExportedFunction(name).Call(ctx) + require.NoError(t, err) + } + }) + } +} diff --git a/internal/integration_test/fuzzcases/testdata/696.wasm b/internal/integration_test/fuzzcases/testdata/696.wasm new file mode 100644 index 00000000..e0eb7547 Binary files /dev/null and b/internal/integration_test/fuzzcases/testdata/696.wasm differ diff --git a/internal/integration_test/fuzzcases/testdata/696.wat b/internal/integration_test/fuzzcases/testdata/696.wat new file mode 100644 index 00000000..4aae7535 --- /dev/null +++ b/internal/integration_test/fuzzcases/testdata/696.wat @@ -0,0 +1,64 @@ +(module + (func $dummy) + (func (export "select with 0 / after calling dummy") + v128.const i64x2 0xffffffffffffffff 0xffffffffffffffff + v128.const i64x2 0xeeeeeeeeeeeeeeee 0xeeeeeeeeeeeeeeee + i32.const 0 ;; choose 0xeeeeeeeeeeeeeeee lane. + call 0 ;; calling dummy function before select to + select + ;; check the equality. + i64x2.extract_lane 0 + i64.const 0xeeeeeeeeeeeeeeee + i64.eq + (if + (then) + (else unreachable) + ) + ) + + (func (export "select with 0") + v128.const i64x2 0xffffffffffffffff 0xffffffffffffffff + v128.const i64x2 0xeeeeeeeeeeeeeeee 0xeeeeeeeeeeeeeeee + i32.const 0 ;; choose 0xeeeeeeeeeeeeeeee lane. + select + ;; check the equality. + i64x2.extract_lane 0 + i64.const 0xeeeeeeeeeeeeeeee + i64.eq + (if + (then) + (else unreachable) + ) + ) + + (func (export "typed select with 1 / after calling dummy") + v128.const i64x2 0xffffffffffffffff 0xffffffffffffffff + v128.const i64x2 0xeeeeeeeeeeeeeeee 0xeeeeeeeeeeeeeeee + i32.const 1 ;; choose 0xffffffffffffffff lane. + call 0 ;; calling dummy function before select to + select (result v128) + ;; check the equality. + i64x2.extract_lane 0 + i64.const 0xffffffffffffffff + i64.eq + (if + (then) + (else unreachable) + ) + ) + + (func (export "typed select with 1") + v128.const i64x2 0xffffffffffffffff 0xffffffffffffffff + v128.const i64x2 0xeeeeeeeeeeeeeeee 0xeeeeeeeeeeeeeeee + i32.const 1 ;; choose 0xffffffffffffffff lane. + select (result v128) + ;; check the equality. + i64x2.extract_lane 0 + i64.const 0xffffffffffffffff + i64.eq + (if + (then) + (else unreachable) + ) + ) +) diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index 013f5096..60616d56 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -1525,7 +1525,7 @@ func (m *Module) validateFunctionWithMaxStackValues( pc++ tp := body[pc] if tp != ValueTypeI32 && tp != ValueTypeI64 && tp != ValueTypeF32 && tp != ValueTypeF64 && - tp != api.ValueTypeExternref && tp != ValueTypeFuncref { + tp != api.ValueTypeExternref && tp != ValueTypeFuncref && tp != ValueTypeV128 { return fmt.Errorf("invalid type %s for %s", ValueTypeName(tp), OpcodeTypedSelectName) } } else if isReferenceValueType(v1) || isReferenceValueType(v2) { diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go index a3a426f9..3be1e21b 100644 --- a/internal/wazeroir/compiler.go +++ b/internal/wazeroir/compiler.go @@ -754,15 +754,23 @@ operatorSwitch: &OperationDrop{Depth: r}, ) case wasm.OpcodeSelect: + // If it is on the unreachable state, ignore the instruction. + if c.unreachableState.on { + break operatorSwitch + } c.emit( - &OperationSelect{}, + &OperationSelect{IsTargetVector: c.stackPeek() == UnsignedTypeV128}, ) case wasm.OpcodeTypedSelect: // Skips two bytes: vector size fixed to 1, and the value type for select. c.pc += 2 + // If it is on the unreachable state, ignore the instruction. + if c.unreachableState.on { + break operatorSwitch + } // Typed select is semantically equivalent to select at runtime. c.emit( - &OperationSelect{}, + &OperationSelect{IsTargetVector: c.stackPeek() == UnsignedTypeV128}, ) case wasm.OpcodeLocalGet: if index == nil { diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go index 3fda7c57..dbd974d8 100644 --- a/internal/wazeroir/compiler_test.go +++ b/internal/wazeroir/compiler_test.go @@ -2562,3 +2562,73 @@ func TestCompile_drop_vectors(t *testing.T) { }) } } + +func TestCompile_select_vectors(t *testing.T) { + tests := []struct { + name string + mod *wasm.Module + expected []Operation + }{ + { + name: "non typed", + mod: &wasm.Module{ + TypeSection: []*wasm.FunctionType{{}}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{ + wasm.OpcodeVecPrefix, + wasm.OpcodeVecV128Const, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + wasm.OpcodeVecPrefix, + wasm.OpcodeVecV128Const, 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeSelect, + wasm.OpcodeDrop, + wasm.OpcodeEnd, + }}}, + FunctionDefinitionSection: []*wasm.FunctionDefinition{{}}, + }, + expected: []Operation{ + &OperationV128Const{Lo: 0x1, Hi: 0x2}, + &OperationV128Const{Lo: 0x3, Hi: 0x4}, + &OperationConstI32{Value: 0}, + &OperationSelect{IsTargetVector: true}, + &OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}}, + &OperationBr{Target: &BranchTarget{}}, + }, + }, + { + name: "typed", + mod: &wasm.Module{ + TypeSection: []*wasm.FunctionType{{}}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{ + wasm.OpcodeVecPrefix, + wasm.OpcodeVecV128Const, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + wasm.OpcodeVecPrefix, + wasm.OpcodeVecV128Const, 3, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeTypedSelect, 0x1, wasm.ValueTypeV128, + wasm.OpcodeDrop, + wasm.OpcodeEnd, + }}}, + FunctionDefinitionSection: []*wasm.FunctionDefinition{{}}, + }, + expected: []Operation{ + &OperationV128Const{Lo: 0x1, Hi: 0x2}, + &OperationV128Const{Lo: 0x3, Hi: 0x4}, + &OperationConstI32{Value: 0}, + &OperationSelect{IsTargetVector: true}, + &OperationDrop{Depth: &InclusiveRange{Start: 0, End: 1}}, + &OperationBr{Target: &BranchTarget{}}, + }, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + res, err := CompileFunctions(ctx, wasm.Features20220419, tc.mod) + require.NoError(t, err) + require.Equal(t, tc.expected, res[0].Operations) + }) + } +} diff --git a/internal/wazeroir/operations.go b/internal/wazeroir/operations.go index c7603c22..e1c3f4e7 100644 --- a/internal/wazeroir/operations.go +++ b/internal/wazeroir/operations.go @@ -931,7 +931,10 @@ func (*OperationDrop) Kind() OperationKind { // // The engines are expected to pop three values, say [..., x2, x1, c], then if the value "c" equals zero, // "x1" is pushed back onto the stack and, otherwise "x2" is pushed back. -type OperationSelect struct{} +type OperationSelect struct { + // IsTargetVector true if the selection target value's type is wasm.ValueTypeV128. + IsTargetVector bool +} // Kind implements Operation.Kind func (*OperationSelect) Kind() OperationKind {