From b7d199094a8479b6279ed0d83e8da3984a474526 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:10:36 -0700 Subject: [PATCH] compiler: bound aggregate call signatures Plan internal aggregate parameter lowering from each complete function signature. Count scalar leaves, the context parameter, and any hidden aggregate-result pointer, then pass the largest aggregates indirectly until the signature fits within the 1,000-parameter limit. Keep the ABI policy target-independent and leave fitting signatures unchanged. Include the interface typecode when budgeting invoke signatures and keep parameter lowering consistent between concrete and interface calls. Preserve exported ABIs and diagnose exported methods that cannot use the internal interface ABI. Attach valid debug metadata to these diagnostic wrappers so that errors keep their source positions. Materialize indirect aggregate phi inputs in their predecessor blocks. Retain temporary argument-promotion guards through the ThinLTO pre-link pipeline, then remove the guards before final dead-code elimination. Validate final WebAssembly signatures that cannot be rewritten. --- builder/build.go | 5 + compiler/calls.go | 50 ++---- compiler/compiler.go | 60 ++++--- compiler/compiler_test.go | 188 +++++++++++++++++++- compiler/defer.go | 31 ++-- compiler/func.go | 200 +++++++++++++++++++--- compiler/goroutine.go | 16 +- compiler/interface.go | 59 ++++++- compiler/llvmutil/llvm.go | 57 ++++++ compiler/llvmutil/llvm_test.go | 41 +++++ compiler/symbol.go | 28 +-- compiler/testdata/aggregate-abi.go | 119 +++++++++++++ compiler/testdata/aggregate-export-abi.go | 43 +++++ transform/interface-lowering.go | 25 +++ transform/optimizer.go | 35 ++-- 15 files changed, 832 insertions(+), 125 deletions(-) create mode 100644 compiler/llvmutil/llvm_test.go create mode 100644 compiler/testdata/aggregate-abi.go create mode 100644 compiler/testdata/aggregate-export-abi.go diff --git a/builder/build.go b/builder/build.go index 64c10334d7..adf8247215 100644 --- a/builder/build.go +++ b/builder/build.go @@ -652,6 +652,11 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe if err != nil { return err } + if strings.HasPrefix(config.Triple(), "wasm") { + if err := compiler.ValidateWasmFunctionParameters(mod); err != nil { + return err + } + } // Make sure stack sizes are loaded from a separate section so they can be // modified after linking. diff --git a/compiler/calls.go b/compiler/calls.go index 7dab9432b1..93fcc5a39f 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -597,20 +597,6 @@ func (b *builder) createUnwindReturnOrUnreachable() { } } -// Expand an argument type to a list that can be used in a function call -// parameter list. -func (c *compilerContext) expandFormalParamType(t llvm.Type, name string, goType types.Type) []paramInfo { - if c.isIndirectAggregate(t) { - return []paramInfo{{ - llvmType: c.dataPtrType, - name: name, - elemSize: c.targetData.TypeAllocSize(t), - flags: paramIsGoParam | paramIsReadonly | paramIsIndirect, - }} - } - return c.expandDirectFormalParamType(t, name, goType) -} - func (c *compilerContext) expandDirectFormalParamType(t llvm.Type, name string, goType types.Type) []paramInfo { switch t.TypeKind() { case llvm.StructTypeKind: @@ -625,34 +611,36 @@ func (c *compilerContext) expandDirectFormalParamType(t llvm.Type, name string, return []paramInfo{c.getParamInfo(t, name, goType)} } -func (c *compilerContext) storedParamType(t llvm.Type, exported bool) llvm.Type { - if c.isIndirectParam(t, exported) { +func (c *compilerContext) storedParamType(t llvm.Type) llvm.Type { + if c.isIndirectAggregate(t) { return c.dataPtrType } return t } -func (c *compilerContext) isIndirectParam(t llvm.Type, exported bool) bool { - return !exported && c.isIndirectAggregate(t) -} - -func (b *builder) appendStoredValueTypes(valueTypes []llvm.Type, values []ssa.Value, exported bool) []llvm.Type { - for _, value := range values { - valueTypes = append(valueTypes, b.storedParamType(b.getLLVMType(value.Type()), exported)) +func (b *builder) appendStoredParamTypes(valueTypes []llvm.Type, params []functionABIParam) []llvm.Type { + for _, param := range params { + if param.indirect { + valueTypes = append(valueTypes, b.dataPtrType) + } else { + valueTypes = append(valueTypes, param.llvmType) + } } return valueTypes } -func (b *builder) appendStoredParamTypes(valueTypes []llvm.Type, params []*types.Var, exported bool) []llvm.Type { - for _, param := range params { - valueTypes = append(valueTypes, b.storedParamType(b.getLLVMType(param.Type()), exported)) +func (b *builder) getCallArguments(values []ssa.Value, params []functionABIParam) []llvm.Value { + args := make([]llvm.Value, len(values)) + for i, value := range values { + args[i] = b.getCallArgument(value, params[i].indirect) } - return valueTypes + return args } func (b *builder) prependIndirectResult(sig *types.Signature, exported bool, params []llvm.Value, name string) []llvm.Value { - if resultType, indirect := b.hasIndirectResult(sig); !exported && indirect { - return append([]llvm.Value{b.createIndirectStorage(resultType, name)}, params...) + abi := b.getFunctionABI(sig, exported) + if abi.indirectResult { + return append([]llvm.Value{b.createIndirectStorage(abi.resultType, name)}, params...) } return params } @@ -679,8 +667,8 @@ func (b *builder) expandFormalParamOffsets(t llvm.Type) []uint64 { // expandFormalParam splits a formal param value into pieces, so it can be // passed directly as part of a function call. For example, it splits up small -// structs into individual fields. It is the equivalent of expandFormalParamType -// for parameter values. +// structs into individual fields. It is the equivalent of +// expandDirectFormalParamType for parameter values. func (b *builder) expandFormalParam(v llvm.Value) []llvm.Value { switch v.Type().TypeKind() { case llvm.StructTypeKind: diff --git a/compiler/compiler.go b/compiler/compiler.go index 30eb1fd468..377ace62a1 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -93,6 +93,7 @@ type compilerContext struct { directCatchers map[llvm.Value]llvm.Value indirectCatchers map[llvm.Type]llvm.Value asyncifyReplays map[llvm.Type]llvm.Value + functionABIs map[functionABIKey]functionABI astComments map[string]*ast.CommentGroup embedGlobals map[string][]*loader.EmbedFile pkg *types.Package @@ -118,6 +119,7 @@ func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *C directCatchers: map[llvm.Value]llvm.Value{}, indirectCatchers: map[llvm.Type]llvm.Value{}, asyncifyReplays: map[llvm.Type]llvm.Value{}, + functionABIs: map[functionABIKey]functionABI{}, astComments: map[string]*ast.CommentGroup{}, } @@ -805,7 +807,7 @@ func (b *builder) getLocalVariable(variable *types.Var) llvm.Metadata { return dilocal } -// attachDebugInfo adds debug info to a function declaration. It returns the +// attachDebugInfo adds debug info to a function. It returns the // DISubprogram metadata node. func (c *compilerContext) attachDebugInfo(f *ssa.Function) llvm.Metadata { pos := c.program.Fset.Position(f.Syntax().Pos()) @@ -813,10 +815,18 @@ func (c *compilerContext) attachDebugInfo(f *ssa.Function) llvm.Metadata { return c.attachDebugInfoRaw(f, fn, "", pos.Filename, pos.Line) } -// attachDebugInfo adds debug info to a function declaration. It returns the +// attachDebugInfoRaw adds debug info to a function. It returns the // DISubprogram metadata node. This method allows some more control over how // debug info is added to the function. func (c *compilerContext) attachDebugInfoRaw(f *ssa.Function, llvmFn llvm.Value, suffix, filename string, line int) llvm.Metadata { + return c.attachDebugInfoRawWithDefinition(f, llvmFn, suffix, filename, line, true) +} + +func (c *compilerContext) attachDebugInfoDeclarationRaw(f *ssa.Function, llvmFn llvm.Value, suffix, filename string, line int) llvm.Metadata { + return c.attachDebugInfoRawWithDefinition(f, llvmFn, suffix, filename, line, false) +} + +func (c *compilerContext) attachDebugInfoRawWithDefinition(f *ssa.Function, llvmFn llvm.Value, suffix, filename string, line int, isDefinition bool) llvm.Metadata { // Debug info for this function. params := getParams(f.Signature) diparams := make([]llvm.Metadata, 0, len(params)) @@ -835,7 +845,7 @@ func (c *compilerContext) attachDebugInfoRaw(f *ssa.Function, llvmFn llvm.Value, Line: line, Type: diFuncType, LocalToUnit: true, - IsDefinition: true, + IsDefinition: isDefinition, ScopeLine: 0, Flags: llvm.FlagPrototyped, Optimized: true, @@ -1300,27 +1310,23 @@ func (b *builder) createFunctionStart(intrinsic bool) { } // Load function parameters + abi := b.getFunctionABI(b.fn.Signature, b.info.exported) llvmParamIndex := 0 - if _, indirectResult := b.hasIndirectResult(b.fn.Signature); indirectResult && !b.info.exported { + if abi.indirectResult { b.indirectReturn = b.llvmFn.Param(llvmParamIndex) b.indirectReturn.SetName("return") llvmParamIndex++ } - for _, param := range b.fn.Params { - llvmType := b.getLLVMType(param.Type()) - if b.isIndirectParam(llvmType, b.info.exported) { + for i, param := range b.fn.Params { + llvmType := abi.params[i].llvmType + if abi.params[i].indirect { llvmParam := b.llvmFn.Param(llvmParamIndex) llvmParam.SetName(param.Name()) b.indirectValues[param] = llvmParam llvmParamIndex++ continue } - var paramInfos []paramInfo - if b.info.exported { - paramInfos = b.expandDirectFormalParamType(llvmType, param.Name(), param.Type()) - } else { - paramInfos = b.expandFormalParamType(llvmType, param.Name(), param.Type()) - } + paramInfos := b.expandDirectFormalParamType(llvmType, param.Name(), param.Type()) fields := make([]llvm.Value, 0, 1) for _, info := range paramInfos { param := b.llvmFn.Param(llvmParamIndex) @@ -1459,14 +1465,20 @@ func (b *builder) createFunction() { } // Resolve phi nodes + phiBuilder := b.ctx.NewBuilder() + originalBuilder := b.Builder + b.Builder = phiBuilder for _, phi := range b.phis { block := phi.ssa.Block() for i, edge := range phi.ssa.Edges { - llvmVal := b.getCallArgument(edge, false) llvmBlock := b.blockInfo[block.Preds[i].Index].exit + b.SetInsertPointBefore(llvmBlock.LastInstruction()) + llvmVal := b.getCallArgument(edge, b.isIndirectAggregate(b.getLLVMType(edge.Type()))) phi.llvm.AddIncoming([]llvm.Value{llvmVal}, []llvm.BasicBlock{llvmBlock}) } } + b.Builder = originalBuilder + phiBuilder.Dispose() if b.NeedsStackObjects { // Track phi nodes. @@ -1697,9 +1709,8 @@ func (b *builder) getValuePointer(value ssa.Value) llvm.Value { return ptr } -func (b *builder) getCallArgument(value ssa.Value, exported bool) llvm.Value { - paramType := b.getLLVMType(value.Type()) - if b.isIndirectParam(paramType, exported) { +func (b *builder) getCallArgument(value ssa.Value, indirect bool) llvm.Value { + if indirect { return b.getValuePointer(value) } return b.getValue(value, getPos(value)) @@ -2344,18 +2355,21 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) b.createNilCheck(instr.Value, callee, "fpcall") } - var params []llvm.Value - for _, param := range instr.Args { - params = append(params, b.getCallArgument(param, exported)) + abi := b.getFunctionABI(instr.Signature(), exported) + paramOffset := 0 + if instr.IsInvoke() { + abi = b.getInterfaceFunctionABI(instr.Signature()) + paramOffset = 1 } + params := b.getCallArguments(instr.Args, abi.params[paramOffset:]) if instr.IsInvoke() { params = append([]llvm.Value{invokeReceiver}, params...) params = append(params, invokeTypecode) } if !exported { - if resultType, indirectResult := b.hasIndirectResult(instr.Signature()); indirectResult { - result := b.createIndirectStorage(resultType, "call.result") + if abi.indirectResult { + result := b.createIndirectStorage(abi.resultType, "call.result") params = append([]llvm.Value{result}, params...) params = append(params, context) b.createInvoke(calleeType, callee, params, "", instr) @@ -2736,7 +2750,7 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) { return b.createMapIteratorNext(rangeVal, llvmRangeVal, it), nil } case *ssa.Phi: - phiType := b.storedParamType(b.getLLVMType(expr.Type()), false) + phiType := b.storedParamType(b.getLLVMType(expr.Type())) phi := b.CreatePHI(phiType, "") b.phis = append(b.phis, phiNode{expr, phi}) return phi, nil diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 8f9a79d564..b87f964fd8 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -2,6 +2,7 @@ package compiler import ( "flag" + "go/scanner" "go/types" "os" "regexp" @@ -12,6 +13,7 @@ import ( "github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/loader" + "github.com/tinygo-org/tinygo/transform" "tinygo.org/x/go-llvm" ) @@ -174,6 +176,182 @@ func TestOptimizedLargeAggregateABI(t *testing.T) { } } +func TestAggregateFunctionABI(t *testing.T) { + for _, target := range []string{"wasm", "cortex-m-qemu"} { + t.Run(target, func(t *testing.T) { + options := &compileopts.Options{Target: target} + if target != "wasm" { + options.Scheduler = "tasks" + } + mod, errs := testCompilePackage(t, options, "aggregate-abi.go") + if len(errs) != 0 { + for _, err := range errs { + t.Error(err) + } + return + } + defer mod.Dispose() + + passOptions := llvm.NewPassBuilderOptions() + defer passOptions.Dispose() + if err := mod.RunPasses("default", llvm.TargetMachine{}, passOptions); err != nil { + t.Fatal(err) + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + + checkFunctionParamABI(t, mod, "main.readDirectAggregates", false, false) + checkFunctionParamABI(t, mod, "main.readLimitAggregates", false, false) + checkFunctionParamABI(t, mod, "main.readBoundaryAggregates", true, false) + checkFunctionParamABI(t, mod, "main.readAggregates", true, false) + checkFunctionParamABI(t, mod, "main.readSingleAggregate", false) + checkFunctionParamABI(t, mod, "main.readThreeAggregates", true, false, false) + checkFunctionParamABI(t, mod, "main.readResultBudget", false, true) + checkFunctionParamABI(t, mod, "readAggregateExport", false) + if target == "wasm" { + if err := ValidateWasmFunctionParameters(mod); err != nil { + t.Error(err) + } + } + }) + } +} + +func checkFunctionParamABI(t *testing.T, mod llvm.Module, name string, indirect ...bool) { + t.Helper() + fn := mod.NamedFunction(name) + if fn.IsNil() { + t.Fatalf("missing function %s", name) + } + paramTypes := fn.GlobalValueType().ParamTypes() + for i, wantIndirect := range indirect { + gotIndirect := paramTypes[i].TypeKind() == llvm.PointerTypeKind + if gotIndirect != wantIndirect { + t.Errorf("%s parameter %d indirect=%t, want %t", name, i, gotIndirect, wantIndirect) + } + } +} + +func TestAggregateExportedInterfaceABI(t *testing.T) { + for _, debug := range []bool{false, true} { + t.Run("debug="+strconv.FormatBool(debug), func(t *testing.T) { + options := &compileopts.Options{Target: "wasm"} + mod, errs := testCompilePackageWithDebug(t, options, "aggregate-export-abi.go", debug) + defer mod.Dispose() + + for _, err := range errs { + t.Error(err) + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + + var markedWrappers int + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if attr := fn.GetStringAttributeAtIndex(-1, "tinygo-interface-abi-error"); !attr.IsNil() { + markedWrappers++ + } + } + if markedWrappers != 2 { + t.Errorf("found %d exported interface ABI markers, want 2", markedWrappers) + } + if err := ValidateWasmFunctionParameters(mod); err == nil { + t.Error("missing oversized WebAssembly signature error") + } else if !strings.Contains(err.Error(), "exported functions cannot lower aggregate parameters indirectly") { + t.Errorf("unexpected oversized WebAssembly signature error: %v", err) + } + + target, err := compileopts.LoadTarget(options) + if err != nil { + t.Fatal(err) + } + err = transform.LowerInterfaces(mod, &compileopts.Config{ + Options: options, + Target: target, + }) + if err == nil { + t.Fatal("missing exported interface ABI error") + } + errList, ok := err.(scanner.ErrorList) + if !ok { + t.Fatalf("expected scanner.ErrorList, got %T", err) + } + if len(errList) != 2 { + t.Fatalf("got %d exported interface ABI errors, want 2", len(errList)) + } + if debug { + for _, err := range errList { + if !strings.HasSuffix(err.Pos.Filename, "aggregate-export-abi.go") || err.Pos.Line == 0 { + t.Errorf("missing source position in exported interface ABI error: %v", err) + } + } + } + }) + } +} + +func TestValidateWasmFunctionParameters(t *testing.T) { + for _, test := range []struct { + name string + params int + resultFields int + declaration bool + used bool + wantError bool + }{ + {"at limit", 999, 2, false, false, false}, + {"hidden result over limit", 1000, 2, false, false, true}, + {"single result at limit", 1000, 1, false, false, false}, + {"empty result at limit", 1000, 0, false, false, false}, + {"unused declaration", 1001, 0, true, false, false}, + {"used declaration", 1001, 0, true, true, true}, + } { + t.Run(test.name, func(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := ctx.NewModule("test") + defer mod.Dispose() + builder := ctx.NewBuilder() + defer builder.Dispose() + + paramTypes := make([]llvm.Type, test.params) + for i := range paramTypes { + paramTypes[i] = ctx.Int32Type() + } + resultFields := make([]llvm.Type, test.resultFields) + for i := range resultFields { + resultFields[i] = ctx.Int32Type() + } + resultType := ctx.StructType(resultFields, false) + fnType := llvm.FunctionType(resultType, paramTypes, false) + fn := llvm.AddFunction(mod, "test", fnType) + if test.declaration { + if test.used { + caller := llvm.AddFunction(mod, "caller", llvm.FunctionType(ctx.VoidType(), nil, false)) + block := ctx.AddBasicBlock(caller, "entry") + builder.SetInsertPointAtEnd(block) + args := make([]llvm.Value, len(paramTypes)) + for i, paramType := range paramTypes { + args[i] = llvm.Undef(paramType) + } + builder.CreateCall(fnType, fn, args, "") + builder.CreateRetVoid() + } + } else { + block := ctx.AddBasicBlock(fn, "entry") + builder.SetInsertPointAtEnd(block) + builder.CreateRet(llvm.Undef(resultType)) + } + + err := ValidateWasmFunctionParameters(mod) + if (err != nil) != test.wantError { + t.Errorf("ValidateWasmFunctionParameters() error = %v, wantError = %t", err, test.wantError) + } + }) + } +} + // normalizeIR canonicalizes LLVM-version-specific IR spellings for comparison // and when regenerating golden files. func normalizeIR(s string) string { @@ -280,6 +458,9 @@ func filterIrrelevantIRLines(lines []string) []string { if strings.HasPrefix(line, "source_filename = ") { continue } + if strings.HasPrefix(line, "@tinygo.indirect-abi = ") { + continue + } if llvmVersion < 15 && strings.HasPrefix(line, "target datalayout = ") { // The datalayout string may vary betewen LLVM versions. // Right now test outputs are for LLVM 15 and higher. @@ -358,7 +539,7 @@ func TestAggregateValueCount(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - count, exceeded := aggregateValueCount(test.typ, 0) + count, exceeded := aggregateValueCountLimit(test.typ, 0, maxDirectAggregateValues) if exceeded != test.exceeded { t.Errorf("expected exceeded=%t, got %t", test.exceeded, exceeded) } @@ -371,6 +552,10 @@ func TestAggregateValueCount(t *testing.T) { // Build a package given a number of compiler options and a file. func testCompilePackage(t *testing.T, options *compileopts.Options, file string) (llvm.Module, []error) { + return testCompilePackageWithDebug(t, options, file, false) +} + +func testCompilePackageWithDebug(t *testing.T, options *compileopts.Options, file string, debug bool) (llvm.Module, []error) { target, err := compileopts.LoadTarget(options) if err != nil { t.Fatal("failed to load target:", err) @@ -392,6 +577,7 @@ func testCompilePackage(t *testing.T, options *compileopts.Options, file string) DefaultStackSize: config.StackSize(), NeedsStackObjects: config.NeedsStackObjects(), PanicUnwind: config.PanicUnwind(), + Debug: debug, } machine, err := NewTargetMachine(compilerConfig) if err != nil { diff --git a/compiler/defer.go b/compiler/defer.go index e0d85f0938..8340a3d83d 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -425,9 +425,6 @@ func (b *builder) createDefer(instr *ssa.Defer) { next := b.CreateLoad(b.dataPtrType, b.deferPtr, "defer.next") var values llvmValueList - lowerArgument := func(value ssa.Value) llvm.Value { - return b.getCallArgument(value, false) - } if instr.Call.IsInvoke() { // Method call on an interface. @@ -445,7 +442,8 @@ func (b *builder) createDefer(instr *ssa.Defer) { typecode := b.CreateExtractValue(itf, 0, "invoke.func.typecode") receiverValue := b.CreateExtractValue(itf, 1, "invoke.func.receiver") values = newLLVMValueList(callback, next, typecode, receiverValue) - values.appendSSAValues(instr.Call.Args, lowerArgument) + abi := b.getInterfaceFunctionABI(instr.Call.Signature()) + values.append(b.getCallArguments(instr.Call.Args, abi.params[1:])...) } else if callee, ok := instr.Call.Value.(*ssa.Function); ok { // Regular function call. @@ -459,9 +457,8 @@ func (b *builder) createDefer(instr *ssa.Defer) { // runtime._defer fields). values = newLLVMValueList(callback, next) exported := b.getFunctionInfo(callee).exported - values.appendSSAValues(instr.Call.Args, func(value ssa.Value) llvm.Value { - return b.getCallArgument(value, exported) - }) + abi := b.getFunctionABI(callee.Signature, exported) + values.append(b.getCallArguments(instr.Call.Args, abi.params)...) } else if makeClosure, ok := instr.Call.Value.(*ssa.MakeClosure); ok { // Immediately applied function literal with free variables. @@ -485,7 +482,8 @@ func (b *builder) createDefer(instr *ssa.Defer) { // runtime._defer fields, followed by all parameters including the // context pointer). values = newLLVMValueList(callback, next) - values.appendSSAValues(instr.Call.Args, lowerArgument) + abi := b.getFunctionABI(fn.Signature, false) + values.append(b.getCallArguments(instr.Call.Args, abi.params)...) values.append(context) } else if builtin, ok := instr.Call.Value.(*ssa.Builtin); ok { @@ -526,7 +524,8 @@ func (b *builder) createDefer(instr *ssa.Defer) { // runtime._defer fields, followed by all parameters including the // context pointer). values = newLLVMValueList(callback, next, funcValue) - values.appendSSAValues(instr.Call.Args, lowerArgument) + abi := b.getFunctionABI(instr.Call.Signature(), false) + values.append(b.getCallArguments(instr.Call.Args, abi.params)...) } // Make a struct out of the collected values to put in the deferred call @@ -636,7 +635,13 @@ func (b *builder) createRunDefers() { valueTypes = append(valueTypes, b.dataPtrType, b.dataPtrType) } - valueTypes = b.appendStoredValueTypes(valueTypes, callback.Args, false) + var params []functionABIParam + if callback.IsInvoke() { + params = b.getInterfaceFunctionABI(callback.Signature()).params[1:] + } else { + params = b.getFunctionABI(callback.Signature(), false).params + } + valueTypes = b.appendStoredParamTypes(valueTypes, params) // Extract the params from the struct (including receiver). deferredCallType := b.ctx.StructType(valueTypes, false) @@ -679,7 +684,8 @@ func (b *builder) createRunDefers() { // Get the real defer struct type and cast to it. valueTypes := []llvm.Type{b.uintptrType, b.dataPtrType} exported := b.getFunctionInfo(callback).exported - valueTypes = b.appendStoredParamTypes(valueTypes, getParams(callback.Signature), exported) + abi := b.getFunctionABI(callback.Signature, exported) + valueTypes = b.appendStoredParamTypes(valueTypes, abi.params) deferredCallType := b.ctx.StructType(valueTypes, false) // Extract the params from the struct. @@ -702,7 +708,8 @@ func (b *builder) createRunDefers() { // Get the real defer struct type and cast to it. fn := callback.Fn.(*ssa.Function) valueTypes := []llvm.Type{b.uintptrType, b.dataPtrType} - valueTypes = b.appendStoredParamTypes(valueTypes, getParams(fn.Signature), false) + abi := b.getFunctionABI(fn.Signature, false) + valueTypes = b.appendStoredParamTypes(valueTypes, abi.params) valueTypes = append(valueTypes, b.dataPtrType) // closure deferredCallType := b.ctx.StructType(valueTypes, false) diff --git a/compiler/func.go b/compiler/func.go index 260ff07da0..517d585ea9 100644 --- a/compiler/func.go +++ b/compiler/func.go @@ -4,17 +4,163 @@ package compiler // in a later step, see func-lowering.go. import ( + "fmt" "go/types" + "sort" "golang.org/x/tools/go/ssa" "tinygo.org/x/go-llvm" ) -// LLVM recursively expands each struct field and array element in parameters -// and results into separate values. It gets very slow with too many values, so -// pass larger aggregates indirectly before LLVM expands them. +// LLVM recursively expands each struct field and array element into separate +// values. Pass larger aggregates indirectly before LLVM expands them. const maxDirectAggregateValues = 1024 +// The WebAssembly JavaScript API limits function types to 1000 parameters. +// Apply the same internal ABI cap on every target. +const maxFunctionParams = 1000 + +type functionABIParam struct { + llvmType llvm.Type + indirect bool + leafCount uint64 +} + +type functionABI struct { + resultType llvm.Type + indirectResult bool + params []functionABIParam +} + +type functionABIKey struct { + signature *types.Signature + exported bool + interfaceReceiver bool + budgetReceiverPtr bool + extraParams uint64 +} + +func (c *compilerContext) getFunctionABI(sig *types.Signature, exported bool) functionABI { + budgetReceiverPtr := sig.Recv() != nil && !exported + extraParams := uint64(0) + if budgetReceiverPtr { + // Keep ordinary parameter decisions identical between concrete method + // calls and interface invokes. + extraParams++ // interface typecode + } + return c.getFunctionABIWithReceiver(sig, exported, false, budgetReceiverPtr, extraParams) +} + +func (c *compilerContext) getInterfaceFunctionABI(sig *types.Signature) functionABI { + return c.getFunctionABIWithReceiver(sig, false, true, false, 1) +} + +// getFunctionABIWithReceiver lowers the fewest aggregate parameters necessary +// to keep the complete scalarized signature within the internal ABI cap. +func (c *compilerContext) getFunctionABIWithReceiver(sig *types.Signature, exported, interfaceReceiver, budgetReceiverPtr bool, extraParams uint64) functionABI { + key := functionABIKey{sig, exported, interfaceReceiver, budgetReceiverPtr, extraParams} + if abi, ok := c.functionABIs[key]; ok { + return abi + } + + abi := functionABI{} + abi.resultType, abi.indirectResult = c.hasIndirectResult(sig) + if exported { + abi.indirectResult = false + } + + for i, param := range getParams(sig) { + llvmType := c.getLLVMType(param.Type()) + if i == 0 && interfaceReceiver { + llvmType = c.dataPtrType + } + leafCount, exceeded := aggregateValueCountLimit(llvmType, 0, maxFunctionParams) + if exceeded { + leafCount = maxFunctionParams + 1 + } + abi.params = append(abi.params, functionABIParam{ + llvmType: llvmType, + indirect: !exported && c.isIndirectAggregate(llvmType), + leafCount: leafCount, + }) + } + + if exported { + c.functionABIs[key] = abi + return abi + } + + count := uint64(1) + extraParams // context and synthetic parameters + if abi.indirectResult { + count++ + } else if aggregateValueCountExceeds(abi.resultType, 1) { + count++ + } + + var candidates []int + for i, param := range abi.params { + if param.indirect { + count++ + continue + } + if i == 0 && budgetReceiverPtr { + count++ + continue + } + count += param.leafCount + switch param.llvmType.TypeKind() { + case llvm.ArrayTypeKind, llvm.StructTypeKind: + if param.leafCount > 1 { + candidates = append(candidates, i) + } + } + } + sort.SliceStable(candidates, func(i, j int) bool { + return abi.params[candidates[i]].leafCount > abi.params[candidates[j]].leafCount + }) + // Minimize ABI changes by lowering the largest aggregates first. + for _, i := range candidates { + if count <= maxFunctionParams { + break + } + abi.params[i].indirect = true + count -= abi.params[i].leafCount - 1 + } + if budgetReceiverPtr && !abi.params[0].indirect { + concreteCount := count - extraParams - 1 + abi.params[0].leafCount + if concreteCount > maxFunctionParams { + abi.params[0].indirect = true + } + } + + c.functionABIs[key] = abi + return abi +} + +// ValidateWasmFunctionParameters checks the final LLVM module before the +// WebAssembly backend expands aggregate parameters into scalar values. +func ValidateWasmFunctionParameters(mod llvm.Module) error { + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if fn.IsDeclaration() && fn.FirstUse().IsNil() { + continue + } + count := uint64(0) + if returnType := fn.GlobalValueType().ReturnType(); returnType.TypeKind() == llvm.ArrayTypeKind || returnType.TypeKind() == llvm.StructTypeKind { + if _, indirect := aggregateValueCountLimit(returnType, 0, 1); indirect { + count++ + } + } + for _, paramType := range fn.GlobalValueType().ParamTypes() { + var exceeded bool + count, exceeded = aggregateValueCountLimit(paramType, count, maxFunctionParams) + if exceeded { + return fmt.Errorf("function %s has more than %d WebAssembly parameters after ABI lowering; reduce the number or size of its parameters (exported functions cannot lower aggregate parameters indirectly)", fn.Name(), maxFunctionParams) + } + } + } + return nil +} + func (c *compilerContext) getLLVMResultType(sig *types.Signature) llvm.Type { switch sig.Results().Len() { case 0: @@ -36,34 +182,38 @@ func (c *compilerContext) hasIndirectResult(sig *types.Signature) (llvm.Type, bo } func (c *compilerContext) isIndirectAggregate(typ llvm.Type) bool { + return aggregateValueCountExceeds(typ, maxDirectAggregateValues) +} + +func aggregateValueCountExceeds(typ llvm.Type, limit uint64) bool { switch typ.TypeKind() { case llvm.ArrayTypeKind, llvm.StructTypeKind: - _, exceeded := aggregateValueCount(typ, 0) + _, exceeded := aggregateValueCountLimit(typ, 0, limit) return exceeded default: return false } } -func aggregateValueCount(typ llvm.Type, count uint64) (uint64, bool) { +func aggregateValueCountLimit(typ llvm.Type, count, limit uint64) (uint64, bool) { switch typ.TypeKind() { case llvm.ArrayTypeKind: length := uint64(typ.ArrayLength()) if length == 0 { return count, false } - elementCount, exceeded := aggregateValueCount(typ.ElementType(), 0) + elementCount, exceeded := aggregateValueCountLimit(typ.ElementType(), 0, limit) if exceeded { return count, true } - if elementCount != 0 && length > (maxDirectAggregateValues-count)/elementCount { + if elementCount != 0 && length > (limit-count)/elementCount { return count, true } return count + length*elementCount, false case llvm.StructTypeKind: for _, field := range typ.StructElementTypes() { var exceeded bool - count, exceeded = aggregateValueCount(field, count) + count, exceeded = aggregateValueCountLimit(field, count, limit) if exceeded { return count, true } @@ -71,7 +221,7 @@ func aggregateValueCount(typ llvm.Type, count uint64) (uint64, bool) { return count, false default: count++ - return count, count > maxDirectAggregateValues + return count, count > limit } } @@ -133,11 +283,17 @@ func (c *compilerContext) getFuncType(typ *types.Signature) llvm.Type { // getLLVMFunctionType returns a LLVM function type for a given signature. func (c *compilerContext) getLLVMFunctionType(typ *types.Signature) llvm.Type { - returnType, indirectResult := c.hasIndirectResult(typ) + var abi functionABI + if typ.Recv() != nil && c.getLLVMType(typ.Recv().Type()).StructName() == "runtime._interface" { + abi = c.getFunctionABIWithReceiver(typ, false, true, false, 0) + } else { + abi = c.getFunctionABI(typ, false) + } + returnType := abi.resultType // Get the parameter types. var paramTypes []llvm.Type - if indirectResult { + if abi.indirectResult { // LLVM expands aggregate returns into scalar leaves before deciding // whether to pass them indirectly, so a large IR return can exhaust // memory. Returning void avoids that expansion and cannot be demoted @@ -145,21 +301,13 @@ func (c *compilerContext) getLLVMFunctionType(typ *types.Signature) llvm.Type { paramTypes = append(paramTypes, c.dataPtrType) returnType = c.ctx.VoidType() } - if typ.Recv() != nil { - recv := c.getLLVMType(typ.Recv().Type()) - if recv.StructName() == "runtime._interface" { - // This is a call on an interface, not a concrete type. - // The receiver is not an interface, but a i8* type. - recv = c.dataPtrType - } - for _, info := range c.expandFormalParamType(recv, "", nil) { - paramTypes = append(paramTypes, info.llvmType) - } - } - for v := range typ.Params().Variables() { - subType := c.getLLVMType(v.Type()) - for _, info := range c.expandFormalParamType(subType, "", nil) { - paramTypes = append(paramTypes, info.llvmType) + for _, param := range abi.params { + if param.indirect { + paramTypes = append(paramTypes, c.dataPtrType) + } else { + for _, info := range c.expandDirectFormalParamType(param.llvmType, "", nil) { + paramTypes = append(paramTypes, info.llvmType) + } } } // All functions take these parameters at the end. diff --git a/compiler/goroutine.go b/compiler/goroutine.go index 8bc7da53cd..4b078efe8f 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -94,8 +94,14 @@ func (b *builder) createGo(instr *ssa.Go) { prefix = b.getFunctionInfo(b.fn).linkName } - for _, param := range instr.Call.Args { - params = append(params, b.getGoroutineCallArgument(param, exported)...) + abi := b.getFunctionABI(instr.Call.Signature(), exported) + paramOffset := 0 + if instr.Call.IsInvoke() { + abi = b.getInterfaceFunctionABI(instr.Call.Signature()) + paramOffset = 1 + } + for i, param := range instr.Call.Args { + params = append(params, b.getGoroutineCallArgument(param, abi.params[paramOffset+i].indirect)...) } if !context.IsNil() { params = append(params, context) @@ -127,10 +133,10 @@ func (b *builder) createGo(instr *ssa.Go) { b.createCall(fnType, start, []llvm.Value{callee, paramBundle, stackSize, llvm.Undef(b.dataPtrType)}, "") } -func (b *builder) getGoroutineCallArgument(value ssa.Value, exported bool) []llvm.Value { +func (b *builder) getGoroutineCallArgument(value ssa.Value, indirect bool) []llvm.Value { typ := b.getLLVMType(value.Type()) - arg := b.getCallArgument(value, exported) - if b.isIndirectParam(typ, exported) { + arg := b.getCallArgument(value, indirect) + if indirect { return []llvm.Value{b.copyToIndirectStorage(arg, typ, "go.param")} } return b.expandFormalParam(arg) diff --git a/compiler/interface.go b/compiler/interface.go index 245039027f..3ab2af6048 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -1306,11 +1306,62 @@ func (c *compilerContext) getInterfaceInvokeWrapper(fn *ssa.Function, llvmFnType return wrapper } + exported := c.getFunctionInfo(fn).exported + abi := c.getFunctionABI(fn.Signature, exported) + if exported { + internalABI := c.getFunctionABI(fn.Signature, false) + var abiError string + if internalABI.indirectResult { + abiError = fmt.Sprintf("exported method %s with a large aggregate result cannot be called through an interface", fn.RelString(nil)) + } + if abiError == "" { + for _, param := range internalABI.params[1:] { + if param.indirect { + abiError = fmt.Sprintf("exported method %s with an aggregate parameter passed indirectly by the internal ABI cannot be called through an interface", fn.RelString(nil)) + break + } + } + } + if abiError != "" { + resultType := internalABI.resultType + var paramTypes []llvm.Type + if internalABI.indirectResult { + paramTypes = append(paramTypes, c.dataPtrType) + resultType = c.ctx.VoidType() + } + paramTypes = append(paramTypes, c.dataPtrType) + for _, param := range internalABI.params[1:] { + if param.indirect { + paramTypes = append(paramTypes, c.dataPtrType) + continue + } + for _, info := range c.expandDirectFormalParamType(param.llvmType, "", nil) { + paramTypes = append(paramTypes, info.llvmType) + } + } + paramTypes = append(paramTypes, c.dataPtrType) + wrapper = llvm.AddFunction(c.mod, wrapperName, llvm.FunctionType(resultType, paramTypes, false)) + c.addStandardDeclaredAttributes(wrapper) + if c.Debug { + pos := c.program.Fset.Position(fn.Pos()) + c.attachDebugInfoDeclarationRaw(fn, wrapper, "$invoke", pos.Filename, pos.Line) + } + wrapper.AddFunctionAttr(c.ctx.CreateStringAttribute("tinygo-interface-abi-error", abiError)) + return wrapper + } + } + // Get the expanded receiver type. - receiverType := c.getLLVMType(fn.Signature.Recv().Type()) + receiverType := abi.params[0].llvmType var expandedReceiverType []llvm.Type - receiverIndirect := c.isIndirectAggregate(receiverType) - for _, info := range c.expandFormalParamType(receiverType, "", nil) { + receiverIndirect := abi.params[0].indirect + var receiverInfos []paramInfo + if receiverIndirect { + receiverInfos = []paramInfo{{llvmType: c.dataPtrType}} + } else { + receiverInfos = c.expandDirectFormalParamType(receiverType, "", nil) + } + for _, info := range receiverInfos { expandedReceiverType = append(expandedReceiverType, info.llvmType) } @@ -1325,7 +1376,7 @@ func (c *compilerContext) getInterfaceInvokeWrapper(fn *ssa.Function, llvmFnType // create wrapper function resultOffset := 0 - if _, indirect := c.hasIndirectResult(fn.Signature); indirect { + if abi.indirectResult { resultOffset = 1 } paramTypes := append([]llvm.Type{}, llvmFnType.ParamTypes()[:resultOffset]...) diff --git a/compiler/llvmutil/llvm.go b/compiler/llvmutil/llvm.go index a3edc878d4..e3a63b4959 100644 --- a/compiler/llvmutil/llvm.go +++ b/compiler/llvmutil/llvm.go @@ -228,6 +228,63 @@ func AppendToGlobal(mod llvm.Module, globalName string, values ...llvm.Value) { used.SetLinkage(llvm.AppendingLinkage) } +// RemoveFromGlobal removes values matching the predicate from an appending +// global such as llvm.used. +func RemoveFromGlobal(mod llvm.Module, globalName string, remove func(llvm.Value) bool) { + global := mod.NamedGlobal(globalName) + if global.IsNil() { + return + } + + builder := mod.Context().NewBuilder() + defer builder.Dispose() + initializer := global.Initializer() + var kept []llvm.Value + for i := 0; i < initializer.Type().ArrayLength(); i++ { + value := builder.CreateExtractValue(initializer, i, "") + base := value + for !base.IsAConstantExpr().IsNil() && base.OperandsCount() == 1 { + base = base.Operand(0) + } + if !remove(base) { + kept = append(kept, value) + } + } + global.EraseFromParentAsGlobal() + if len(kept) != 0 { + AppendToGlobal(mod, globalName, kept...) + } +} + +// RemoveGlobalReferences removes one occurrence from targetGlobal for each +// value listed in referenceGlobal, then removes referenceGlobal itself. +func RemoveGlobalReferences(mod llvm.Module, targetGlobal, referenceGlobal string) { + references := mod.NamedGlobal(referenceGlobal) + if references.IsNil() { + return + } + + builder := mod.Context().NewBuilder() + defer builder.Dispose() + initializer := references.Initializer() + values := make(map[llvm.Value]int, initializer.Type().ArrayLength()) + for i := 0; i < initializer.Type().ArrayLength(); i++ { + value := builder.CreateExtractValue(initializer, i, "") + for !value.IsAConstantExpr().IsNil() && value.OperandsCount() == 1 { + value = value.Operand(0) + } + values[value]++ + } + references.EraseFromParentAsGlobal() + RemoveFromGlobal(mod, targetGlobal, func(value llvm.Value) bool { + if values[value] == 0 { + return false + } + values[value]-- + return true + }) +} + // Version returns the LLVM major version. func Version() int { majorStr := strings.Split(llvm.Version, ".")[0] diff --git a/compiler/llvmutil/llvm_test.go b/compiler/llvmutil/llvm_test.go new file mode 100644 index 0000000000..b001f9bec8 --- /dev/null +++ b/compiler/llvmutil/llvm_test.go @@ -0,0 +1,41 @@ +package llvmutil + +import ( + "testing" + + "tinygo.org/x/go-llvm" +) + +func TestRemoveGlobalReferences(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := ctx.NewModule("test") + defer mod.Dispose() + + fnType := llvm.FunctionType(ctx.VoidType(), nil, false) + kept := llvm.AddFunction(mod, "kept", fnType) + kept.SetLinkage(llvm.InternalLinkage) + shared := llvm.AddFunction(mod, "shared", fnType) + shared.SetLinkage(llvm.InternalLinkage) + temporary := llvm.AddFunction(mod, "temporary", fnType) + temporary.SetLinkage(llvm.InternalLinkage) + AppendToGlobal(mod, "llvm.used", kept, shared, shared, temporary) + AppendToGlobal(mod, "temporary.roots", shared, temporary) + + RemoveGlobalReferences(mod, "llvm.used", "temporary.roots") + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + if err := mod.RunPasses("globaldce", llvm.TargetMachine{}, options); err != nil { + t.Fatal(err) + } + + if mod.NamedFunction("kept").IsNil() { + t.Error("permanent root was removed") + } + if mod.NamedFunction("shared").IsNil() { + t.Error("permanent root was removed") + } + if !mod.NamedFunction("temporary").IsNil() { + t.Error("temporary root was retained") + } +} diff --git a/compiler/symbol.go b/compiler/symbol.go index 944f74240e..6df4e84cc7 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -79,13 +79,11 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) return llvmFn.GlobalValueType(), llvmFn } - retType, indirectResult := c.hasIndirectResult(fn.Signature) - if info.exported { - indirectResult = false - } + abi := c.getFunctionABI(fn.Signature, info.exported) + retType := abi.resultType var paramInfos []paramInfo - if indirectResult { + if abi.indirectResult { paramInfos = append(paramInfos, paramInfo{ llvmType: c.dataPtrType, name: "return", @@ -93,12 +91,16 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) }) retType = c.ctx.VoidType() } - for _, param := range getParams(fn.Signature) { - paramType := c.getLLVMType(param.Type()) - if info.exported { - paramInfos = append(paramInfos, c.expandDirectFormalParamType(paramType, param.Name(), param.Type())...) + for i, param := range getParams(fn.Signature) { + if abi.params[i].indirect { + paramInfos = append(paramInfos, paramInfo{ + llvmType: c.dataPtrType, + name: param.Name(), + elemSize: c.targetData.TypeAllocSize(abi.params[i].llvmType), + flags: paramIsGoParam | paramIsReadonly | paramIsIndirect, + }) } else { - paramInfos = append(paramInfos, c.expandFormalParamType(paramType, param.Name(), param.Type())...) + paramInfos = append(paramInfos, c.expandDirectFormalParamType(abi.params[i].llvmType, param.Name(), param.Type())...) } } @@ -109,7 +111,7 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) } var paramTypes []llvm.Type - hasIndirectABI := indirectResult + hasIndirectABI := abi.indirectResult for _, info := range paramInfos { paramTypes = append(paramTypes, info.llvmType) hasIndirectABI = hasIndirectABI || info.flags¶mIsIndirect != 0 @@ -120,8 +122,10 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) if hasIndirectABI { // Argument promotion only rewrites functions whose uses are all direct // calls. Keep an address use so LLVM cannot reconstruct the large - // aggregate signature that this ABI exists to avoid. + // aggregate signature that this ABI exists to avoid. The optimizer + // removes this temporary root before its final dead-code elimination. llvmutil.AppendToGlobal(c.mod, "llvm.used", llvmFn) + llvmutil.AppendToGlobal(c.mod, "tinygo.indirect-abi", llvmFn) } if strings.HasPrefix(c.Triple, "wasm") { // C functions without prototypes like this: diff --git a/compiler/testdata/aggregate-abi.go b/compiler/testdata/aggregate-abi.go new file mode 100644 index 0000000000..5f1cd57a35 --- /dev/null +++ b/compiler/testdata/aggregate-abi.go @@ -0,0 +1,119 @@ +package main + +type aggregateValue [600]int32 + +type directAggregate [499]int32 + +type boundaryAggregate [500]int32 + +type moderateAggregate [400]int32 + +type interfaceBoundaryAggregate [998]int32 + +//go:noinline +func readDirectAggregates(x, y directAggregate) int32 { + return x[0] + y[len(y)-1] +} + +//go:noinline +func readLimitAggregates(x, y directAggregate) (int32, int32) { + return x[0], y[0] +} + +//go:noinline +func readBoundaryAggregates(x, y boundaryAggregate) int32 { + return x[0] + y[len(y)-1] +} + +//go:noinline +func readAggregates(x, y aggregateValue) int32 { + return x[0] + y[len(y)-1] +} + +//go:noinline +func readSingleAggregate(value aggregateValue) int32 { + return value[0] +} + +//go:noinline +func readThreeAggregates(x, y, z moderateAggregate) int32 { + return x[0] + y[0] + z[0] +} + +//go:noinline +func readResultBudget(x directAggregate, y boundaryAggregate) (int32, int32) { + return x[0], y[0] +} + +func selectAggregate(cond bool, x, y aggregateValue) int32 { + selected := x + if cond { + selected = y + } + return readSingleAggregate(selected) +} + +func callAggregates(x, y aggregateValue) int32 { + return readAggregates(x, y) +} + +func callAggregateFunction(fn func(aggregateValue, aggregateValue) int32, x, y aggregateValue) int32 { + return fn(x, y) +} + +type aggregateReceiver struct{} + +type aggregateInterface interface { + read(aggregateValue, aggregateValue) int32 +} + +type boundaryInterface interface { + readBoundary(interfaceBoundaryAggregate) int32 +} + +//go:noinline +func (aggregateReceiver) read(x, y aggregateValue) int32 { + return x[0] + y[len(y)-1] +} + +func callAggregateMethod(receiver aggregateReceiver, x, y aggregateValue) int32 { + return receiver.read(x, y) +} + +func callBoundAggregateMethod(receiver aggregateReceiver, x, y aggregateValue) int32 { + method := receiver.read + return method(x, y) +} + +func callAggregateInterface(receiver aggregateInterface, x, y aggregateValue) int32 { + return receiver.read(x, y) +} + +func (aggregateReceiver) readBoundary(value interfaceBoundaryAggregate) int32 { + return value[0] +} + +func callBoundaryInterface(receiver boundaryInterface, value interfaceBoundaryAggregate) int32 { + return receiver.readBoundary(value) +} + +func deferAggregates(x, y aggregateValue) { + defer readAggregates(x, y) +} + +func deferAggregateFunction(fn func(aggregateValue, aggregateValue) int32, x, y aggregateValue) { + defer fn(x, y) +} + +func goAggregates(x, y aggregateValue) { + go readAggregates(x, y) +} + +func goAggregateFunction(fn func(aggregateValue, aggregateValue) int32, x, y aggregateValue) { + go fn(x, y) +} + +//export readAggregateExport +func readAggregateExport(value aggregateValue) int32 { + return value[0] +} diff --git a/compiler/testdata/aggregate-export-abi.go b/compiler/testdata/aggregate-export-abi.go new file mode 100644 index 0000000000..7081788993 --- /dev/null +++ b/compiler/testdata/aggregate-export-abi.go @@ -0,0 +1,43 @@ +package main + +type exportedAggregateParamMethod struct{} + +//export exportedAggregateParamMethodCall +func (exportedAggregateParamMethod) call(value [600]int32, other [600]int32) int32 { + return value[0] + other[len(other)-1] +} + +//export exportedOversizedAggregate +func exportedOversizedAggregate(value [1001]int32) { +} + +type exportedAggregateResultMethod struct{} + +//export exportedAggregateResultMethodCall +func (exportedAggregateResultMethod) call() [1025]int32 { + return [1025]int32{} +} + +type exportedLargeReceiver [600]int32 + +//export exportedLargeReceiverCall +func (receiver exportedLargeReceiver) call(value [399]int32) int32 { + return receiver[0] + value[0] +} + +func exerciseExportedAggregateMethods() { + var paramMethod interface { + call([600]int32, [600]int32) int32 + } = exportedAggregateParamMethod{} + paramMethod.call([600]int32{}, [600]int32{}) + + var resultMethod interface { + call() [1025]int32 + } = exportedAggregateResultMethod{} + resultMethod.call() + + var receiverMethod interface { + call([399]int32) int32 + } = exportedLargeReceiver{} + receiverMethod.call([399]int32{}) +} diff --git a/transform/interface-lowering.go b/transform/interface-lowering.go index 9c1adf7247..958baa36b9 100644 --- a/transform/interface-lowering.go +++ b/transform/interface-lowering.go @@ -29,6 +29,7 @@ package transform // compiler does it: https://research.swtch.com/interfaces import ( + "go/scanner" "sort" "strings" @@ -269,6 +270,30 @@ func (p *lowerInterfacesPass) run() error { }) } + // Report incompatible exported methods before defining any invoke thunks. + var abiErrors scanner.ErrorList + seenABIErrors := make(map[llvm.Value]struct{}) + for _, fn := range interfaceInvokeFunctions { + methodsAttr := fn.GetStringAttributeAtIndex(-1, "tinygo-methods") + invokeAttr := fn.GetStringAttributeAtIndex(-1, "tinygo-invoke") + itf := p.interfaces[methodsAttr.GetStringValue()] + signature := itf.signatures[invokeAttr.GetStringValue()] + for _, typ := range itf.types { + function := typ.getMethod(signature).function + if attr := function.GetStringAttributeAtIndex(-1, "tinygo-interface-abi-error"); !attr.IsNil() { + if _, ok := seenABIErrors[function]; ok { + continue + } + err := errorAt(function, attr.GetStringValue()) + abiErrors = append(abiErrors, &err) + seenABIErrors[function] = struct{}{} + } + } + } + if len(abiErrors) != 0 { + return abiErrors + } + // Define all interface invoke thunks. for _, fn := range interfaceInvokeFunctions { methodsAttr := fn.GetStringAttributeAtIndex(-1, "tinygo-methods") diff --git a/transform/optimizer.go b/transform/optimizer.go index 6158383835..7c646263c1 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -130,17 +130,6 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { } } - if config.Scheduler() == "none" { - // Check for any goroutine starts. - if start := mod.NamedFunction("internal/task.start"); !start.IsNil() && len(getUses(start)) > 0 { - errs := []error{} - for _, call := range getUses(start) { - errs = append(errs, errorAt(call, "attempted to start a goroutine without a scheduler")) - } - return errs - } - } - if speedLevel > 0 && config.PanicUnwind() == "asyncify" { AddUnwindAssumptions(mod) } @@ -177,6 +166,30 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { return []error{fmt.Errorf("could not build pass pipeline: %w", err)} } + // Keep these temporary roots through the ThinLTO pre-link pipeline, which + // can run argument promotion and reconstruct the oversized signatures. + if !mod.NamedGlobal("tinygo.indirect-abi").IsNil() { + llvmutil.RemoveGlobalReferences(mod, "llvm.used", "tinygo.indirect-abi") + cleanupOptions := llvm.NewPassBuilderOptions() + defer cleanupOptions.Dispose() + if err := mod.RunPasses("globaldce", llvm.TargetMachine{}, cleanupOptions); err != nil { + return []error{fmt.Errorf("could not run final globaldce pass: %w", err)} + } + } + + if config.Scheduler() == "none" { + // Check only after temporary ABI roots have been removed and dead code + // eliminated. Otherwise, a dead function kept alive solely to prevent + // argument promotion can produce a spurious scheduler error. + if start := mod.NamedFunction("internal/task.start"); !start.IsNil() && len(getUses(start)) > 0 { + errs := []error{} + for _, call := range getUses(start) { + errs = append(errs, errorAt(call, "attempted to start a goroutine without a scheduler")) + } + return errs + } + } + hasGCPass := MakeGCStackSlots(mod) if hasGCPass { if err := llvm.VerifyModule(mod, llvm.PrintMessageAction); err != nil {