package compiler import ( "fmt" "testing" "unsafe" "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_compileHostFunction(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil) err := compiler.compileGoDefinedHostFunction() require.NoError(t, err) // Get the location of caller function's location stored in the stack, which depends on the type. // In this test, the host function has empty sig. _, _, callerFuncLoc := compiler.runtimeValueLocationStack().getCallFrameLocations(&wasm.FunctionType{}) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the machine code for the test. _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // Set the caller's function which always exists in the real usecase. f := &function{moduleInstance: &wasm.ModuleInstance{}} env.stack()[callerFuncLoc.stackPointer] = uint64(uintptr(unsafe.Pointer(f))) env.exec(code.Bytes()) // On the return, the code must exit with the host call status. require.Equal(t, nativeCallStatusCodeCallGoHostFunction, env.compilerStatus()) // Plus, the exitContext holds the caller's wasm.FunctionInstance. require.Equal(t, f.moduleInstance, env.ce.exitContext.callerModuleInstance) // Re-enter the return address. require.NotEqual(t, uintptr(0), uintptr(env.ce.returnAddress)) nativecall(env.ce.returnAddress, env.callEngine(), env.module()) // After that, the code must exit with returned status. require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) } func TestCompiler_compileLabel(t *testing.T) { label := wazeroir.NewLabel(wazeroir.LabelKindContinuation, 100) for _, expectSkip := range []bool{false, true} { expectSkip := expectSkip t.Run(fmt.Sprintf("expect skip=%v", expectSkip), func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil) if expectSkip { // If the initial stack is not set, compileLabel must return skip=true. actual := compiler.compileLabel(operationPtr(wazeroir.NewOperationLabel(label))) require.True(t, actual) } else { err := compiler.compileBr(operationPtr(wazeroir.NewOperationBr(label))) require.NoError(t, err) actual := compiler.compileLabel(operationPtr(wazeroir.NewOperationLabel(label))) require.False(t, actual) } }) } } func TestCompiler_compileBrIf(t *testing.T) { unreachableStatus, thenLabelExitStatus, elseLabelExitStatus := nativeCallStatusCodeUnreachable, nativeCallStatusCodeUnreachable+1, nativeCallStatusCodeUnreachable+2 thenBranchTarget := wazeroir.NewLabel(wazeroir.LabelKindHeader, 1) elseBranchTarget := wazeroir.NewLabel(wazeroir.LabelKindHeader, 2) tests := []struct { name string setupFunc func(t *testing.T, compiler compilerImpl, shouldGoElse bool) }{ { name: "cond on register", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { val := uint32(1) if shouldGoElse { val = 0 } err := compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(val))) require.NoError(t, err) }, }, { name: "LS", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(1), uint32(2) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Le on unsigned integer produces the value on COND_LS register. err := compiler.compileLe(operationPtr(wazeroir.NewOperationLe(wazeroir.SignedTypeUint32))) require.NoError(t, err) }, }, { name: "LE", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(1), uint32(2) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Le on signed integer produces the value on COND_LE register. err := compiler.compileLe(operationPtr(wazeroir.NewOperationLe(wazeroir.SignedTypeInt32))) require.NoError(t, err) }, }, { name: "HS", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(2), uint32(1) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Ge on unsigned integer produces the value on COND_HS register. err := compiler.compileGe(operationPtr(wazeroir.NewOperationGe(wazeroir.SignedTypeUint32))) require.NoError(t, err) }, }, { name: "GE", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(2), uint32(1) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Ge on signed integer produces the value on COND_GE register. err := compiler.compileGe(operationPtr(wazeroir.NewOperationGe(wazeroir.SignedTypeInt32))) require.NoError(t, err) }, }, { name: "HI", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(2), uint32(1) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Gt on unsigned integer produces the value on COND_HI register. err := compiler.compileGt(operationPtr(wazeroir.NewOperationGt(wazeroir.SignedTypeUint32))) require.NoError(t, err) }, }, { name: "GT", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(2), uint32(1) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Gt on signed integer produces the value on COND_GT register. err := compiler.compileGt(operationPtr(wazeroir.NewOperationGt(wazeroir.SignedTypeInt32))) require.NoError(t, err) }, }, { name: "LO", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(1), uint32(2) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Lt on unsigned integer produces the value on COND_LO register. err := compiler.compileLt(operationPtr(wazeroir.NewOperationLt(wazeroir.SignedTypeUint32))) require.NoError(t, err) }, }, { name: "LT", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(1), uint32(2) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoInt32Consts(t, x1, x2, compiler) // Lt on signed integer produces the value on COND_LT register. err := compiler.compileLt(operationPtr(wazeroir.NewOperationLt(wazeroir.SignedTypeInt32))) require.NoError(t, err) }, }, { name: "MI", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := float32(1), float32(2) if shouldGoElse { x2, x1 = x1, x2 } requirePushTwoFloat32Consts(t, x1, x2, compiler) // Lt on floats produces the value on COND_MI register. err := compiler.compileLt(operationPtr(wazeroir.NewOperationLt(wazeroir.SignedTypeFloat32))) require.NoError(t, err) }, }, { name: "EQ", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(1), uint32(1) if shouldGoElse { x2++ } requirePushTwoInt32Consts(t, x1, x2, compiler) err := compiler.compileEq(operationPtr(wazeroir.NewOperationEq(wazeroir.UnsignedTypeI32))) require.NoError(t, err) }, }, { name: "NE", setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { x1, x2 := uint32(1), uint32(2) if shouldGoElse { x2 = x1 } requirePushTwoInt32Consts(t, x1, x2, compiler) err := compiler.compileNe(operationPtr(wazeroir.NewOperationNe(wazeroir.UnsignedTypeI32))) require.NoError(t, err) }, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { for _, shouldGoToElse := range []bool{false, true} { shouldGoToElse := shouldGoToElse t.Run(fmt.Sprintf("should_goto_else=%v", shouldGoToElse), func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) tc.setupFunc(t, compiler, shouldGoToElse) requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler) err = compiler.compileBrIf(operationPtr(wazeroir.NewOperationBrIf(thenBranchTarget, elseBranchTarget, wazeroir.NopInclusiveRange))) require.NoError(t, err) compiler.compileExitFromNativeCode(unreachableStatus) // Emit code for .then label. skip := compiler.compileLabel(operationPtr(wazeroir.NewOperationLabel(thenBranchTarget))) require.False(t, skip) compiler.compileExitFromNativeCode(thenLabelExitStatus) // Emit code for .else label. skip = compiler.compileLabel(operationPtr(wazeroir.NewOperationLabel(elseBranchTarget))) require.False(t, skip) compiler.compileExitFromNativeCode(elseLabelExitStatus) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // The generated code looks like this: // // ... code from compilePreamble() // ... code from tc.setupFunc() // br_if .then, .else // exit $unreachableStatus // .then: // exit $thenLabelExitStatus // .else: // exit $elseLabelExitStatus // // Therefore, if we start executing from the top, we must end up exiting with an appropriate status. env.exec(code.Bytes()) require.NotEqual(t, unreachableStatus, env.compilerStatus()) if shouldGoToElse { require.Equal(t, elseLabelExitStatus, env.compilerStatus()) } else { require.Equal(t, thenLabelExitStatus, env.compilerStatus()) } }) } }) } } func TestCompiler_compileBrTable(t *testing.T) { requireRunAndExpectedValueReturned := func(t *testing.T, env *compilerEnv, c compilerImpl, expValue uint32) { // Emit code for each label which returns the frame ID. for returnValue := uint32(0); returnValue < 7; returnValue++ { label := wazeroir.NewLabel(wazeroir.LabelKindHeader, returnValue) err := c.compileBr(operationPtr(wazeroir.NewOperationBr(label))) require.NoError(t, err) _ = c.compileLabel(operationPtr(wazeroir.NewOperationLabel(label))) _ = c.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(uint32(label.FrameID())))) err = c.compileReturnFunction() require.NoError(t, err) } code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Generate the code under test and run. _, err := c.compile(code.NextCodeSection()) require.NoError(t, err) env.exec(code.Bytes()) // Check the returned value. require.Equal(t, uint64(1), env.stackPointer()) require.Equal(t, expValue, env.stackTopAsUint32()) } getBranchLabelFromFrameID := func(frameid uint32) uint64 { return uint64(wazeroir.NewLabel(wazeroir.LabelKindHeader, frameid)) } tests := []struct { name string index int64 o *wazeroir.UnionOperation expectedValue uint32 }{ { name: "only default with index 0", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(6), wazeroir.NopInclusiveRange.AsU64(), })), index: 0, expectedValue: 6, }, { name: "only default with index 100", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(6), wazeroir.NopInclusiveRange.AsU64(), })), index: 100, expectedValue: 6, }, { name: "select default with targets and good index", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(1), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(2), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(6), // default wazeroir.NopInclusiveRange.AsU64(), })), index: 3, expectedValue: 6, }, { name: "select default with targets and huge index", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(1), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(2), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(6), // default wazeroir.NopInclusiveRange.AsU64(), }, )), index: 100000, expectedValue: 6, }, { name: "select first with two targets", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(1), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(2), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(5), // default wazeroir.NopInclusiveRange.AsU64(), })), index: 0, expectedValue: 1, }, { name: "select last with two targets", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(1), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(2), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(6), // default wazeroir.NopInclusiveRange.AsU64(), })), index: 1, expectedValue: 2, }, { name: "select first with five targets", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(1), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(2), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(3), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(4), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(5), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(5), // default wazeroir.NopInclusiveRange.AsU64(), })), index: 0, expectedValue: 1, }, { name: "select middle with five targets", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(1), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(2), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(3), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(4), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(5), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(5), // default wazeroir.NopInclusiveRange.AsU64(), })), index: 2, expectedValue: 3, }, { name: "select last with five targets", o: operationPtr(wazeroir.NewOperationBrTable([]uint64{ getBranchLabelFromFrameID(1), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(2), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(3), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(4), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(5), wazeroir.NopInclusiveRange.AsU64(), getBranchLabelFromFrameID(5), // default wazeroir.NopInclusiveRange.AsU64(), })), index: 4, expectedValue: 5, }, } 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) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(uint32(tc.index)))) require.NoError(t, err) err = compiler.compileBrTable(tc.o) require.NoError(t, err) require.Zero(t, len(compiler.runtimeValueLocationStack().usedRegisters.list())) requireRunAndExpectedValueReturned(t, env, compiler, tc.expectedValue) }) } } func requirePushTwoInt32Consts(t *testing.T, x1, x2 uint32, compiler compilerImpl) { err := compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(x1))) require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(x2))) require.NoError(t, err) } func requirePushTwoFloat32Consts(t *testing.T, x1, x2 float32, compiler compilerImpl) { err := compiler.compileConstF32(operationPtr(wazeroir.NewOperationConstF32(x1))) require.NoError(t, err) err = compiler.compileConstF32(operationPtr(wazeroir.NewOperationConstF32(x2))) require.NoError(t, err) } func TestCompiler_compileBr(t *testing.T) { t.Run("return", func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) // Branch into nil label is interpreted as return. See BranchTarget.IsReturnTarget err = compiler.compileBr(operationPtr(wazeroir.NewOperationBr(wazeroir.NewLabel(wazeroir.LabelKindReturn, 0)))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() // Compile and execute the code under test. // Note: we don't invoke "compiler.return()" as the code emitted by compilerBr is enough to exit. _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) env.exec(code.Bytes()) require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) }) t.Run("back-and-forth br", func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, nil) err := compiler.compilePreamble() require.NoError(t, err) // Emit the forward br, meaning that handle Br instruction where the target label hasn't been compiled yet. forwardLabel := wazeroir.NewLabel(wazeroir.LabelKindHeader, 0) err = compiler.compileBr(operationPtr(wazeroir.NewOperationBr(forwardLabel))) require.NoError(t, err) // We must not reach the code after Br, so emit the code exiting with Unreachable status. compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) require.NoError(t, err) exitLabel := wazeroir.NewLabel(wazeroir.LabelKindHeader, 1) err = compiler.compileBr(operationPtr(wazeroir.NewOperationBr(exitLabel))) require.NoError(t, err) // Emit code for the exitLabel. skip := compiler.compileLabel(operationPtr(wazeroir.NewOperationLabel(exitLabel))) require.False(t, skip) compiler.compileExitFromNativeCode(nativeCallStatusCodeReturned) require.NoError(t, err) // Emit code for the forwardLabel. skip = compiler.compileLabel(operationPtr(wazeroir.NewOperationLabel(forwardLabel))) require.False(t, skip) err = compiler.compileBr(operationPtr(wazeroir.NewOperationBr(exitLabel))) require.NoError(t, err) code := asm.CodeSegment{} defer func() { require.NoError(t, code.Unmap()) }() _, err = compiler.compile(code.NextCodeSection()) require.NoError(t, err) // The generated code looks like this:) // // ... code from compilePreamble() // br .forwardLabel // exit nativeCallStatusCodeUnreachable // must not be reached // br .exitLabel // must not be reached // .exitLabel: // exit nativeCallStatusCodeReturned // .forwardLabel: // br .exitLabel // // Therefore, if we start executing from the top, we must end up exiting nativeCallStatusCodeReturned. env.exec(code.Bytes()) require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) }) } func TestCompiler_compileCallIndirect(t *testing.T) { t.Run("out of bounds", func(t *testing.T) { env := newCompilerEnvironment() env.addTable(&wasm.TableInstance{References: make([]wasm.Reference, 10)}) compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ Types: []wasm.FunctionType{{}}, HasTable: true, }) err := compiler.compilePreamble() require.NoError(t, err) targetOperation := operationPtr(wazeroir.NewOperationCallIndirect(0, 0)) // Place the offset value. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(10))) require.NoError(t, err) err = compiler.compileCallIndirect(targetOperation) require.NoError(t, err) // We expect to exit from the code in callIndirect so the subsequent code must be unreachable. compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) 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, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) }) t.Run("uninitialized", func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ Types: []wasm.FunctionType{{}}, HasTable: true, }) err := compiler.compilePreamble() require.NoError(t, err) targetOperation := operationPtr(wazeroir.NewOperationCallIndirect(0, 0)) targetOffset := operationPtr(wazeroir.NewOperationConstI32(uint32(0))) // and the typeID doesn't match the table[targetOffset]'s type ID. table := make([]wasm.Reference, 10) env.addTable(&wasm.TableInstance{References: table}) env.module().TypeIDs = make([]wasm.FunctionTypeID, 10) // Place the offset value. err = compiler.compileConstI32(targetOffset) require.NoError(t, err) err = compiler.compileCallIndirect(targetOperation) require.NoError(t, err) // We expect to exit from the code in callIndirect so the subsequent code must be unreachable. compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) 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, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) }) t.Run("type not match", func(t *testing.T) { env := newCompilerEnvironment() compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ Types: []wasm.FunctionType{{}}, HasTable: true, }) err := compiler.compilePreamble() require.NoError(t, err) targetOperation := operationPtr(wazeroir.NewOperationCallIndirect(0, 0)) targetOffset := operationPtr(wazeroir.NewOperationConstI32(uint32(0))) env.module().TypeIDs = []wasm.FunctionTypeID{1000} // Ensure that the module instance has the type information for targetOperation.TypeIndex, // and the typeID doesn't match the table[targetOffset]'s type ID. table := make([]wasm.Reference, 10) env.addTable(&wasm.TableInstance{References: table}) cf := &function{typeID: 50} table[0] = uintptr(unsafe.Pointer(cf)) // Place the offset value. err = compiler.compileConstI32(targetOffset) require.NoError(t, err) // Now emit the code. require.NoError(t, compiler.compileCallIndirect(targetOperation)) // We expect to exit from the code in callIndirect so the subsequent code must be unreachable. compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) 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, nativeCallStatusCodeTypeMismatchOnIndirectCall.String(), env.compilerStatus().String()) }) t.Run("ok", func(t *testing.T) { targetType := wasm.FunctionType{ Results: []wasm.ValueType{wasm.ValueTypeI32}, ResultNumInUint64: 1, } const typeIndex = 0 targetTypeID := wasm.FunctionTypeID(10) operation := operationPtr(wazeroir.NewOperationCallIndirect(typeIndex, 0)) table := make([]wasm.Reference, 10) env := newCompilerEnvironment() 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. env.module().TypeIDs = make([]wasm.FunctionTypeID, 100) env.module().TypeIDs[typeIndex] = targetTypeID env.module().Engine = &moduleEngine{functions: []function{}} me := env.moduleEngine() me.functions = make([]function, len(table)) for i := 0; i < len(table); i++ { // First, we create the call target function for the table element i. // To match its function type, it must return one value. expectedReturnValue := uint32(i * 1000) compiler := env.requireNewCompiler(t, &targetType, newCompiler, &wazeroir.CompilationResult{}) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(expectedReturnValue))) require.NoError(t, err) requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler) // The function result value must be set at the bottom of the stack. err = compiler.compileSet(operationPtr(wazeroir.NewOperationSet(int(compiler.runtimeValueLocationStack().sp-1), false))) 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) makeExecutable(code.Bytes()) // Now that we've generated the code for this function, // add it to the module engine and assign its pointer to the table index. me.functions[i] = function{ codeInitialAddress: uintptr(unsafe.Pointer(&code.Bytes()[0])), moduleInstance: env.moduleInstance, typeID: targetTypeID, } table[i] = uintptr(unsafe.Pointer(&me.functions[i])) } // Test to ensure that we can call all the functions stored in the table. for i := 1; i < len(table); i++ { expectedReturnValue := uint32(i * 1000) t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ Types: []wasm.FunctionType{targetType}, HasTable: true, }, ) err := compiler.compilePreamble() require.NoError(t, err) // Place the offset value. Here we try calling a function of functionaddr == table[i].FunctionIndex. err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(uint32(i)))) require.NoError(t, err) // At this point, we should have one item (offset value) on the stack. requireRuntimeLocationStackPointerEqual(t, 1, compiler) require.NoError(t, compiler.compileCallIndirect(operation)) // At this point, we consumed the offset value, but the function returns one value, // so the stack pointer results in the same. requireRuntimeLocationStackPointerEqual(t, 1, compiler) 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()) require.Equal(t, uint64(1), env.stackPointer()) require.Equal(t, expectedReturnValue, uint32(env.ce.popValue())) }) } }) } // TestCompiler_callIndirect_largeTypeIndex ensures that non-trivial large type index works well during call_indirect. // Note: any index larger than 8-bit range is considered as large for arm64 compiler. func TestCompiler_callIndirect_largeTypeIndex(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. const typeIndex, typeID = 12345, 0 operation := operationPtr(wazeroir.NewOperationCallIndirect(typeIndex, 0)) env.module().TypeIDs = make([]wasm.FunctionTypeID, typeIndex+1) env.module().TypeIDs[typeIndex] = typeID env.module().Engine = &moduleEngine{functions: []function{}} types := make([]wasm.FunctionType, typeIndex+1) types[typeIndex] = wasm.FunctionType{} code1 := asm.CodeSegment{} code2 := asm.CodeSegment{} defer func() { require.NoError(t, code1.Unmap()) require.NoError(t, code2.Unmap()) }() 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) _, err = compiler.compile(code1.NextCodeSection()) require.NoError(t, err) makeExecutable(code1.Bytes()) f := function{ parent: &compiledFunction{parent: &compiledCode{executable: code1}}, codeInitialAddress: uintptr(unsafe.Pointer(&code1.Bytes()[0])), 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: types, HasTable: true, }) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(0))) require.NoError(t, err) require.NoError(t, compiler.compileCallIndirect(operation)) err = compiler.compileReturnFunction() require.NoError(t, err) // Generate the code under test and run. _, err = compiler.compile(code2.NextCodeSection()) require.NoError(t, err) env.exec(code2.Bytes()) } func TestCompiler_compileCall(t *testing.T) { env := newCompilerEnvironment() me := env.moduleEngine() expectedValue := uint32(0) // Emit the call target function. const numCalls = 3 targetFunctionType := wasm.FunctionType{ Params: []wasm.ValueType{wasm.ValueTypeI32}, Results: []wasm.ValueType{wasm.ValueTypeI32}, ParamNumInUint64: 1, ResultNumInUint64: 1, } for i := 0; i < numCalls; i++ { // Each function takes one argument, adds the value with 100 + i and returns the result. addTargetValue := uint32(100 + i) expectedValue += addTargetValue compiler := env.requireNewCompiler(t, &targetFunctionType, newCompiler, &wazeroir.CompilationResult{}) err := compiler.compilePreamble() require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(addTargetValue))) require.NoError(t, err) // Picks the function argument placed at the bottom of the stack. err = compiler.compilePick(operationPtr(wazeroir.NewOperationPick(int(compiler.runtimeValueLocationStack().sp-1), false))) require.NoError(t, err) // Adds the const to the picked value. err = compiler.compileAdd(operationPtr(wazeroir.NewOperationAdd(wazeroir.UnsignedTypeI32))) require.NoError(t, err) // Then store the added result into the bottom of the stack (which is treated as the result of the function). err = compiler.compileSet(operationPtr(wazeroir.NewOperationSet(int(compiler.runtimeValueLocationStack().sp-1), false))) 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) makeExecutable(code.Bytes()) me.functions = append(me.functions, function{ parent: &compiledFunction{parent: &compiledCode{executable: code}}, codeInitialAddress: uintptr(unsafe.Pointer(&code.Bytes()[0])), moduleInstance: env.moduleInstance, }) } // Now we start building the caller's code. compiler := env.requireNewCompiler(t, &wasm.FunctionType{}, newCompiler, &wazeroir.CompilationResult{ Functions: make([]uint32, numCalls), Types: []wasm.FunctionType{targetFunctionType}, }) err := compiler.compilePreamble() require.NoError(t, err) const initialValue = 100 expectedValue += initialValue err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(1234))) // Dummy value so the base pointer would be non-trivial for callees. require.NoError(t, err) err = compiler.compileConstI32(operationPtr(wazeroir.NewOperationConstI32(initialValue))) require.NoError(t, err) // Call all the built functions. for i := 0; i < numCalls; i++ { err = compiler.compileCall(operationPtr(wazeroir.NewOperationCall(1))) require.NoError(t, err) } // Set the result slot 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()) // Check status and returned values. require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) require.Equal(t, uint64(0), env.stackBasePointer()) require.Equal(t, uint64(2), env.stackPointer()) // Must be 2 (dummy value + the calculation results) require.Equal(t, expectedValue, env.stackTopAsUint32()) }