Signed-off-by: Achille Roussel <achille.roussel@gmail.com> Co-authored-by: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com>
786 lines
29 KiB
Go
786 lines
29 KiB
Go
package compiler
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/tetratelabs/wazero/internal/asm"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
"github.com/tetratelabs/wazero/internal/wasm"
|
|
"github.com/tetratelabs/wazero/internal/wazeroir"
|
|
)
|
|
|
|
func TestCompiler_releaseRegisterToStack(t *testing.T) {
|
|
const val = 10000
|
|
tests := []struct {
|
|
name string
|
|
stackPointer uint64
|
|
isFloat bool
|
|
}{
|
|
{name: "int", stackPointer: 10, isFloat: false},
|
|
{name: "float", stackPointer: 10, isFloat: true},
|
|
{name: "int-huge-height", stackPointer: math.MaxInt16 + 1, isFloat: false},
|
|
{name: "float-huge-height", stackPointer: math.MaxInt16 + 1, isFloat: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
|
|
// Compile code.
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
// Set up the location stack so that we push the const on the specified height.
|
|
s := &runtimeValueLocationStack{
|
|
sp: tc.stackPointer,
|
|
stack: make([]runtimeValueLocation, tc.stackPointer),
|
|
unreservedVectorRegisters: unreservedVectorRegisters,
|
|
unreservedGeneralPurposeRegisters: unreservedGeneralPurposeRegisters,
|
|
}
|
|
// Peek must be non-nil. Otherwise, compileConst* would fail.
|
|
compiler.setRuntimeValueLocationStack(s)
|
|
|
|
if tc.isFloat {
|
|
err = compiler.compileConstF64(operationPtr(wazeroir.NewOperationConstF64(math.Float64frombits(val))))
|
|
} else {
|
|
err = compiler.compileConstI64(operationPtr(wazeroir.NewOperationConstI64(val)))
|
|
}
|
|
require.NoError(t, err)
|
|
// Release the register allocated value to the memory stack so that we can see the value after exiting.
|
|
compiler.compileReleaseRegisterToStack(compiler.runtimeValueLocationStack().peek())
|
|
compiler.compileExitFromNativeCode(nativeCallStatusCodeReturned)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
// Generate the code under test.
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
|
|
// Run native code after growing the value stack.
|
|
env.callEngine().builtinFunctionGrowStack(tc.stackPointer)
|
|
env.exec(code.Bytes())
|
|
|
|
// Compiler status must be returned and stack pointer must end up the specified one.
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
require.Equal(t, tc.stackPointer+1, env.ce.stackPointer)
|
|
|
|
if tc.isFloat {
|
|
require.Equal(t, math.Float64frombits(val), env.stackTopAsFloat64())
|
|
} else {
|
|
require.Equal(t, uint64(val), env.stackTopAsUint64())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompiler_compileLoadValueOnStackToRegister(t *testing.T) {
|
|
const val = 123
|
|
tests := []struct {
|
|
name string
|
|
stackPointer uint64
|
|
isFloat bool
|
|
}{
|
|
{name: "int", stackPointer: 10, isFloat: false},
|
|
{name: "float", stackPointer: 10, isFloat: true},
|
|
{name: "int-huge-height", stackPointer: math.MaxInt16 + 1, isFloat: false},
|
|
{name: "float-huge-height", stackPointer: math.MaxInt16 + 1, isFloat: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
|
|
// Compile code.
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
// Setup the location stack so that we push the const on the specified height.
|
|
compiler.runtimeValueLocationStack().sp = tc.stackPointer
|
|
compiler.runtimeValueLocationStack().stack = make([]runtimeValueLocation, tc.stackPointer)
|
|
|
|
require.Zero(t, len(compiler.runtimeValueLocationStack().usedRegisters.list()))
|
|
loc := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
if tc.isFloat {
|
|
loc.valueType = runtimeValueTypeF64
|
|
} else {
|
|
loc.valueType = runtimeValueTypeI64
|
|
}
|
|
// At this point the value must be recorded as being on stack.
|
|
require.True(t, loc.onStack())
|
|
|
|
// Release the stack-allocated value to register.
|
|
err = compiler.compileEnsureOnRegister(loc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(compiler.runtimeValueLocationStack().usedRegisters.list()))
|
|
require.True(t, loc.onRegister())
|
|
|
|
// To verify the behavior, increment the value on the register.
|
|
if tc.isFloat {
|
|
err = compiler.compileConstF64(operationPtr(wazeroir.NewOperationConstF64(1)))
|
|
require.NoError(t, err)
|
|
err = compiler.compileAdd(operationPtr(wazeroir.NewOperationAdd(wazeroir.UnsignedTypeF64)))
|
|
require.NoError(t, err)
|
|
} else {
|
|
err = compiler.compileConstI64(operationPtr(wazeroir.NewOperationConstI64(1)))
|
|
require.NoError(t, err)
|
|
err = compiler.compileAdd(operationPtr(wazeroir.NewOperationAdd(wazeroir.UnsignedTypeI64)))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Release the value to the memory stack so that we can see the value after exiting.
|
|
compiler.compileReleaseRegisterToStack(loc)
|
|
require.NoError(t, err)
|
|
compiler.compileExitFromNativeCode(nativeCallStatusCodeReturned)
|
|
require.NoError(t, err)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
// Generate the code under test.
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
|
|
// Run native code after growing the value stack, and place the original value.
|
|
env.callEngine().builtinFunctionGrowStack(tc.stackPointer)
|
|
env.stack()[tc.stackPointer] = val
|
|
env.exec(code.Bytes())
|
|
|
|
// Compiler status must be returned and stack pointer must end up the specified one.
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
require.Equal(t, tc.stackPointer+1, env.ce.stackPointer)
|
|
|
|
if tc.isFloat {
|
|
require.Equal(t, math.Float64frombits(val)+1, env.stackTopAsFloat64())
|
|
} else {
|
|
require.Equal(t, uint64(val)+1, env.stackTopAsUint64())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompiler_compilePick_v128(t *testing.T) {
|
|
const pickTargetLo, pickTargetHi uint64 = 12345, 6789
|
|
|
|
op := operationPtr(wazeroir.NewOperationPick(2, true))
|
|
tests := []struct {
|
|
name string
|
|
isPickTargetOnRegister bool
|
|
}{
|
|
{name: "target on register", isPickTargetOnRegister: false},
|
|
{name: "target on stack", isPickTargetOnRegister: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
// Set up the stack before picking.
|
|
if tc.isPickTargetOnRegister {
|
|
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(pickTargetLo, pickTargetHi)))
|
|
require.NoError(t, err)
|
|
} else {
|
|
lo := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // lo
|
|
lo.valueType = runtimeValueTypeV128Lo
|
|
env.stack()[lo.stackPointer] = pickTargetLo
|
|
hi := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // hi
|
|
hi.valueType = runtimeValueTypeV128Hi
|
|
env.stack()[hi.stackPointer] = pickTargetHi
|
|
}
|
|
|
|
// Push the unused median value.
|
|
_ = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(3), compiler)
|
|
|
|
// Now ready to compile Pick operation.
|
|
err = compiler.compilePick(op)
|
|
require.NoError(t, err)
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(5), compiler)
|
|
|
|
hiLoc := compiler.runtimeValueLocationStack().peek()
|
|
loLoc := compiler.runtimeValueLocationStack().stack[hiLoc.stackPointer-1]
|
|
require.True(t, hiLoc.onRegister())
|
|
require.Equal(t, runtimeValueTypeV128Hi, hiLoc.valueType)
|
|
require.Equal(t, runtimeValueTypeV128Lo, loLoc.valueType)
|
|
|
|
err = compiler.compileReturnFunction()
|
|
require.NoError(t, err)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
// Compile and execute the code under test.
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
env.exec(code.Bytes())
|
|
|
|
// Check the returned status and stack pointer.
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
require.Equal(t, uint64(5), env.stackPointer())
|
|
|
|
// Verify the top value is the picked one and the pick target's value stays the same.
|
|
lo, hi := env.stackTopAsV128()
|
|
require.Equal(t, pickTargetLo, lo)
|
|
require.Equal(t, pickTargetHi, hi)
|
|
require.Equal(t, pickTargetLo, env.stack()[loLoc.stackPointer])
|
|
require.Equal(t, pickTargetHi, env.stack()[hiLoc.stackPointer])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompiler_compilePick(t *testing.T) {
|
|
const pickTargetValue uint64 = 12345
|
|
op := operationPtr(wazeroir.NewOperationPick(1, false))
|
|
tests := []struct {
|
|
name string
|
|
pickTargetSetupFunc func(compiler compilerImpl, ce *callEngine) error
|
|
isPickTargetFloat, isPickTargetOnRegister bool
|
|
}{
|
|
{
|
|
name: "float on register",
|
|
pickTargetSetupFunc: func(compiler compilerImpl, _ *callEngine) error {
|
|
return compiler.compileConstF64(operationPtr(wazeroir.NewOperationConstF64(math.Float64frombits(pickTargetValue))))
|
|
},
|
|
isPickTargetFloat: true,
|
|
isPickTargetOnRegister: true,
|
|
},
|
|
{
|
|
name: "int on register",
|
|
pickTargetSetupFunc: func(compiler compilerImpl, _ *callEngine) error {
|
|
return compiler.compileConstI64(operationPtr(wazeroir.NewOperationConstI64(pickTargetValue)))
|
|
},
|
|
isPickTargetFloat: false,
|
|
isPickTargetOnRegister: true,
|
|
},
|
|
{
|
|
name: "float on stack",
|
|
pickTargetSetupFunc: func(compiler compilerImpl, ce *callEngine) error {
|
|
pickTargetLocation := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
pickTargetLocation.valueType = runtimeValueTypeF64
|
|
ce.stack[pickTargetLocation.stackPointer] = pickTargetValue
|
|
return nil
|
|
},
|
|
isPickTargetFloat: true,
|
|
isPickTargetOnRegister: false,
|
|
},
|
|
{
|
|
name: "int on stack",
|
|
pickTargetSetupFunc: func(compiler compilerImpl, ce *callEngine) error {
|
|
pickTargetLocation := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
pickTargetLocation.valueType = runtimeValueTypeI64
|
|
ce.stack[pickTargetLocation.stackPointer] = pickTargetValue
|
|
return nil
|
|
},
|
|
isPickTargetFloat: false,
|
|
isPickTargetOnRegister: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
// Set up the stack before picking.
|
|
err = tc.pickTargetSetupFunc(compiler, env.callEngine())
|
|
require.NoError(t, err)
|
|
pickTargetLocation := compiler.runtimeValueLocationStack().peek()
|
|
|
|
// Push the unused median value.
|
|
_ = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler)
|
|
|
|
// Now ready to compile Pick operation.
|
|
err = compiler.compilePick(op)
|
|
require.NoError(t, err)
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(3), compiler)
|
|
|
|
pickedLocation := compiler.runtimeValueLocationStack().peek()
|
|
require.True(t, pickedLocation.onRegister())
|
|
require.Equal(t, pickTargetLocation.getRegisterType(), pickedLocation.getRegisterType())
|
|
|
|
err = compiler.compileReturnFunction()
|
|
require.NoError(t, err)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
// Compile and execute the code under test.
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
env.exec(code.Bytes())
|
|
|
|
// Check the returned status and stack pointer.
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
require.Equal(t, uint64(3), env.stackPointer())
|
|
|
|
// Verify the top value is the picked one and the pick target's value stays the same.
|
|
if tc.isPickTargetFloat {
|
|
require.Equal(t, math.Float64frombits(pickTargetValue), env.stackTopAsFloat64())
|
|
require.Equal(t, math.Float64frombits(pickTargetValue), math.Float64frombits(env.stack()[pickTargetLocation.stackPointer]))
|
|
} else {
|
|
require.Equal(t, pickTargetValue, env.stackTopAsUint64())
|
|
require.Equal(t, pickTargetValue, env.stack()[pickTargetLocation.stackPointer])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompiler_compileDrop(t *testing.T) {
|
|
t.Run("range nop", func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
// Put existing contents on stack.
|
|
liveNum := 10
|
|
for i := 0; i < liveNum; i++ {
|
|
compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
}
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(liveNum), compiler)
|
|
|
|
err = compiler.compileDrop(operationPtr(wazeroir.NewOperationDrop(wazeroir.NopInclusiveRange)))
|
|
require.NoError(t, err)
|
|
|
|
// After the nil range drop, the stack must remain the same.
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(liveNum), compiler)
|
|
|
|
err = compiler.compileReturnFunction()
|
|
require.NoError(t, err)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
|
|
env.exec(code.Bytes())
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
})
|
|
t.Run("start top", func(t *testing.T) {
|
|
r := wazeroir.InclusiveRange{Start: 0, End: 2}
|
|
dropTargetNum := int(r.End - r.Start + 1) // +1 as the range is inclusive!
|
|
liveNum := 5
|
|
|
|
env := newCompilerEnvironment()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
// Put existing contents on stack.
|
|
const expectedTopLiveValue = 100
|
|
for i := 0; i < liveNum+dropTargetNum; i++ {
|
|
if i == liveNum-1 {
|
|
err := compiler.compileConstI64(operationPtr(wazeroir.NewOperationConstI64(expectedTopLiveValue)))
|
|
require.NoError(t, err)
|
|
} else {
|
|
compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
}
|
|
}
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(liveNum+dropTargetNum), compiler)
|
|
|
|
err = compiler.compileDrop(operationPtr(wazeroir.NewOperationDrop(r)))
|
|
require.NoError(t, err)
|
|
|
|
// After the drop operation, the stack contains only live contents.
|
|
requireRuntimeLocationStackPointerEqual(t, uint64(liveNum), compiler)
|
|
// Plus, the top value must stay on a register.
|
|
top := compiler.runtimeValueLocationStack().peek()
|
|
require.True(t, top.onRegister())
|
|
|
|
err = compiler.compileReturnFunction()
|
|
require.NoError(t, err)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
|
|
env.exec(code.Bytes())
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
require.Equal(t, uint64(5), env.stackPointer())
|
|
require.Equal(t, uint64(expectedTopLiveValue), env.stackTopAsUint64())
|
|
})
|
|
|
|
t.Run("start from middle", func(t *testing.T) {
|
|
r := wazeroir.InclusiveRange{Start: 2, End: 3}
|
|
liveAboveDropStartNum := 3
|
|
dropTargetNum := int(r.End - r.Start + 1) // +1 as the range is inclusive!
|
|
liveBelowDropEndNum := 5
|
|
total := liveAboveDropStartNum + dropTargetNum + liveBelowDropEndNum
|
|
liveTotal := liveAboveDropStartNum + liveBelowDropEndNum
|
|
|
|
env := newCompilerEnvironment()
|
|
ce := env.callEngine()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
// We don't need call frame in this test case, so simply pop them out!
|
|
for i := 0; i < callFrameDataSizeInUint64; i++ {
|
|
compiler.runtimeValueLocationStack().pop()
|
|
}
|
|
|
|
// Put existing contents except the top on stack
|
|
for i := 0; i < total-1; i++ {
|
|
loc := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
loc.valueType = runtimeValueTypeI32
|
|
ce.stack[loc.stackPointer] = uint64(i) // Put the initial value.
|
|
}
|
|
|
|
// Place the top value.
|
|
const expectedTopLiveValue = 100
|
|
err = compiler.compileConstI64(operationPtr(wazeroir.NewOperationConstI64(expectedTopLiveValue)))
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, uint64(total), compiler.runtimeValueLocationStack().sp)
|
|
|
|
err = compiler.compileDrop(operationPtr(wazeroir.NewOperationDrop(r)))
|
|
require.NoError(t, err)
|
|
|
|
// After the drop operation, the stack contains only live contents.
|
|
require.Equal(t, uint64(liveTotal), compiler.runtimeValueLocationStack().sp)
|
|
// Plus, the top value must stay on a register.
|
|
require.True(t, compiler.runtimeValueLocationStack().peek().onRegister())
|
|
|
|
err = compiler.compileReturnFunction()
|
|
require.NoError(t, err)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
|
|
env.exec(code.Bytes())
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
require.Equal(t, uint64(liveTotal), env.ce.stackPointer)
|
|
|
|
stack := env.stack()[:env.stackPointer()]
|
|
for i, val := range stack {
|
|
if i <= liveBelowDropEndNum {
|
|
require.Equal(t, uint64(i), val)
|
|
} else if i == liveTotal-1 {
|
|
require.Equal(t, uint64(expectedTopLiveValue), val)
|
|
} else {
|
|
require.Equal(t, uint64(i+dropTargetNum), val)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCompiler_compileSelect(t *testing.T) {
|
|
// There are mainly 8 cases we have to test:
|
|
// - [x1 = reg, x2 = reg] select x1
|
|
// - [x1 = reg, x2 = reg] select x2
|
|
// - [x1 = reg, x2 = stack] select x1
|
|
// - [x1 = reg, x2 = stack] select x2
|
|
// - [x1 = stack, x2 = reg] select x1
|
|
// - [x1 = stack, x2 = reg] select x2
|
|
// - [x1 = stack, x2 = stack] select x1
|
|
// - [x1 = stack, x2 = stack] select x2
|
|
// And for each case, we have to test with
|
|
// three conditional value location: stack, gp register, conditional register.
|
|
// So in total we have 24 cases.
|
|
tests := []struct {
|
|
x1OnRegister, x2OnRegister bool
|
|
selectX1 bool
|
|
condlValueOnStack, condValueOnGPRegister, condValueOnCondRegister bool
|
|
}{
|
|
// Conditional value on stack.
|
|
{x1OnRegister: true, x2OnRegister: true, selectX1: true, condlValueOnStack: true},
|
|
{x1OnRegister: true, x2OnRegister: true, selectX1: false, condlValueOnStack: true},
|
|
{x1OnRegister: true, x2OnRegister: false, selectX1: true, condlValueOnStack: true},
|
|
{x1OnRegister: true, x2OnRegister: false, selectX1: false, condlValueOnStack: true},
|
|
{x1OnRegister: false, x2OnRegister: true, selectX1: true, condlValueOnStack: true},
|
|
{x1OnRegister: false, x2OnRegister: true, selectX1: false, condlValueOnStack: true},
|
|
{x1OnRegister: false, x2OnRegister: false, selectX1: true, condlValueOnStack: true},
|
|
{x1OnRegister: false, x2OnRegister: false, selectX1: false, condlValueOnStack: true},
|
|
// Conditional value on register.
|
|
{x1OnRegister: true, x2OnRegister: true, selectX1: true, condValueOnGPRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: true, selectX1: false, condValueOnGPRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: false, selectX1: true, condValueOnGPRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: false, selectX1: false, condValueOnGPRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: true, selectX1: true, condValueOnGPRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: true, selectX1: false, condValueOnGPRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: false, selectX1: true, condValueOnGPRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: false, selectX1: false, condValueOnGPRegister: true},
|
|
// Conditional value on conditional register.
|
|
{x1OnRegister: true, x2OnRegister: true, selectX1: true, condValueOnCondRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: true, selectX1: false, condValueOnCondRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: false, selectX1: true, condValueOnCondRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: false, selectX1: false, condValueOnCondRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: true, selectX1: true, condValueOnCondRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: true, selectX1: false, condValueOnCondRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: false, selectX1: true, condValueOnCondRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: false, selectX1: false, condValueOnCondRegister: true},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
tc := tt
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
for _, vals := range [][2]uint64{
|
|
{1, 2},
|
|
{0, 1},
|
|
{1, 0},
|
|
{math.Float64bits(-1), math.Float64bits(-1)},
|
|
{math.Float64bits(-1), math.Float64bits(1)},
|
|
{math.Float64bits(1), math.Float64bits(-1)},
|
|
} {
|
|
x1Value, x2Value := vals[0], vals[1]
|
|
t.Run(fmt.Sprintf("x1=0x%x,x2=0x%x", vals[0], vals[1]), func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
|
|
// To make the assertion below stable, we preallocate the underlying stack,
|
|
// so that the pointer to the entry will be stale.
|
|
compiler.runtimeValueLocationStack().stack = make([]runtimeValueLocation, 100)
|
|
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
x1 := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
x1.valueType = runtimeValueTypeI64
|
|
env.stack()[x1.stackPointer] = x1Value
|
|
if tc.x1OnRegister {
|
|
err = compiler.compileEnsureOnRegister(x1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
x2 := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
x2.valueType = runtimeValueTypeI64
|
|
env.stack()[x2.stackPointer] = x2Value
|
|
if tc.x2OnRegister {
|
|
err = compiler.compileEnsureOnRegister(x2)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
var c *runtimeValueLocation
|
|
if tc.condlValueOnStack {
|
|
c = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
c.valueType = runtimeValueTypeI32
|
|
if tc.selectX1 {
|
|
env.stack()[c.stackPointer] = 1
|
|
} else {
|
|
env.stack()[c.stackPointer] = 0
|
|
}
|
|
} else if tc.condValueOnGPRegister {
|
|
c = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
c.valueType = runtimeValueTypeI32
|
|
if tc.selectX1 {
|
|
env.stack()[c.stackPointer] = 1
|
|
} else {
|
|
env.stack()[c.stackPointer] = 0
|
|
}
|
|
err = compiler.compileEnsureOnRegister(c)
|
|
require.NoError(t, err)
|
|
} else if tc.condValueOnCondRegister {
|
|
err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(0)))
|
|
require.NoError(t, err)
|
|
err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(0)))
|
|
require.NoError(t, err)
|
|
if tc.selectX1 {
|
|
err = compiler.compileEq(operationPtr(wazeroir.NewOperationEq(wazeroir.UnsignedTypeI32)))
|
|
} else {
|
|
err = compiler.compileNe(operationPtr(wazeroir.NewOperationNe(wazeroir.UnsignedTypeI32)))
|
|
}
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Now emit code for select.
|
|
err = compiler.compileSelect(operationPtr(wazeroir.NewOperationSelect(false)))
|
|
require.NoError(t, err)
|
|
|
|
// x1 should be top of the stack.
|
|
require.Equal(t, x1, compiler.runtimeValueLocationStack().peek())
|
|
|
|
err = compiler.compileReturnFunction()
|
|
require.NoError(t, err)
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
// Run code.
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
env.exec(code.Bytes())
|
|
|
|
// Check the selected value.
|
|
require.Equal(t, uint64(1), env.stackPointer())
|
|
if tc.selectX1 {
|
|
require.Equal(t, env.stack()[x1.stackPointer], x1Value)
|
|
} else {
|
|
require.Equal(t, env.stack()[x1.stackPointer], x2Value)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompiler_compileSwap_v128(t *testing.T) {
|
|
const x1Lo, x1Hi uint64 = 100000, 200000
|
|
const x2Lo, x2Hi uint64 = 1, 2
|
|
|
|
tests := []struct {
|
|
x1OnRegister, x2OnRegister bool
|
|
}{
|
|
{x1OnRegister: true, x2OnRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: false},
|
|
{x1OnRegister: false, x2OnRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(fmt.Sprintf("x1_register=%v, x2_register=%v", tc.x1OnRegister, tc.x2OnRegister), func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
if tc.x1OnRegister {
|
|
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(x1Lo, x1Hi)))
|
|
require.NoError(t, err)
|
|
} else {
|
|
lo := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // lo
|
|
lo.valueType = runtimeValueTypeV128Lo
|
|
env.stack()[lo.stackPointer] = x1Lo
|
|
hi := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // hi
|
|
hi.valueType = runtimeValueTypeV128Hi
|
|
env.stack()[hi.stackPointer] = x1Hi
|
|
}
|
|
|
|
_ = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // Dummy value!
|
|
|
|
if tc.x2OnRegister {
|
|
err = compiler.compileV128Const(operationPtr(wazeroir.NewOperationV128Const(x2Lo, x2Hi)))
|
|
require.NoError(t, err)
|
|
} else {
|
|
lo := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // lo
|
|
lo.valueType = runtimeValueTypeV128Lo
|
|
env.stack()[lo.stackPointer] = x2Lo
|
|
hi := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // hi
|
|
hi.valueType = runtimeValueTypeV128Hi
|
|
env.stack()[hi.stackPointer] = x2Hi
|
|
}
|
|
|
|
// Swap x1 and x2.
|
|
err = compiler.compileSet(operationPtr(wazeroir.NewOperationSet(4, true)))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, compiler.compileReturnFunction())
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
// Generate the code under test.
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
|
|
// Run code.
|
|
env.exec(code.Bytes())
|
|
|
|
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
|
|
require.Equal(t, uint64(3), env.stackPointer())
|
|
|
|
// The first variable is above the call frame.
|
|
st := env.stack()
|
|
require.Equal(t, x2Lo, st[callFrameDataSizeInUint64])
|
|
require.Equal(t, x2Hi, st[callFrameDataSizeInUint64+1])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompiler_compileSet(t *testing.T) {
|
|
var x1Value, x2Value int64 = 100, 200
|
|
tests := []struct {
|
|
x1OnConditionalRegister, x1OnRegister, x2OnRegister bool
|
|
}{
|
|
{x1OnRegister: true, x2OnRegister: true},
|
|
{x1OnRegister: true, x2OnRegister: false},
|
|
{x1OnRegister: false, x2OnRegister: true},
|
|
{x1OnRegister: false, x2OnRegister: false},
|
|
// x1 on conditional register
|
|
{x1OnConditionalRegister: true, x2OnRegister: false},
|
|
{x1OnConditionalRegister: true, x2OnRegister: true},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
tc := tt
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
env := newCompilerEnvironment()
|
|
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
|
|
err := compiler.compilePreamble()
|
|
require.NoError(t, err)
|
|
|
|
x2 := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
x2.valueType = runtimeValueTypeI32
|
|
env.stack()[x2.stackPointer] = uint64(x2Value)
|
|
if tc.x2OnRegister {
|
|
err = compiler.compileEnsureOnRegister(x2)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_ = compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack() // Dummy value!
|
|
if tc.x1OnRegister && !tc.x1OnConditionalRegister {
|
|
x1 := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
x1.valueType = runtimeValueTypeI32
|
|
env.stack()[x1.stackPointer] = uint64(x1Value)
|
|
err = compiler.compileEnsureOnRegister(x1)
|
|
require.NoError(t, err)
|
|
} else if !tc.x1OnConditionalRegister {
|
|
x1 := compiler.runtimeValueLocationStack().pushRuntimeValueLocationOnStack()
|
|
x1.valueType = runtimeValueTypeI32
|
|
env.stack()[x1.stackPointer] = uint64(x1Value)
|
|
} else {
|
|
err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(0)))
|
|
require.NoError(t, err)
|
|
err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(0)))
|
|
require.NoError(t, err)
|
|
err = compiler.compileEq(operationPtr(wazeroir.NewOperationEq(wazeroir.UnsignedTypeI32)))
|
|
require.NoError(t, err)
|
|
x1Value = 1
|
|
}
|
|
|
|
// Set x2 into the x1.
|
|
err = compiler.compileSet(operationPtr(wazeroir.NewOperationSet(2, false)))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, compiler.compileReturnFunction())
|
|
|
|
code := asm.CodeSegment{}
|
|
defer func() { require.NoError(t, code.Unmap()) }()
|
|
|
|
// Generate the code under test.
|
|
_, err = compiler.compile(code.NextCodeSection())
|
|
require.NoError(t, err)
|
|
|
|
// Run code.
|
|
env.exec(code.Bytes())
|
|
|
|
require.Equal(t, uint64(2), env.stackPointer())
|
|
// Check the value was set. Note that it is placed above the call frame.
|
|
require.Equal(t, uint64(x1Value), env.stack()[callFrameDataSizeInUint64])
|
|
})
|
|
}
|
|
}
|