Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions alias_go123_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build go1.23

package plan9asm

import "go/types"

func newAliasTypeForTest(obj *types.TypeName, rhs types.Type) types.Type {
return types.NewAlias(obj, rhs)
}
9 changes: 9 additions & 0 deletions alias_legacy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !go1.23

package plan9asm

import "go/types"

func newAliasTypeForTest(_ *types.TypeName, rhs types.Type) types.Type {
return rhs
}
56 changes: 48 additions & 8 deletions go_translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"go/ast"
"go/constant"
"go/types"
"reflect"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -158,6 +159,7 @@ func goSigsForAsmFile(pkg GoPackage, file *File, resolve func(sym string) string
}
b := goSigBuilder{
sigs: make(map[string]FuncSig, len(file.Funcs)),
localSigs: make(map[string]bool),
scope: pkg.Types.Scope(),
sz: sz,
linknames: goLinknameRemoteToLocal(pkg.Syntax),
Expand All @@ -175,11 +177,24 @@ func goSigsForAsmFile(pkg GoPackage, file *File, resolve func(sym string) string
if err := b.addReferencedFuncSigs(file); err != nil {
return nil, err
}
// File-local TEXT symbols (the Plan 9 `<>` form) are commonly used for
// assembly trampolines that are only reached through a raw function
// pointer. They intentionally have no Go declaration. Give any such
// symbols that could not be inferred from a tail jump a conservative
// zero-argument/void signature so they can still be emitted and referenced
// by DATA directives. A caller-provided ManualSig remains authoritative.
for resolved := range b.localSigs {
if _, ok := b.sigs[resolved]; ok {
continue
}
b.sigs[resolved] = FuncSig{Name: resolved, Ret: Void}
}
return b.sigs, nil
}

type goSigBuilder struct {
sigs map[string]FuncSig
localSigs map[string]bool
scope *types.Scope
sz types.Sizes
linknames map[string]string
Expand All @@ -191,7 +206,8 @@ type goSigBuilder struct {

func (b *goSigBuilder) addDeclaredFuncSigs(file *File) error {
for i := range file.Funcs {
sym := goStripABISuffix(file.Funcs[i].Sym)
textSym := file.Funcs[i].Sym
sym := goStripABISuffix(textSym)
resolved := b.resolve(sym)
if ms, ok := goLookupManualSig(b.manualSig, resolved); ok {
b.sigs[resolved] = ms
Expand All @@ -204,6 +220,13 @@ func (b *goSigBuilder) addDeclaredFuncSigs(file *File) error {
}
obj := b.scope.Lookup(declName)
if obj == nil {
if strings.HasSuffix(textSym, "<>") {
if b.localSigs == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two notes on this block and the related post-pass:

  1. Redundant lazy init. b.localSigs is already unconditionally initialized in goSigsForAsmFile (localSigs: make(map[string]bool)), the only production caller. This nil-check + lazy make only fires for hand-built goSigBuilders in tests, creating split-brain initialization. Prefer a single source of truth (drop the lazy init and init in the test, mirroring how sigs is handled).

  2. Void for <> locals that take args (post-pass at lines 186-191): a file-local <> TEXT symbol not inferred via a tail jump gets FuncSig{Ret: Void}. That sig is used to emit the function definition header too, so a <> helper that actually reads argument/frame registers is defined with an empty param frame — arg/frame loads resolve against nothing. The comment states these are "only reached through a raw function pointer," but the relaxation is unconditional; ManualSig is the only escape and no diagnostic tells the caller one is needed.

b.localSigs = make(map[string]bool)
}
b.localSigs[resolved] = true
continue
}
return fmt.Errorf("missing Go declaration for asm symbol %q", sym)
}
fn, ok := obj.(*types.Func)
Expand Down Expand Up @@ -235,15 +258,24 @@ func (b *goSigBuilder) addReferencedFuncSigs(file *File) error {
if _, ok := b.sigs[targetResolved]; ok {
continue
}
if !tailJump || !hasCallerSig {
if tailJump && hasCallerSig {
// Best-effort fallback for helper<> tail-jumps where the callee is an
// internal asm label with no Go declaration. Callers can override this
// via ManualSig when the inferred signature is not identical.
fs := callerSig
fs.Name = targetResolved
b.sigs[targetResolved] = fs
continue
}
if !tailJump {
// Keep ordinary unresolved calls untyped so the backend reports the
// missing declaration instead of silently dropping arguments/results.
continue
}
// Best-effort fallback for helper<> tail-jumps where the callee is an
// internal asm label with no Go declaration. Callers can override this
// via ManualSig when the inferred signature is not identical.
fs := callerSig
fs.Name = targetResolved
b.sigs[targetResolved] = fs
// An undeclared tail target can be an assembly trampoline reached
// through a raw function pointer. Its ABI is opaque to go/types; keep a
// conservative declaration so the trampoline can still be emitted.
b.sigs[targetResolved] = FuncSig{Name: targetResolved, Ret: Void}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old code skipped (continue) any referenced-but-unresolved symbol, so a non-tail CALL foo(SB) to an undeclared symbol failed loudly downstream (amd64_lower_branch.go:266 "amd64 call missing signature"). This else branch now assigns FuncSig{Ret: Void} (zero args) to any such symbol, not only cgo_import_dynamic/opaque-C symbols as the comment implies.

Consequence: a genuine CALL realfunc(SB) whose callee takes register args / returns a value, but has no discoverable Go declaration, now lowers as a zero-arg void call — register arguments are dropped and the return is not captured, i.e. a silently-wrong call instead of a diagnostic (a typo'd or forgotten decl no longer fails fast).

Suggest scoping this fallback to the intended cases (tail-jumps / recognizably-opaque C symbols) and letting genuine unresolved intra-ABI CALLs keep erroring — or at minimum surfacing the synthesized-Void symbols so callers can tell "intentional trampoline" from "missing decl."

}
}
return nil
Expand Down Expand Up @@ -547,6 +579,14 @@ func goLLVMTypeForType(t types.Type, goarch string) (LLVMType, error) {
case *types.Named:
return goLLVMTypeForType(tt.Underlying(), goarch)
default:
// *types.Alias was added after the oldest Go version supported by this
// module. Detect it by its stable reflect identity so the package still
// compiles with Go 1.21, while newer go/types implementations can lower
// an alias through its RHS just like a named type. Keep this probe in the
// fallback path so common concrete types avoid reflection overhead.
if reflect.TypeOf(t).String() == "*types.Alias" {
return goLLVMTypeForType(t.Underlying(), goarch)
}
return "", fmt.Errorf("unsupported type %s", t.String())
}
}
Expand Down
47 changes: 45 additions & 2 deletions go_translate_deep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ func TestGoTranslateTypeCoverage(t *testing.T) {
word32 := types.Typ[types.Int]
namedObj := types.NewTypeName(token.NoPos, nil, "MyInt", nil)
named := types.NewNamed(namedObj, types.Typ[types.Int32], nil)
aliasObj := types.NewTypeName(token.NoPos, nil, "MyUint64", nil)
alias := newAliasTypeForTest(aliasObj, types.Typ[types.Uint64])
for _, tc := range []struct {
typ types.Type
goarch string
Expand All @@ -130,6 +132,7 @@ func TestGoTranslateTypeCoverage(t *testing.T) {
{types.NewSlice(types.Typ[types.Byte]), "arm64", LLVMType("{ ptr, i64, i64 }"), true},
{types.NewInterfaceType(nil, nil), "amd64", LLVMType("{ ptr, ptr }"), true},
{named, "amd64", I32, true},
{alias, "arm64", I64, true},
{types.Typ[types.Complex64], "amd64", "", false},
{types.NewStruct(nil, nil), "amd64", "", false},
} {
Expand Down Expand Up @@ -203,8 +206,9 @@ func cmp(a, b int) int { return a }
file := &File{
Arch: ArchARM64,
Funcs: []Func{
{Sym: "·Plain", Instrs: []Instr{{Op: OpTEXT}, {Op: "CALL", Args: []Operand{{Kind: OpSym, Sym: "runtime·cmp(SB)"}}}, {Op: OpRET}}},
{Sym: "·Plain", Instrs: []Instr{{Op: OpTEXT}, {Op: "CALL", Args: []Operand{{Kind: OpSym, Sym: "runtime·cmp(SB)"}}}, {Op: "CALL", Args: []Operand{{Kind: OpSym, Sym: "undeclared_call(SB)"}}}, {Op: "B", Args: []Operand{{Kind: OpSym, Sym: "localtarget<>(SB)"}}}, {Op: OpRET}}},
{Sym: "localhelper<>", Instrs: []Instr{{Op: OpTEXT}, {Op: "B", Args: []Operand{{Kind: OpSym, Sym: "helper<>(SB)"}}}, {Op: OpRET}}},
{Sym: "localtarget<>"},
},
}
sigs, err := goSigsForAsmFile(pkg, file, testResolveSym("test/pkg"), "arm64", func(resolved string) (FuncSig, bool) {
Expand All @@ -216,11 +220,14 @@ func cmp(a, b int) int { return a }
if err != nil {
t.Fatalf("goSigsForAsmFile() error = %v", err)
}
for _, want := range []string{"test/pkg.Plain", "runtime.cmp", "test/pkg.localhelper"} {
for _, want := range []string{"test/pkg.Plain", "runtime.cmp", "test/pkg.localhelper", "test/pkg.localtarget"} {
if _, ok := sigs[want]; !ok {
t.Fatalf("missing signature %q", want)
}
}
if _, ok := sigs["test/pkg.undeclared_call"]; ok {
t.Fatalf("ordinary unresolved CALL unexpectedly received a synthesized signature")
}

builder := goSigBuilder{
sigs: map[string]FuncSig{},
Expand Down Expand Up @@ -259,6 +266,21 @@ var helper int
if err := badBuilder.addDeclaredFuncSigs(badFile); err == nil {
t.Fatalf("addDeclaredFuncSigs(non-func) unexpectedly succeeded")
}
if err := builder.addDeclaredFuncSigs(&File{Funcs: []Func{{Sym: "·missing"}}}); err == nil {
t.Fatalf("addDeclaredFuncSigs(missing) unexpectedly succeeded")
}
nilLocalBuilder := goSigBuilder{
sigs: map[string]FuncSig{},
localSigs: nil,
scope: scope,
sz: types.SizesFor("gc", "arm64"),
pkgPath: "test/pkg",
resolve: testResolveSym("test/pkg"),
goarch: "arm64",
}
if err := nilLocalBuilder.addDeclaredFuncSigs(&File{Funcs: []Func{{Sym: "missing_local<>"}}}); err != nil {
t.Fatalf("addDeclaredFuncSigs(local with nil map) error = %v", err)
}
if _, err := goSigsForAsmFile(pkg, file, testResolveSym("test/pkg"), "madeup", nil); err == nil {
t.Fatalf("goSigsForAsmFile(madeup) unexpectedly succeeded")
}
Expand Down Expand Up @@ -320,3 +342,24 @@ RET
t.Fatalf("goExpandConsts() = %q", expanded)
}
}

func TestTranslateGoModuleUndeclaredLocalTrampoline(t *testing.T) {
pkg := mustGoPackage(t, "test/pkg", `package testpkg
func F() {}
`)
tr, err := TranslateGoModule(pkg, []byte(`TEXT local_trampoline<>(SB),NOSPLIT,$0-0
JMP external_symbol(SB)
`), GoModuleOptions{
GOARCH: "arm64",
TargetTriple: "aarch64-unknown-linux-gnu",
ResolveSym: testResolveSym("test/pkg"),
})
if err != nil {
t.Fatalf("TranslateGoModule(undeclared local trampoline) error = %v", err)
}
defer tr.Module.Dispose()
sig, ok := tr.Signatures["test/pkg.local_trampoline"]
if !ok || sig.Ret != Void || len(sig.Args) != 0 {
t.Fatalf("local trampoline signature = %#v, want void()", sig)
}
}
Loading