From c6426160c2bcafacfd0cf9dff39bd0c5a3fbb75a Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Mon, 7 Mar 2022 15:08:57 +0900 Subject: [PATCH] Opt-in support for sign-extend (#339) Allows users to do the following to enable sign-extension-ops: ``` r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithFeatureSignExtensionOps(true)) ``` Resolves #66 Signed-off-by: Takeshi Yoneda Co-authored-by: Adrian Cole --- config.go | 9 ++ config_test.go | 8 ++ internal/wasm/func_validation.go | 18 ++- internal/wasm/func_validation_test.go | 46 +++--- internal/wasm/instruction.go | 18 ++- internal/wasm/interpreter/interpreter.go | 33 +++++ internal/wasm/interpreter/interpreter_test.go | 113 +++++++++++++++ internal/wasm/jit/compiler.go | 15 ++ internal/wasm/jit/engine.go | 10 ++ internal/wasm/jit/jit_amd64.go | 25 ++++ internal/wasm/jit/jit_amd64_test.go | 136 ++++++++++++++++++ internal/wasm/jit/jit_arm64.go | 25 ++++ internal/wasm/jit/jit_arm64_test.go | 134 +++++++++++++++++ internal/wasm/text/decoder_test.go | 42 +++++- internal/wasm/text/func_parser.go | 23 +++ internal/wasm/text/func_parser_test.go | 75 ++++++++++ internal/wasm/text/type_parser_test.go | 1 + internal/wazeroir/compiler.go | 20 +++ internal/wazeroir/operations.go | 35 +++++ internal/wazeroir/signature.go | 4 + tests/post1_0/post1_0_test.go | 128 ++++++++++++++++- vs/codec_test.go | 20 +-- vs/testdata/example.wat | 3 + 23 files changed, 907 insertions(+), 34 deletions(-) diff --git a/config.go b/config.go index aa2a0166..833f1779 100644 --- a/config.go +++ b/config.go @@ -62,6 +62,15 @@ func (r *RuntimeConfig) WithFeatureMutableGlobal(enabled bool) *RuntimeConfig { return &RuntimeConfig{engine: r.engine, ctx: r.ctx, enabledFeatures: enabledFeatures} } +// WithFeatureSignExtensionOps enables sign-extend operations. This defaults to false as the feature was not finished in +// WebAssembly 1.0 (20191205). +// +// See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md +func (r *RuntimeConfig) WithFeatureSignExtensionOps(enabled bool) *RuntimeConfig { + enabledFeatures := r.enabledFeatures.Set(internalwasm.FeatureSignExtensionOps, enabled) + return &RuntimeConfig{engine: r.engine, ctx: r.ctx, enabledFeatures: enabledFeatures} +} + // DecodedModule is a WebAssembly 1.0 (20191205) text or binary encoded module to instantiate. type DecodedModule struct { name string diff --git a/config_test.go b/config_test.go index 93b15cc9..b3206363 100644 --- a/config_test.go +++ b/config_test.go @@ -23,6 +23,14 @@ func TestRuntimeConfig_Features(t *testing.T) { return c.WithFeatureMutableGlobal(v) }, }, + { + name: "sign-extension-ops", + feature: internalwasm.FeatureSignExtensionOps, + expectDefault: false, + setFeature: func(c *RuntimeConfig, v bool) *RuntimeConfig { + return c.WithFeatureSignExtensionOps(v) + }, + }, } for _, tt := range tests { diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index 6aa1ec11..e3dfe154 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -698,8 +698,22 @@ func validateFunction( return fmt.Errorf("cannot pop the operand for f64.reinterpret_i64: %v", err) } valueTypeStack.push(ValueTypeF64) - case OpcodeI32Extend8S, OpcodeI32Extend16S, OpcodeI64Extend8S, OpcodeI64Extend16S, OpcodeI64Extend32S: - return fmt.Errorf("%s invalid as %s is not yet supported. See #66", InstructionName(op), FeatureSignExtensionOps) + case OpcodeI32Extend8S, OpcodeI32Extend16S: + if err := features.Require(FeatureSignExtensionOps); err != nil { + return fmt.Errorf("%s invalid as %v", instructionNames[op], err) + } + if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil { + return fmt.Errorf("cannot pop the operand for %s: %v", instructionNames[op], err) + } + valueTypeStack.push(ValueTypeI32) + case OpcodeI64Extend8S, OpcodeI64Extend16S, OpcodeI64Extend32S: + if err := features.Require(FeatureSignExtensionOps); err != nil { + return fmt.Errorf("%s invalid as %v", instructionNames[op], err) + } + if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil { + return fmt.Errorf("cannot pop the operand for %s: %v", instructionNames[op], err) + } + valueTypeStack.push(ValueTypeI64) default: return fmt.Errorf("invalid numeric instruction 0x%x", op) } diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go index 9dc37d0d..008c2032 100644 --- a/internal/wasm/func_validation_test.go +++ b/internal/wasm/func_validation_test.go @@ -38,39 +38,53 @@ func TestValidateFunction_valueStackLimit(t *testing.T) { } func TestValidateFunction_SignExtensionOps(t *testing.T) { - // TODO: actually support, guarded by FeatureSignExtensionOps flag which defaults to false #66 + const maxStackHeight = 100 // arbitrary tests := []struct { - input Opcode - expectedErr string + input Opcode + expectedErrOnDisable string }{ { - input: OpcodeI32Extend8S, - expectedErr: "i32.extend8_s invalid as sign-extension-ops is not yet supported. See #66", + input: OpcodeI32Extend8S, + expectedErrOnDisable: "i32.extend8_s invalid as feature sign-extension-ops is disabled", }, { - input: OpcodeI32Extend16S, - expectedErr: "i32.extend16_s invalid as sign-extension-ops is not yet supported. See #66", + input: OpcodeI32Extend16S, + expectedErrOnDisable: "i32.extend16_s invalid as feature sign-extension-ops is disabled", }, { - input: OpcodeI64Extend8S, - expectedErr: "i64.extend8_s invalid as sign-extension-ops is not yet supported. See #66", + input: OpcodeI64Extend8S, + expectedErrOnDisable: "i64.extend8_s invalid as feature sign-extension-ops is disabled", }, { - input: OpcodeI64Extend16S, - expectedErr: "i64.extend16_s invalid as sign-extension-ops is not yet supported. See #66", + input: OpcodeI64Extend16S, + expectedErrOnDisable: "i64.extend16_s invalid as feature sign-extension-ops is disabled", }, { - input: OpcodeI64Extend32S, - expectedErr: "i64.extend32_s invalid as sign-extension-ops is not yet supported. See #66", + input: OpcodeI64Extend32S, + expectedErrOnDisable: "i64.extend32_s invalid as feature sign-extension-ops is disabled", }, } for _, tt := range tests { tc := tt - t.Run(InstructionName(tc.input), func(t *testing.T) { - err := validateFunction(&FunctionType{}, []byte{tc.input}, nil, nil, nil, nil, nil, nil, 0, Features(0)) - require.EqualError(t, err, tc.expectedErr) + t.Run("disabled", func(t *testing.T) { + err := validateFunction(&FunctionType{}, []byte{tc.input}, nil, nil, nil, nil, nil, nil, maxStackHeight, Features(0)) + require.EqualError(t, err, tc.expectedErrOnDisable) + }) + t.Run("enabled", func(t *testing.T) { + is32bit := tc.input == OpcodeI32Extend8S || tc.input == OpcodeI32Extend16S + var body []byte + if is32bit { + body = append(body, OpcodeI32Const) + } else { + body = append(body, OpcodeI64Const) + } + body = append(body, tc.input, 123, OpcodeDrop, OpcodeEnd) + fmt.Println(body) + err := validateFunction(&FunctionType{}, body, nil, nil, nil, nil, nil, nil, maxStackHeight, FeatureSignExtensionOps) + require.NoError(t, err) + }) }) } } diff --git a/internal/wasm/instruction.go b/internal/wasm/instruction.go index f24a2901..ebb81340 100644 --- a/internal/wasm/instruction.go +++ b/internal/wasm/instruction.go @@ -217,10 +217,24 @@ const ( // Below are toggled with FeatureSignExtensionOps - OpcodeI32Extend8S Opcode = 0xc0 + // OpcodeI32Extend8S extends a signed 8-bit integer to a 32-bit integer. + // Note: This is dependent on the flag FeatureSignExtensionOps + OpcodeI32Extend8S Opcode = 0xc0 + + // OpcodeI32Extend16S extends a signed 16-bit integer to a 32-bit integer. + // Note: This is dependent on the flag FeatureSignExtensionOps OpcodeI32Extend16S Opcode = 0xc1 - OpcodeI64Extend8S Opcode = 0xc2 + + // OpcodeI64Extend8S extends a signed 8-bit integer to a 64-bit integer. + // Note: This is dependent on the flag FeatureSignExtensionOps + OpcodeI64Extend8S Opcode = 0xc2 + + // OpcodeI64Extend16S extends a signed 16-bit integer to a 64-bit integer. + // Note: This is dependent on the flag FeatureSignExtensionOps OpcodeI64Extend16S Opcode = 0xc3 + + // OpcodeI64Extend32S extends a signed 32-bit integer to a 64-bit integer. + // Note: This is dependent on the flag FeatureSignExtensionOps OpcodeI64Extend32S Opcode = 0xc4 LastOpcode = OpcodeI64Extend32S diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go index 054646e4..66e31eac 100644 --- a/internal/wasm/interpreter/interpreter.go +++ b/internal/wasm/interpreter/interpreter.go @@ -430,6 +430,8 @@ func (e *engine) lowerIROps(f *wasm.FunctionInstance, if o.Signed { op.b1 = 1 } + case *wazeroir.OperationSignExtend32From8, *wazeroir.OperationSignExtend32From16, *wazeroir.OperationSignExtend64From8, + *wazeroir.OperationSignExtend64From16, *wazeroir.OperationSignExtend64From32: default: return nil, fmt.Errorf("unreachable: a bug in wazeroir engine") } @@ -1490,6 +1492,37 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *compiledFunctio } frame.pc++ } + + case wazeroir.OperationKindSignExtend32From8: + { + v := int32(int8(ce.pop())) + ce.push(uint64(v)) + frame.pc++ + } + case wazeroir.OperationKindSignExtend32From16: + { + v := int32(int16(ce.pop())) + ce.push(uint64(v)) + frame.pc++ + } + case wazeroir.OperationKindSignExtend64From8: + { + v := int64(int8(ce.pop())) + ce.push(uint64(v)) + frame.pc++ + } + case wazeroir.OperationKindSignExtend64From16: + { + v := int64(int16(ce.pop())) + ce.push(uint64(v)) + frame.pc++ + } + case wazeroir.OperationKindSignExtend64From32: + { + v := int64(int32(ce.pop())) + ce.push(uint64(v)) + frame.pc++ + } } } ce.popFrame() diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go index 421d7c5a..2aeb372b 100644 --- a/internal/wasm/interpreter/interpreter_test.go +++ b/internal/wasm/interpreter/interpreter_test.go @@ -2,6 +2,8 @@ package interpreter import ( "context" + "fmt" + "math" "reflect" "testing" @@ -9,6 +11,7 @@ import ( wasm "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm/buildoptions" + "github.com/tetratelabs/wazero/internal/wazeroir" publicwasm "github.com/tetratelabs/wazero/wasm" ) @@ -117,3 +120,113 @@ func TestEngine_Call_HostFn(t *testing.T) { require.EqualError(t, err, "expected 1 params, but passed 2") }) } + +func TestCallEngine_callNativeFunc_signExtend(t *testing.T) { + translateToIROperationKind := func(op wasm.Opcode) (kind wazeroir.OperationKind) { + switch op { + case wasm.OpcodeI32Extend8S: + kind = wazeroir.OperationKindSignExtend32From8 + case wasm.OpcodeI32Extend16S: + kind = wazeroir.OperationKindSignExtend32From16 + case wasm.OpcodeI64Extend8S: + kind = wazeroir.OperationKindSignExtend64From8 + case wasm.OpcodeI64Extend16S: + kind = wazeroir.OperationKindSignExtend64From16 + case wasm.OpcodeI64Extend32S: + kind = wazeroir.OperationKindSignExtend64From32 + } + return + } + t.Run("32bit", func(t *testing.T) { + for _, tc := range []struct { + in int32 + expected int32 + opcode wasm.Opcode + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L270-L276 + {in: 0, expected: 0, opcode: wasm.OpcodeI32Extend8S}, + {in: 0x7f, expected: 127, opcode: wasm.OpcodeI32Extend8S}, + {in: 0x80, expected: -128, opcode: wasm.OpcodeI32Extend8S}, + {in: 0xff, expected: -1, opcode: wasm.OpcodeI32Extend8S}, + {in: 0x012345_00, expected: 0, opcode: wasm.OpcodeI32Extend8S}, + {in: -19088768 /* = 0xfedcba_80 bit pattern */, expected: -0x80, opcode: wasm.OpcodeI32Extend8S}, + {in: -1, expected: -1, opcode: wasm.OpcodeI32Extend8S}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L278-L284 + {in: 0, expected: 0, opcode: wasm.OpcodeI32Extend16S}, + {in: 0x7fff, expected: 32767, opcode: wasm.OpcodeI32Extend16S}, + {in: 0x8000, expected: -32768, opcode: wasm.OpcodeI32Extend16S}, + {in: 0xffff, expected: -1, opcode: wasm.OpcodeI32Extend16S}, + {in: 0x0123_0000, expected: 0, opcode: wasm.OpcodeI32Extend16S}, + {in: -19103744 /* = 0xfedc_8000 bit pattern */, expected: -0x8000, opcode: wasm.OpcodeI32Extend16S}, + {in: -1, expected: -1, opcode: wasm.OpcodeI32Extend16S}, + } { + tc := tc + t.Run(fmt.Sprintf("%s(i32.const(0x%x))", wasm.InstructionName(tc.opcode), tc.in), func(t *testing.T) { + ce := &callEngine{} + f := &compiledFunction{ + funcInstance: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{}}, + body: []*interpreterOp{ + {kind: wazeroir.OperationKindConstI32, us: []uint64{uint64(uint32(tc.in))}}, + {kind: translateToIROperationKind(tc.opcode)}, + {kind: wazeroir.OperationKindBr, us: []uint64{math.MaxUint64}}, + }, + } + ce.callNativeFunc(&wasm.ModuleContext{}, f) + require.Equal(t, tc.expected, int32(uint32(ce.pop()))) + }) + } + }) + t.Run("64bit", func(t *testing.T) { + for _, tc := range []struct { + in int64 + expected int64 + opcode wasm.Opcode + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L271-L277 + {in: 0, expected: 0, opcode: wasm.OpcodeI64Extend8S}, + {in: 0x7f, expected: 127, opcode: wasm.OpcodeI64Extend8S}, + {in: 0x80, expected: -128, opcode: wasm.OpcodeI64Extend8S}, + {in: 0xff, expected: -1, opcode: wasm.OpcodeI64Extend8S}, + {in: 0x01234567_89abcd_00, expected: 0, opcode: wasm.OpcodeI64Extend8S}, + {in: 81985529216486784 /* = 0xfedcba98_765432_80 bit pattern */, expected: -0x80, opcode: wasm.OpcodeI64Extend8S}, + {in: -1, expected: -1, opcode: wasm.OpcodeI64Extend8S}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L279-L285 + {in: 0, expected: 0, opcode: wasm.OpcodeI64Extend16S}, + {in: 0x7fff, expected: 32767, opcode: wasm.OpcodeI64Extend16S}, + {in: 0x8000, expected: -32768, opcode: wasm.OpcodeI64Extend16S}, + {in: 0xffff, expected: -1, opcode: wasm.OpcodeI64Extend16S}, + {in: 0x12345678_9abc_0000, expected: 0, opcode: wasm.OpcodeI64Extend16S}, + {in: 81985529216466944 /* = 0xfedcba98_7654_8000 bit pattern */, expected: -0x8000, opcode: wasm.OpcodeI64Extend16S}, + {in: -1, expected: -1, opcode: wasm.OpcodeI64Extend16S}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L287-L296 + {in: 0, expected: 0, opcode: wasm.OpcodeI64Extend32S}, + {in: 0x7fff, expected: 32767, opcode: wasm.OpcodeI64Extend32S}, + {in: 0x8000, expected: 32768, opcode: wasm.OpcodeI64Extend32S}, + {in: 0xffff, expected: 65535, opcode: wasm.OpcodeI64Extend32S}, + {in: 0x7fffffff, expected: 0x7fffffff, opcode: wasm.OpcodeI64Extend32S}, + {in: 0x80000000, expected: -0x80000000, opcode: wasm.OpcodeI64Extend32S}, + {in: 0xffffffff, expected: -1, opcode: wasm.OpcodeI64Extend32S}, + {in: 0x01234567_00000000, expected: 0, opcode: wasm.OpcodeI64Extend32S}, + {in: -81985529054232576 /* = 0xfedcba98_80000000 bit pattern */, expected: -0x80000000, opcode: wasm.OpcodeI64Extend32S}, + {in: -1, expected: -1, opcode: wasm.OpcodeI64Extend32S}, + } { + tc := tc + t.Run(fmt.Sprintf("%s(i64.const(0x%x))", wasm.InstructionName(tc.opcode), tc.in), func(t *testing.T) { + ce := &callEngine{} + f := &compiledFunction{ + funcInstance: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{}}, + body: []*interpreterOp{ + {kind: wazeroir.OperationKindConstI64, us: []uint64{uint64(tc.in)}}, + {kind: translateToIROperationKind(tc.opcode)}, + {kind: wazeroir.OperationKindBr, us: []uint64{math.MaxUint64}}, + }, + } + ce.callNativeFunc(&wasm.ModuleContext{}, f) + require.Equal(t, tc.expected, int64(ce.pop())) + }) + } + }) +} diff --git a/internal/wasm/jit/compiler.go b/internal/wasm/jit/compiler.go index 733de1b1..9e0e0fe5 100644 --- a/internal/wasm/jit/compiler.go +++ b/internal/wasm/jit/compiler.go @@ -323,4 +323,19 @@ type compiler interface { // compileConstI32 adds instruction to push the given constant f64 value onto the stack. // See wasm.OpcodeF64Const compileConstF64(o *wazeroir.OperationConstF64) error + // compileSignExtend32From8 adds instruction to sign-extends the first 8-bits of 32-bit in as signed 32-bit int. + // See wasm.OpcodeI32Extend8S + compileSignExtend32From8() error + // compileSignExtend32From16 adds instruction to sign-extends the first 16-bits of 32-bit in as signed 32-bit int. + // See wasm.OpcodeI32Extend16S + compileSignExtend32From16() error + // compileSignExtend64From8 adds instruction to sign-extends the first 8-bits of 64-bit in as signed 64-bit int. + // See wasm.OpcodeI64Extend8S + compileSignExtend64From8() error + // compileSignExtend64From16 adds instruction to sign-extends the first 16-bits of 64-bit in as signed 64-bit int. + // See wasm.OpcodeI64Extend16S + compileSignExtend64From16() error + // compileSignExtend64From32 adds instruction to sign-extends the first 32-bits of 64-bit in as signed 64-bit int. + // See wasm.OpcodeI64Extend32S + compileSignExtend64From32() error } diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go index e3605e80..86ee02a6 100644 --- a/internal/wasm/jit/engine.go +++ b/internal/wasm/jit/engine.go @@ -880,6 +880,16 @@ func compileWasmFunction(f *wasm.FunctionInstance) (*compiledFunction, error) { err = compiler.compileF64ReinterpretFromI64() case *wazeroir.OperationExtend: err = compiler.compileExtend(o) + case *wazeroir.OperationSignExtend32From8: + err = compiler.compileSignExtend32From8() + case *wazeroir.OperationSignExtend32From16: + err = compiler.compileSignExtend32From16() + case *wazeroir.OperationSignExtend64From8: + err = compiler.compileSignExtend64From8() + case *wazeroir.OperationSignExtend64From16: + err = compiler.compileSignExtend64From16() + case *wazeroir.OperationSignExtend64From32: + err = compiler.compileSignExtend64From32() } if err != nil { return nil, fmt.Errorf("failed to compile operation %s: %w", op.Kind().String(), err) diff --git a/internal/wasm/jit/jit_amd64.go b/internal/wasm/jit/jit_amd64.go index 2a198664..a928fe6a 100644 --- a/internal/wasm/jit/jit_amd64.go +++ b/internal/wasm/jit/jit_amd64.go @@ -3450,6 +3450,31 @@ func (c *amd64Compiler) compileExtend(o *wazeroir.OperationExtend) error { return c.compileExtendImpl(inst) } +// compileSignExtend32From8 implements compiler.compileSignExtend32From8 for the amd64 architecture. +func (c *amd64Compiler) compileSignExtend32From8() error { + return c.compileExtendImpl(x86.AMOVBLSX) +} + +// compileSignExtend32From16 implements compiler.compileSignExtend32From16 for the amd64 architecture. +func (c *amd64Compiler) compileSignExtend32From16() error { + return c.compileExtendImpl(x86.AMOVWLSX) +} + +// compileSignExtend64From8 implements compiler.compileSignExtend64From8 for the amd64 architecture. +func (c *amd64Compiler) compileSignExtend64From8() error { + return c.compileExtendImpl(x86.AMOVBQSX) +} + +// compileSignExtend64From16 implements compiler.compileSignExtend64From16 for the amd64 architecture. +func (c *amd64Compiler) compileSignExtend64From16() error { + return c.compileExtendImpl(x86.AMOVWQSX) +} + +// compileSignExtend64From32 implements compiler.compileSignExtend64From32 for the amd64 architecture. +func (c *amd64Compiler) compileSignExtend64From32() error { + return c.compileExtendImpl(x86.AMOVLQSX) +} + func (c *amd64Compiler) compileExtendImpl(inst obj.As) error { target := c.locationStack.peek() // Note this is peek! if err := c.ensureOnGeneralPurposeRegister(target); err != nil { diff --git a/internal/wasm/jit/jit_amd64_test.go b/internal/wasm/jit/jit_amd64_test.go index 38b917c8..8cdeb682 100644 --- a/internal/wasm/jit/jit_amd64_test.go +++ b/internal/wasm/jit/jit_amd64_test.go @@ -4154,6 +4154,142 @@ func TestAmd64Compiler_compileExtend(t *testing.T) { } } +func TestAmd64Compiler_compileSignExtend(t *testing.T) { + type fromKind byte + from8, from16, from32 := fromKind(0), fromKind(1), fromKind(2) + + t.Run("32bit", func(t *testing.T) { + for _, tc := range []struct { + in int32 + expected int32 + fromKind fromKind + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L270-L276 + {in: 0, expected: 0, fromKind: from8}, + {in: 0x7f, expected: 127, fromKind: from8}, + {in: 0x80, expected: -128, fromKind: from8}, + {in: 0xff, expected: -1, fromKind: from8}, + {in: 0x012345_00, expected: 0, fromKind: from8}, + {in: -19088768 /* = 0xfedcba_80 bit pattern */, expected: -0x80, fromKind: from8}, + {in: -1, expected: -1, fromKind: from8}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L278-L284 + {in: 0, expected: 0, fromKind: from16}, + {in: 0x7fff, expected: 32767, fromKind: from16}, + {in: 0x8000, expected: -32768, fromKind: from16}, + {in: 0xffff, expected: -1, fromKind: from16}, + {in: 0x0123_0000, expected: 0, fromKind: from16}, + {in: -19103744 /* = 0xfedc_8000 bit pattern */, expected: -0x8000, fromKind: from16}, + {in: -1, expected: -1, fromKind: from16}, + } { + tc := tc + t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { + env := newJITEnvironment() + compiler := env.requireNewCompiler(t) + err := compiler.compilePreamble() + require.NoError(t, err) + + // Setup the promote target. + err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(tc.in)}) + require.NoError(t, err) + + if tc.fromKind == from8 { + err = compiler.compileSignExtend32From8() + } else { + err = compiler.compileSignExtend32From16() + } + require.NoError(t, err) + + // To verify the behavior, we release the value + // to the stack. + err = compiler.releaseAllRegistersToStack() + require.NoError(t, err) + compiler.exit(jitCallStatusCodeReturned) + + // Generate and run the code under test. + code, _, _, err := compiler.compile() + require.NoError(t, err) + env.exec(code) + + require.Equal(t, uint64(1), env.stackPointer()) + require.Equal(t, tc.expected, env.stackTopAsInt32()) + }) + } + }) + t.Run("64bit", func(t *testing.T) { + for _, tc := range []struct { + in int64 + expected int64 + fromKind fromKind + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L271-L277 + {in: 0, expected: 0, fromKind: from8}, + {in: 0x7f, expected: 127, fromKind: from8}, + {in: 0x80, expected: -128, fromKind: from8}, + {in: 0xff, expected: -1, fromKind: from8}, + {in: 0x01234567_89abcd_00, expected: 0, fromKind: from8}, + {in: 81985529216486784 /* = 0xfedcba98_765432_80 bit pattern */, expected: -0x80, fromKind: from8}, + {in: -1, expected: -1, fromKind: from8}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L279-L285 + {in: 0, expected: 0, fromKind: from16}, + {in: 0x7fff, expected: 32767, fromKind: from16}, + {in: 0x8000, expected: -32768, fromKind: from16}, + {in: 0xffff, expected: -1, fromKind: from16}, + {in: 0x12345678_9abc_0000, expected: 0, fromKind: from16}, + {in: 81985529216466944 /* = 0xfedcba98_7654_8000 bit pattern */, expected: -0x8000, fromKind: from16}, + {in: -1, expected: -1, fromKind: from16}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L287-L296 + {in: 0, expected: 0, fromKind: from32}, + {in: 0x7fff, expected: 32767, fromKind: from32}, + {in: 0x8000, expected: 32768, fromKind: from32}, + {in: 0xffff, expected: 65535, fromKind: from32}, + {in: 0x7fffffff, expected: 0x7fffffff, fromKind: from32}, + {in: 0x80000000, expected: -0x80000000, fromKind: from32}, + {in: 0xffffffff, expected: -1, fromKind: from32}, + {in: 0x01234567_00000000, expected: 0, fromKind: from32}, + {in: -81985529054232576 /* = 0xfedcba98_80000000 bit pattern */, expected: -0x80000000, fromKind: from32}, + {in: -1, expected: -1, fromKind: from32}, + } { + tc := tc + t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { + env := newJITEnvironment() + compiler := env.requireNewCompiler(t) + err := compiler.compilePreamble() + require.NoError(t, err) + + // Setup the promote target. + err = compiler.compileConstI64(&wazeroir.OperationConstI64{Value: uint64(tc.in)}) + require.NoError(t, err) + + if tc.fromKind == from8 { + err = compiler.compileSignExtend64From8() + } else if tc.fromKind == from16 { + err = compiler.compileSignExtend64From16() + } else { + err = compiler.compileSignExtend64From32() + } + require.NoError(t, err) + + // To verify the behavior, we release the value + // to the stack. + err = compiler.releaseAllRegistersToStack() + require.NoError(t, err) + compiler.exit(jitCallStatusCodeReturned) + + // Generate and run the code under test. + code, _, _, err := compiler.compile() + require.NoError(t, err) + env.exec(code) + + require.Equal(t, uint64(1), env.stackPointer()) + require.Equal(t, tc.expected, env.stackTopAsInt64()) + }) + } + }) +} + func TestAmd64Compiler_compileITruncFromF(t *testing.T) { for _, tc := range []struct { outputType wazeroir.SignedInt diff --git a/internal/wasm/jit/jit_arm64.go b/internal/wasm/jit/jit_arm64.go index 1f0b18ba..064c9621 100644 --- a/internal/wasm/jit/jit_arm64.go +++ b/internal/wasm/jit/jit_arm64.go @@ -2552,6 +2552,31 @@ func (c *arm64Compiler) compileExtend(o *wazeroir.OperationExtend) error { } } +// compileSignExtend32From8 implements compiler.compileSignExtend32From8 for the arm64 architecture. +func (c *arm64Compiler) compileSignExtend32From8() error { + return c.compileSimpleUnop(arm64.ASXTBW) +} + +// compileSignExtend32From16 implements compiler.compileSignExtend32From16 for the arm64 architecture. +func (c *arm64Compiler) compileSignExtend32From16() error { + return c.compileSimpleUnop(arm64.ASXTHW) +} + +// compileSignExtend64From8 implements compiler.compileSignExtend64From8 for the arm64 architecture. +func (c *arm64Compiler) compileSignExtend64From8() error { + return c.compileSimpleUnop(arm64.ASXTB) +} + +// compileSignExtend64From16 implements compiler.compileSignExtend64From16 for the arm64 architecture. +func (c *arm64Compiler) compileSignExtend64From16() error { + return c.compileSimpleUnop(arm64.ASXTH) +} + +// compileSignExtend64From32 implements compiler.compileSignExtend64From32 for the arm64 architecture. +func (c *arm64Compiler) compileSignExtend64From32() error { + return c.compileSimpleUnop(arm64.ASXTW) +} + func (c *arm64Compiler) compileSimpleUnop(inst obj.As) error { v, err := c.popValueOnRegister() if err != nil { diff --git a/internal/wasm/jit/jit_arm64_test.go b/internal/wasm/jit/jit_arm64_test.go index a92393b9..e8e5cdf8 100644 --- a/internal/wasm/jit/jit_arm64_test.go +++ b/internal/wasm/jit/jit_arm64_test.go @@ -3876,6 +3876,140 @@ func TestArm64Compiler_compileExtend(t *testing.T) { } } +func TestArm64Compiler_compileSignExtend(t *testing.T) { + type fromKind byte + from8, from16, from32 := fromKind(0), fromKind(1), fromKind(2) + + t.Run("32bit", func(t *testing.T) { + for _, tc := range []struct { + in int32 + expected int32 + fromKind fromKind + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L270-L276 + {in: 0, expected: 0, fromKind: from8}, + {in: 0x7f, expected: 127, fromKind: from8}, + {in: 0x80, expected: -128, fromKind: from8}, + {in: 0xff, expected: -1, fromKind: from8}, + {in: 0x012345_00, expected: 0, fromKind: from8}, + {in: -19088768 /* = 0xfedcba_80 bit pattern */, expected: -0x80, fromKind: from8}, + {in: -1, expected: -1, fromKind: from8}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L278-L284 + {in: 0, expected: 0, fromKind: from16}, + {in: 0x7fff, expected: 32767, fromKind: from16}, + {in: 0x8000, expected: -32768, fromKind: from16}, + {in: 0xffff, expected: -1, fromKind: from16}, + {in: 0x0123_0000, expected: 0, fromKind: from16}, + {in: -19103744 /* = 0xfedc_8000 bit pattern */, expected: -0x8000, fromKind: from16}, + {in: -1, expected: -1, fromKind: from16}, + } { + tc := tc + t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { + env := newJITEnvironment() + compiler := env.requireNewCompiler(t) + err := compiler.compilePreamble() + require.NoError(t, err) + + // Setup the promote target. + err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(tc.in)}) + require.NoError(t, err) + + if tc.fromKind == from8 { + err = compiler.compileSignExtend32From8() + } else { + err = compiler.compileSignExtend32From16() + } + require.NoError(t, err) + + // To verify the behavior, we release the value + // to the stack. + 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, uint64(1), env.stackPointer()) + require.Equal(t, tc.expected, env.stackTopAsInt32()) + }) + } + }) + t.Run("64bit", func(t *testing.T) { + for _, tc := range []struct { + in int64 + expected int64 + fromKind fromKind + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L271-L277 + {in: 0, expected: 0, fromKind: from8}, + {in: 0x7f, expected: 127, fromKind: from8}, + {in: 0x80, expected: -128, fromKind: from8}, + {in: 0xff, expected: -1, fromKind: from8}, + {in: 0x01234567_89abcd_00, expected: 0, fromKind: from8}, + {in: 81985529216486784 /* = 0xfedcba98_765432_80 bit pattern */, expected: -0x80, fromKind: from8}, + {in: -1, expected: -1, fromKind: from8}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L279-L285 + {in: 0, expected: 0, fromKind: from16}, + {in: 0x7fff, expected: 32767, fromKind: from16}, + {in: 0x8000, expected: -32768, fromKind: from16}, + {in: 0xffff, expected: -1, fromKind: from16}, + {in: 0x12345678_9abc_0000, expected: 0, fromKind: from16}, + {in: 81985529216466944 /* = 0xfedcba98_7654_8000 bit pattern */, expected: -0x8000, fromKind: from16}, + {in: -1, expected: -1, fromKind: from16}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L287-L296 + {in: 0, expected: 0, fromKind: from32}, + {in: 0x7fff, expected: 32767, fromKind: from32}, + {in: 0x8000, expected: 32768, fromKind: from32}, + {in: 0xffff, expected: 65535, fromKind: from32}, + {in: 0x7fffffff, expected: 0x7fffffff, fromKind: from32}, + {in: 0x80000000, expected: -0x80000000, fromKind: from32}, + {in: 0xffffffff, expected: -1, fromKind: from32}, + {in: 0x01234567_00000000, expected: 0, fromKind: from32}, + {in: -81985529054232576 /* = 0xfedcba98_80000000 bit pattern */, expected: -0x80000000, fromKind: from32}, + {in: -1, expected: -1, fromKind: from32}, + } { + tc := tc + t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { + env := newJITEnvironment() + compiler := env.requireNewCompiler(t) + err := compiler.compilePreamble() + require.NoError(t, err) + + // Setup the promote target. + err = compiler.compileConstI64(&wazeroir.OperationConstI64{Value: uint64(tc.in)}) + require.NoError(t, err) + + if tc.fromKind == from8 { + err = compiler.compileSignExtend64From8() + } else if tc.fromKind == from16 { + err = compiler.compileSignExtend64From16() + } else { + err = compiler.compileSignExtend64From32() + } + require.NoError(t, err) + + // To verify the behavior, we release the value + // to the stack. + 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, uint64(1), env.stackPointer()) + require.Equal(t, tc.expected, env.stackTopAsInt64()) + }) + } + }) +} + func TestArm64Compiler_compileITruncFromF(t *testing.T) { for _, tc := range []struct { outputType wazeroir.SignedInt diff --git a/internal/wasm/text/decoder_test.go b/internal/wasm/text/decoder_test.go index 462a401d..8f060309 100644 --- a/internal/wasm/text/decoder_test.go +++ b/internal/wasm/text/decoder_test.go @@ -15,8 +15,9 @@ func TestDecodeModule(t *testing.T) { localGet0End := []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeEnd} tests := []struct { - name, input string - expected *wasm.Module + name, input string + enabledFeatures wasm.Features + expected *wasm.Module }{ { name: "empty", @@ -203,6 +204,20 @@ func TestDecodeModule(t *testing.T) { }, }, }, + { + name: "func sign-extension", + input: `(module + (func (param i64) (result i64) local.get 0 i64.extend16_s) + )`, + enabledFeatures: wasm.FeatureSignExtensionOps, + expected: &wasm.Module{ + TypeSection: []*wasm.FunctionType{i64_i64}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{ + {Body: []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeI64Extend16S, wasm.OpcodeEnd}}, + }, + }, + }, { // Spec says expand abbreviations first. It doesn't explicitly say you can't mix forms. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A0 @@ -1493,7 +1508,10 @@ func TestDecodeModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, err := DecodeModule([]byte(tc.input), wasm.Features20191205) + if tc.enabledFeatures == 0 { + tc.enabledFeatures = wasm.Features20191205 + } + m, err := DecodeModule([]byte(tc.input), tc.enabledFeatures) require.NoError(t, err) require.Equal(t, tc.expected, m) }) @@ -1501,7 +1519,11 @@ func TestDecodeModule(t *testing.T) { } func TestParseModule_Errors(t *testing.T) { - tests := []struct{ name, input, expectedErr string }{ + tests := []struct { + name, input string + enabledFeatures wasm.Features + expectedErr string + }{ { name: "forgot parens", input: "module", @@ -1938,6 +1960,13 @@ func TestParseModule_Errors(t *testing.T) { )`, expectedErr: "3:15: unknown ID $mein in module.code[1].body[1]", }, + { + name: "func sign-extension disabled", + input: `(module + (func (param i64) (result i64) local.get 0 i64.extend16_s) + )`, + expectedErr: "2:47: i64.extend16_s invalid as feature sign-extension-ops is disabled in module.func[0]", + }, { name: "second memory", input: "(module (memory 1) (memory 1))", @@ -2080,7 +2109,10 @@ func TestParseModule_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, err := DecodeModule([]byte(tc.input), wasm.Features20191205) + if tc.enabledFeatures == 0 { + tc.enabledFeatures = wasm.Features20191205 + } + _, err := DecodeModule([]byte(tc.input), tc.enabledFeatures) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wasm/text/func_parser.go b/internal/wasm/text/func_parser.go index b87080db..93acd113 100644 --- a/internal/wasm/text/func_parser.go +++ b/internal/wasm/text/func_parser.go @@ -154,9 +154,32 @@ func (p *funcParser) beginInstruction(tokenBytes []byte) (next tokenParser, err case "call": // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-controlmathsfcallx opCode = wasm.OpcodeCall next = p.parseFuncIndex + case "i32.extend8_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md + opCode = wasm.OpcodeI32Extend8S + next = p.beginFieldOrInstruction + case "i32.extend16_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md + opCode = wasm.OpcodeI32Extend16S + next = p.beginFieldOrInstruction + case "i64.extend8_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md + opCode = wasm.OpcodeI64Extend8S + next = p.beginFieldOrInstruction + case "i64.extend16_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md + opCode = wasm.OpcodeI64Extend16S + next = p.beginFieldOrInstruction + case "i64.extend32_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md + opCode = wasm.OpcodeI64Extend32S + next = p.beginFieldOrInstruction default: return nil, fmt.Errorf("unsupported instruction: %s", tokenBytes) } + + // Guard >1.0 feature sign-extension-ops + if opCode >= wasm.OpcodeI32Extend8S && opCode <= wasm.OpcodeI64Extend32S { + if err = p.enabledFeatures.Require(wasm.FeatureSignExtensionOps); err != nil { + return nil, fmt.Errorf("%s invalid as %v", tokenBytes, err) + } + } + p.currentBody = append(p.currentBody, opCode) return next, nil } diff --git a/internal/wasm/text/func_parser_test.go b/internal/wasm/text/func_parser_test.go index 1b6904bd..b3234de3 100644 --- a/internal/wasm/text/func_parser_test.go +++ b/internal/wasm/text/func_parser_test.go @@ -40,6 +40,56 @@ func TestFuncParser(t *testing.T) { wasm.OpcodeEnd, }}, }, + { + name: "i32.extend8_s", + source: "(func (param i32) local.get 0 i32.extend8_s)", + enabledFeatures: wasm.FeatureSignExtensionOps, + expected: &wasm.Code{Body: []byte{ + wasm.OpcodeLocalGet, 0x00, + wasm.OpcodeI32Extend8S, + wasm.OpcodeEnd, + }}, + }, + { + name: "i32.extend16_s", + source: "(func (param i32) local.get 0 i32.extend16_s)", + enabledFeatures: wasm.FeatureSignExtensionOps, + expected: &wasm.Code{Body: []byte{ + wasm.OpcodeLocalGet, 0x00, + wasm.OpcodeI32Extend16S, + wasm.OpcodeEnd, + }}, + }, + { + name: "i64.extend8_s", + source: "(func (param i64) local.get 0 i64.extend8_s)", + enabledFeatures: wasm.FeatureSignExtensionOps, + expected: &wasm.Code{Body: []byte{ + wasm.OpcodeLocalGet, 0x00, + wasm.OpcodeI64Extend8S, + wasm.OpcodeEnd, + }}, + }, + { + name: "i64.extend16_s", + source: "(func (param i64) local.get 0 i64.extend16_s)", + enabledFeatures: wasm.FeatureSignExtensionOps, + expected: &wasm.Code{Body: []byte{ + wasm.OpcodeLocalGet, 0x00, + wasm.OpcodeI64Extend16S, + wasm.OpcodeEnd, + }}, + }, + { + name: "i64.extend32_s", + source: "(func (param i64) local.get 0 i64.extend32_s)", + enabledFeatures: wasm.FeatureSignExtensionOps, + expected: &wasm.Code{Body: []byte{ + wasm.OpcodeLocalGet, 0x00, + wasm.OpcodeI64Extend32S, + wasm.OpcodeEnd, + }}, + }, } for _, tt := range tests { @@ -226,6 +276,31 @@ func TestFuncParser_Errors(t *testing.T) { source: "(func (result i32) (result i32))", expectedErr: "1:21: at most one result allowed", }, + { + name: "i32.extend8_s disabled", + source: "(func (param i32) local.get 0 i32.extend8_s)", + expectedErr: "1:31: i32.extend8_s invalid as feature sign-extension-ops is disabled", + }, + { + name: "i32.extend16_s disabled", + source: "(func (param i32) local.get 0 i32.extend16_s)", + expectedErr: "1:31: i32.extend16_s invalid as feature sign-extension-ops is disabled", + }, + { + name: "i64.extend8_s disabled", + source: "(func (param i64) local.get 0 i64.extend8_s)", + expectedErr: "1:31: i64.extend8_s invalid as feature sign-extension-ops is disabled", + }, + { + name: "i64.extend16_s disabled", + source: "(func (param i64) local.get 0 i64.extend16_s)", + expectedErr: "1:31: i64.extend16_s invalid as feature sign-extension-ops is disabled", + }, + { + name: "i64.extend32_s disabled", + source: "(func (param i64) local.get 0 i64.extend32_s)", + expectedErr: "1:31: i64.extend32_s invalid as feature sign-extension-ops is disabled", + }, } for _, tt := range tests { diff --git a/internal/wasm/text/type_parser_test.go b/internal/wasm/text/type_parser_test.go index 2cebb338..00dd3146 100644 --- a/internal/wasm/text/type_parser_test.go +++ b/internal/wasm/text/type_parser_test.go @@ -12,6 +12,7 @@ var ( f32, i32, i64 = wasm.ValueTypeF32, wasm.ValueTypeI32, wasm.ValueTypeI64 i32_v = &wasm.FunctionType{Params: []wasm.ValueType{i32}} v_i32 = &wasm.FunctionType{Results: []wasm.ValueType{i32}} + i64_i64 = &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}} i32i64_v = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}} i32i32_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}} i32i64_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}} diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go index 5a4d8143..3b73cf8a 100644 --- a/internal/wazeroir/compiler.go +++ b/internal/wazeroir/compiler.go @@ -1362,6 +1362,26 @@ operatorSwitch: c.emit( &OperationF64ReinterpretFromI64{}, ) + case wasm.OpcodeI32Extend8S: + c.emit( + &OperationSignExtend32From8{}, + ) + case wasm.OpcodeI32Extend16S: + c.emit( + &OperationSignExtend32From16{}, + ) + case wasm.OpcodeI64Extend8S: + c.emit( + &OperationSignExtend64From8{}, + ) + case wasm.OpcodeI64Extend16S: + c.emit( + &OperationSignExtend64From16{}, + ) + case wasm.OpcodeI64Extend32S: + c.emit( + &OperationSignExtend64From32{}, + ) default: return fmt.Errorf("unsupported instruction in wazeroir: 0x%x", op) } diff --git a/internal/wazeroir/operations.go b/internal/wazeroir/operations.go index c18b03dd..7d4fb1e1 100644 --- a/internal/wazeroir/operations.go +++ b/internal/wazeroir/operations.go @@ -335,6 +335,11 @@ const ( OperationKindF32ReinterpretFromI32 OperationKindF64ReinterpretFromI64 OperationKindExtend + OperationKindSignExtend32From8 + OperationKindSignExtend32From16 + OperationKindSignExtend64From8 + OperationKindSignExtend64From16 + OperationKindSignExtend64From32 ) type Label struct { @@ -872,3 +877,33 @@ type OperationExtend struct{ Signed bool } func (o *OperationExtend) Kind() OperationKind { return OperationKindExtend } + +type OperationSignExtend32From8 struct{} + +func (o *OperationSignExtend32From8) Kind() OperationKind { + return OperationKindSignExtend32From8 +} + +type OperationSignExtend32From16 struct{} + +func (o *OperationSignExtend32From16) Kind() OperationKind { + return OperationKindSignExtend32From16 +} + +type OperationSignExtend64From8 struct{} + +func (o *OperationSignExtend64From8) Kind() OperationKind { + return OperationKindSignExtend64From8 +} + +type OperationSignExtend64From16 struct{} + +func (o *OperationSignExtend64From16) Kind() OperationKind { + return OperationKindSignExtend64From16 +} + +type OperationSignExtend64From32 struct{} + +func (o *OperationSignExtend64From32) Kind() OperationKind { + return OperationKindSignExtend64From32 +} diff --git a/internal/wazeroir/signature.go b/internal/wazeroir/signature.go index 4e55f521..d23abc3a 100644 --- a/internal/wazeroir/signature.go +++ b/internal/wazeroir/signature.go @@ -346,6 +346,10 @@ func wasmOpcodeSignature(f *wasm.FunctionInstance, op wasm.Opcode, index uint32) return signature_I32_F32, nil case wasm.OpcodeF64ReinterpretI64: return signature_I64_F64, nil + case wasm.OpcodeI32Extend8S, wasm.OpcodeI32Extend16S: + return signature_I32_I32, nil + case wasm.OpcodeI64Extend8S, wasm.OpcodeI64Extend16S, wasm.OpcodeI64Extend32S: + return signature_I64_I64, nil default: return nil, fmt.Errorf("unsupported instruction in wazeroir: 0x%x", op) } diff --git a/tests/post1_0/post1_0_test.go b/tests/post1_0/post1_0_test.go index 9129d0a0..29c9a8da 100644 --- a/tests/post1_0/post1_0_test.go +++ b/tests/post1_0/post1_0_test.go @@ -1,9 +1,13 @@ package post1_0 import ( + "context" _ "embed" + "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/tetratelabs/wazero" ) @@ -23,5 +27,127 @@ func TestInterpreter(t *testing.T) { // // See https://github.com/WebAssembly/proposals/blob/main/finished-proposals.md func runOptionalFeatureTests(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) { - // TODO: + t.Run("sign-extension-ops", func(t *testing.T) { + testSignExtensionOps(t, newRuntimeConfig) + }) +} + +// signExtend is a WebAssembly 1.0 (20191205) Text Format source, except that it uses opcodes from 'sign-extension-ops'. +// +// See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md +var signExtend = []byte(`(module + (func $i32.extend8_s (param i32) (result i32) local.get 0 i32.extend8_s) + (export "i32.extend8_s" (func $i32.extend8_s)) + + (func $i32.extend16_s (param i32) (result i32) local.get 0 i32.extend16_s) + (export "i32.extend16_s" (func $i32.extend16_s)) + + (func $i64.extend8_s (param i64) (result i64) local.get 0 i64.extend8_s) + (export "i64.extend8_s" (func $i64.extend8_s)) + + (func $i64.extend16_s (param i64) (result i64) local.get 0 i64.extend16_s) + (export "i64.extend16_s" (func $i64.extend16_s)) + + (func $i64.extend32_s (param i64) (result i64) local.get 0 i64.extend32_s) + (export "i64.extend32_s" (func $i64.extend32_s)) +) +`) + +func testSignExtensionOps(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) { + t.Run("disabled", func(t *testing.T) { + // Sign-extension is disabled by default. + r := wazero.NewRuntimeWithConfig(newRuntimeConfig()) + _, err := r.NewModuleFromSource(signExtend) + require.Error(t, err) + }) + t.Run("enabled", func(t *testing.T) { + r := wazero.NewRuntimeWithConfig(newRuntimeConfig().WithFeatureSignExtensionOps(true)) + module, err := r.NewModuleFromSource(signExtend) + require.NoError(t, err) + + signExtend32from8Name, signExtend32from16Name := "i32.extend8_s", "i32.extend16_s" + t.Run("32bit", func(t *testing.T) { + for _, tc := range []struct { + in int32 + expected int32 + funcname string + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L270-L276 + {in: 0, expected: 0, funcname: signExtend32from8Name}, + {in: 0x7f, expected: 127, funcname: signExtend32from8Name}, + {in: 0x80, expected: -128, funcname: signExtend32from8Name}, + {in: 0xff, expected: -1, funcname: signExtend32from8Name}, + {in: 0x012345_00, expected: 0, funcname: signExtend32from8Name}, + {in: -19088768 /* = 0xfedcba_80 bit pattern */, expected: -0x80, funcname: signExtend32from8Name}, + {in: -1, expected: -1, funcname: signExtend32from8Name}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i32.wast#L278-L284 + {in: 0, expected: 0, funcname: signExtend32from16Name}, + {in: 0x7fff, expected: 32767, funcname: signExtend32from16Name}, + {in: 0x8000, expected: -32768, funcname: signExtend32from16Name}, + {in: 0xffff, expected: -1, funcname: signExtend32from16Name}, + {in: 0x0123_0000, expected: 0, funcname: signExtend32from16Name}, + {in: -19103744 /* = 0xfedc_8000 bit pattern */, expected: -0x8000, funcname: signExtend32from16Name}, + {in: -1, expected: -1, funcname: signExtend32from16Name}, + } { + tc := tc + t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { + fn := module.Function(tc.funcname) + require.NotNil(t, fn) + + actual, err := fn.Call(context.Background(), uint64(uint32(tc.in))) + require.NoError(t, err) + require.Equal(t, tc.expected, int32(actual[0])) + }) + } + }) + signExtend64from8Name, signExtend64from16Name, signExtend64from32Name := "i64.extend8_s", "i64.extend16_s", "i64.extend32_s" + t.Run("64bit", func(t *testing.T) { + for _, tc := range []struct { + in int64 + expected int64 + funcname string + }{ + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L271-L277 + {in: 0, expected: 0, funcname: signExtend64from8Name}, + {in: 0x7f, expected: 127, funcname: signExtend64from8Name}, + {in: 0x80, expected: -128, funcname: signExtend64from8Name}, + {in: 0xff, expected: -1, funcname: signExtend64from8Name}, + {in: 0x01234567_89abcd_00, expected: 0, funcname: signExtend64from8Name}, + {in: 81985529216486784 /* = 0xfedcba98_765432_80 bit pattern */, expected: -0x80, funcname: signExtend64from8Name}, + {in: -1, expected: -1, funcname: signExtend64from8Name}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L279-L285 + {in: 0, expected: 0, funcname: signExtend64from16Name}, + {in: 0x7fff, expected: 32767, funcname: signExtend64from16Name}, + {in: 0x8000, expected: -32768, funcname: signExtend64from16Name}, + {in: 0xffff, expected: -1, funcname: signExtend64from16Name}, + {in: 0x12345678_9abc_0000, expected: 0, funcname: signExtend64from16Name}, + {in: 81985529216466944 /* = 0xfedcba98_7654_8000 bit pattern */, expected: -0x8000, funcname: signExtend64from16Name}, + {in: -1, expected: -1, funcname: signExtend64from16Name}, + + // https://github.com/WebAssembly/spec/blob/ee4a6c40afa22e3e4c58610ce75186aafc22344e/test/core/i64.wast#L287-L296 + {in: 0, expected: 0, funcname: signExtend64from32Name}, + {in: 0x7fff, expected: 32767, funcname: signExtend64from32Name}, + {in: 0x8000, expected: 32768, funcname: signExtend64from32Name}, + {in: 0xffff, expected: 65535, funcname: signExtend64from32Name}, + {in: 0x7fffffff, expected: 0x7fffffff, funcname: signExtend64from32Name}, + {in: 0x80000000, expected: -0x80000000, funcname: signExtend64from32Name}, + {in: 0xffffffff, expected: -1, funcname: signExtend64from32Name}, + {in: 0x01234567_00000000, expected: 0, funcname: signExtend64from32Name}, + {in: -81985529054232576 /* = 0xfedcba98_80000000 bit pattern */, expected: -0x80000000, funcname: signExtend64from32Name}, + {in: -1, expected: -1, funcname: signExtend64from32Name}, + } { + tc := tc + t.Run(fmt.Sprintf("0x%x", tc.in), func(t *testing.T) { + fn := module.Function(tc.funcname) + require.NotNil(t, fn) + + actual, err := fn.Call(context.Background(), uint64(tc.in)) + require.NoError(t, err) + require.Equal(t, tc.expected, int64(actual[0])) + }) + } + }) + }) } diff --git a/vs/codec_test.go b/vs/codec_test.go index d8d323eb..c7df41f5 100644 --- a/vs/codec_test.go +++ b/vs/codec_test.go @@ -30,14 +30,17 @@ var exampleText []byte // exampleBinary is the exampleText encoded in the WebAssembly 1.0 binary format. var exampleBinary = binary.EncodeModule(example) +var enabledFeatures = wasm.Features20191205.Set(wasm.FeatureSignExtensionOps, true) + func newExample() *wasm.Module { three := wasm.Index(3) - i32 := wasm.ValueTypeI32 + i32, i64 := wasm.ValueTypeI32, wasm.ValueTypeI64 return &wasm.Module{ TypeSection: []*wasm.FunctionType{ {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, {}, {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, + {Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}}, }, ImportSection: []*wasm.Import{ { @@ -50,11 +53,12 @@ func newExample() *wasm.Module { DescFunc: 2, }, }, - FunctionSection: []wasm.Index{wasm.Index(1), wasm.Index(1), wasm.Index(0)}, + FunctionSection: []wasm.Index{wasm.Index(1), wasm.Index(1), wasm.Index(0), wasm.Index(3)}, CodeSection: []*wasm.Code{ {Body: []byte{wasm.OpcodeCall, 3, wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeI32Add, wasm.OpcodeEnd}}, + {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeI64Extend16S, wasm.OpcodeEnd}}, }, MemorySection: []*wasm.MemoryType{{Min: 1, Max: &three}}, ExportSection: map[string]*wasm.Export{ @@ -90,19 +94,19 @@ func newExample() *wasm.Module { func TestExampleUpToDate(t *testing.T) { t.Run("binary.DecodeModule", func(t *testing.T) { - m, err := binary.DecodeModule(exampleBinary, wasm.Features20191205) + m, err := binary.DecodeModule(exampleBinary, enabledFeatures) require.NoError(t, err) require.Equal(t, example, m) }) t.Run("text.DecodeModule", func(t *testing.T) { - m, err := text.DecodeModule(exampleText, wasm.Features20191205) + m, err := text.DecodeModule(exampleText, enabledFeatures) require.NoError(t, err) require.Equal(t, example, m) }) t.Run("Executable", func(t *testing.T) { - r := wazero.NewRuntime() + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithFeatureSignExtensionOps(true)) // Add WASI to satisfy import tests _, err := r.NewHostModule(wazero.WASISnapshotPreview1()) @@ -123,7 +127,7 @@ func BenchmarkCodecExample(b *testing.B) { b.Run("binary.DecodeModule", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := binary.DecodeModule(exampleBinary, wasm.Features20191205); err != nil { + if _, err := binary.DecodeModule(exampleBinary, enabledFeatures); err != nil { b.Fatal(err) } } @@ -137,7 +141,7 @@ func BenchmarkCodecExample(b *testing.B) { b.Run("text.DecodeModule", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := text.DecodeModule(exampleText, wasm.Features20191205); err != nil { + if _, err := text.DecodeModule(exampleText, enabledFeatures); err != nil { b.Fatal(err) } } @@ -145,7 +149,7 @@ func BenchmarkCodecExample(b *testing.B) { b.Run("wat2wasm via text.DecodeModule->binary.EncodeModule", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if m, err := text.DecodeModule(exampleText, wasm.Features20191205); err != nil { + if m, err := text.DecodeModule(exampleText, enabledFeatures); err != nil { b.Fatal(err) } else { _ = binary.EncodeModule(m) diff --git a/vs/testdata/example.wat b/vs/testdata/example.wat index f1086e61..03ed9485 100644 --- a/vs/testdata/example.wat +++ b/vs/testdata/example.wat @@ -37,4 +37,7 @@ ;; export a memory before it was defined, given its symbolic ID (export "mem" (memory $mem)) (memory $mem 1 3) + + ;; add >1.0 feature from https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md + (func (param i64) (result i64) local.get 0 i64.extend16_s) )