Files
wazero/internal/engine/compiler/impl_arm64_test.go
2023-06-19 19:18:18 +10:00

294 lines
10 KiB
Go

package compiler
import (
"testing"
"unsafe"
"github.com/tetratelabs/wazero/internal/asm"
arm64 "github.com/tetratelabs/wazero/internal/asm/arm64"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wazeroir"
)
// TestArm64Compiler_indirectCallWithTargetOnCallingConvReg is the regression test for #526.
// In short, the offset register for call_indirect might be the same as arm64CallingConventionModuleInstanceAddressRegister
// and that must not be a failure.
func TestArm64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
env := newCompilerEnvironment()
table := make([]wasm.Reference, 1)
env.addTable(&wasm.TableInstance{References: table})
// Ensure that the module instance has the type information for targetOperation.TypeIndex,
// and the typeID matches the table[targetOffset]'s type ID.
operation := operationPtr(wazeroir.NewOperationCallIndirect(0, 0))
env.module().TypeIDs = []wasm.FunctionTypeID{0}
env.module().Engine = &moduleEngine{functions: []function{}}
me := env.moduleEngine()
{ // Compiling call target.
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil)
err := compiler.compilePreamble()
require.NoError(t, err)
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)
executable := code.Bytes()
makeExecutable(executable)
f := function{
parent: &compiledFunction{parent: &compiledModule{executable: code}},
codeInitialAddress: code.Addr(),
moduleInstance: env.moduleInstance,
}
me.functions = append(me.functions, f)
table[0] = uintptr(unsafe.Pointer(&f))
}
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{
Types: []wasm.FunctionType{{}},
HasTable: true,
}).(*arm64Compiler)
err := compiler.compilePreamble()
require.NoError(t, err)
// Place the offset into the calling-convention reserved register.
offsetLoc := compiler.pushRuntimeValueLocationOnRegister(arm64CallingConventionModuleInstanceAddressRegister,
runtimeValueTypeI32)
compiler.assembler.CompileConstToRegister(arm64.MOVD, 0, offsetLoc.register)
require.NoError(t, compiler.compileCallIndirect(operation))
err = compiler.compileReturnFunction()
require.NoError(t, err)
code := asm.CodeSegment{}
defer func() { require.NoError(t, code.Unmap()) }()
// Generate the code under test and run.
_, err = compiler.compile(code.NextCodeSection())
require.NoError(t, err)
env.exec(code.Bytes())
}
func TestArm64Compiler_readInstructionAddress(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newArm64Compiler, nil).(*arm64Compiler)
err := compiler.compilePreamble()
require.NoError(t, err)
// Set the acquisition target instruction to the one after RET,
// and read the absolute address into destinationRegister.
const addressReg = arm64ReservedRegisterForTemporary
compiler.assembler.CompileReadInstructionAddress(addressReg, arm64.RET)
// Branch to the instruction after RET below via the absolute
// address stored in destinationRegister.
compiler.assembler.CompileJumpToRegister(arm64.B, addressReg)
// If we fail to branch, we reach here and exit with unreachable status,
// so the assertion would fail.
compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable)
// This could be the read instruction target as this is the
// right after RET. Therefore, the branch instruction above
// must target here.
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())
}
func TestArm64Compiler_label(t *testing.T) {
c := &arm64Compiler{}
c.label(wazeroir.NewLabel(wazeroir.LabelKindContinuation, 100))
require.Equal(t, 100, c.frameIDMax)
require.Equal(t, 101, len(c.labels[wazeroir.LabelKindContinuation]))
// frameIDMax is for all LabelKind, so this shouldn't change frameIDMax.
c.label(wazeroir.NewLabel(wazeroir.LabelKindHeader, 2))
require.Equal(t, 100, c.frameIDMax)
require.Equal(t, 3, len(c.labels[wazeroir.LabelKindHeader]))
}
func TestArm64Compiler_Init(t *testing.T) {
c := &arm64Compiler{
locationStackForEntrypoint: newRuntimeValueLocationStack(),
assembler: arm64.NewAssembler(0),
}
const stackCap = 12345
c.locationStackForEntrypoint.stack = make([]runtimeValueLocation, stackCap)
c.locationStackForEntrypoint.sp = 5555
c.Init(&wasm.FunctionType{}, nil, false)
// locationStack is the pointer to locationStackForEntrypoint after init.
require.Equal(t, c.locationStack, &c.locationStackForEntrypoint)
// And the underlying stack must be reused (the capacity preserved).
require.Equal(t, stackCap, cap(c.locationStack.stack))
require.Equal(t, stackCap, cap(c.locationStackForEntrypoint.stack))
}
func TestArm64Compiler_resetLabels(t *testing.T) {
c := newArm64Compiler().(*arm64Compiler)
nop := c.compileNOP()
const (
frameIDMax = 50
capacity = 12345
)
c.frameIDMax = frameIDMax
for i := range c.labels {
ifs := make([]arm64LabelInfo, frameIDMax*2)
c.labels[i] = ifs
for j := 0; j <= frameIDMax; j++ {
ifs[j].stackInitialized = true
ifs[j].initialInstruction = nop
ifs[j].initialStack = newRuntimeValueLocationStack()
ifs[j].initialStack.sp = 5555 // should be cleared via runtimeLocationStack.Reset().
ifs[j].initialStack.stack = make([]runtimeValueLocation, 0, capacity)
}
}
c.resetLabels()
for i := range c.labels {
for j := 0; j < len(c.labels[i]); j++ {
l := &c.labels[i][j]
require.False(t, l.stackInitialized)
require.Nil(t, l.initialInstruction)
require.Equal(t, 0, len(l.initialStack.stack))
if j > frameIDMax {
require.Equal(t, 0, cap(l.initialStack.stack))
} else {
require.Equal(t, capacity, cap(l.initialStack.stack))
}
require.Equal(t, uint64(0), l.initialStack.sp)
}
}
}
func TestArm64Compiler_getSavedTemporaryLocationStack(t *testing.T) {
t.Run("len(brTableTmp)<len(current)", func(t *testing.T) {
st := newRuntimeValueLocationStack()
c := &arm64Compiler{locationStack: &st}
c.locationStack.sp = 3
c.locationStack.stack = []runtimeValueLocation{{stackPointer: 150}, {stackPointer: 200}, {stackPointer: 300}}
actual := c.getSavedTemporaryLocationStack()
require.Equal(t, uint64(3), actual.sp)
require.Equal(t, 3, len(actual.stack))
require.Equal(t, c.locationStack.stack[:3], actual.stack)
})
t.Run("len(brTableTmp)==len(current)", func(t *testing.T) {
st := newRuntimeValueLocationStack()
c := &arm64Compiler{locationStack: &st, brTableTmp: make([]runtimeValueLocation, 3)}
initSlicePtr := &c.brTableTmp
c.locationStack.sp = 3
c.locationStack.stack = []runtimeValueLocation{{stackPointer: 150}, {stackPointer: 200}, {stackPointer: 300}}
actual := c.getSavedTemporaryLocationStack()
require.Equal(t, uint64(3), actual.sp)
require.Equal(t, 3, len(actual.stack))
require.Equal(t, c.locationStack.stack[:3], actual.stack)
// The underlying temporary slice shouldn't be changed.
require.Equal(t, initSlicePtr, &c.brTableTmp)
})
t.Run("len(brTableTmp)>len(current)", func(t *testing.T) {
const temporarySliceSize = 100
st := newRuntimeValueLocationStack()
c := &arm64Compiler{locationStack: &st, brTableTmp: make([]runtimeValueLocation, temporarySliceSize)}
c.locationStack.sp = 3
c.locationStack.stack = []runtimeValueLocation{
{stackPointer: 150},
{stackPointer: 200},
{stackPointer: 300},
{},
{},
{},
{},
{stackPointer: 1231455}, // Entries here shouldn't be copied as they are avobe sp.
}
actual := c.getSavedTemporaryLocationStack()
require.Equal(t, uint64(3), actual.sp)
require.Equal(t, temporarySliceSize, len(actual.stack))
require.Equal(t, c.locationStack.stack[:3], actual.stack[:3])
for i := int(actual.sp); i < len(actual.stack); i++ {
// Above the stack pointer, the values must not be copied.
require.Zero(t, actual.stack[i].stackPointer)
}
})
}
// https://github.com/tetratelabs/wazero/issues/1522
func TestArm64Compiler_LargeTrapOffsets(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{
Types: []wasm.FunctionType{{}},
})
err := compiler.compilePreamble()
require.NoError(t, err)
one := operationPtr(wazeroir.NewOperationConstI32(uint32(1)))
five := operationPtr(wazeroir.NewOperationConstI32(uint32(5)))
div := operationPtr(wazeroir.NewOperationDiv(wazeroir.SignedTypeInt32))
// Place the offset value.
err = compiler.compileConstI32(one)
require.NoError(t, err)
// Repeat enough times that jump labels are not within (-524288, 524287).
// Relative offset -2097164/4(=-524291).
// At the time of writing, 52429 is empirically the value that starts
// triggering the bug on arm64. We impose an arbitrarily higher value
// to account for possible future improvement to the number of instructions
// we emit.
for i := 0; i < 80_000; i++ {
err = compiler.compileConstI32(five)
require.NoError(t, err)
err = compiler.compileDiv(div)
require.NoError(t, err)
}
err = compiler.compileReturnFunction()
require.NoError(t, err)
code := asm.CodeSegment{}
defer func() { require.NoError(t, code.Unmap()) }()
// Generate the code under test and run.
_, err = compiler.compile(code.NextCodeSection())
require.NoError(t, err)
env.exec(code.Bytes())
require.Equal(t, nativeCallStatusCodeReturned.String(), env.compilerStatus().String())
}
// compile implements compilerImpl.setStackPointerCeil for the amd64 architecture.
func (c *arm64Compiler) setStackPointerCeil(v uint64) {
c.stackPointerCeil = v
}
// compile implements compilerImpl.setRuntimeValueLocationStack for the amd64 architecture.
func (c *arm64Compiler) setRuntimeValueLocationStack(s *runtimeValueLocationStack) {
c.locationStack = s
}