Skip to content

plan9asm: support Go aliases and undeclared trampolines#21

Open
zhouguangyuan0718 wants to merge 5 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/tsgo-plan9asm-support
Open

plan9asm: support Go aliases and undeclared trampolines#21
zhouguangyuan0718 wants to merge 5 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/tsgo-plan9asm-support

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

go/types can materialize ordinary aliases as *types.Alias, and macOS/CGO assembly commonly defines file-local <> trampolines without Go declarations.

This change makes the Go-aware Plan 9 asm signature pass lower aliases, retain conservative signatures for undeclared local trampolines, and declare opaque external asm branch targets. The behavior is generic and is not coupled to a package path or a specific architecture; existing manual signatures and tail-jump inference remain authoritative where available.

Validation:

  • GOWORK=off GOCACHE=/tmp/plan9asm-pr-tsgo-cache go test ./...
  • GOWORK=off GOCACHE=/tmp/plan9asm-pr-tsgo-cover-cache go test -coverprofile=/tmp/plan9asm-pr-tsgo-cover.out ./...
  • Root coverage: 85.0%; cmd/plan9asmscan: 87.8%

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@zhouguangyuan0718
zhouguangyuan0718 marked this pull request as ready for review July 25, 2026 23:29

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review summary

This PR relaxes two previously-fatal "missing Go declaration" errors into best-effort FuncSig{Ret: Void} signatures, and adds *types.Alias lowering (via a reflect probe for Go 1.21 compatibility). The alias support and the localSigs two-pass design are clean, and ManualSig precedence is correctly preserved. Good test coverage was added for the headline scenarios.

The main concern is soundness of the widened Void fallback in addReferencedFuncSigs: it applies to every unresolved referenced symbol, not just the opaque C symbols described in the comment. See inline notes.

Findings

  • go_translate.go:270-274 — Unconditional Void fallback for referenced symbols downgrades a hard error into a silently-wrong zero-arg/void call (inline).
  • go_translate.go:186-191 / 224Void fallback for <> locals that may take args; redundant lazy init of localSigs (inline).
  • go_translate.go:538reflect.TypeOf on every call; can be moved to the default branch (inline).

No security (injection/authz/secret) issues in the diff. Documentation-accuracy notes about README.md/GOARCH mentioning arm were pre-existing and outside this diff, so omitted.

Comment thread go_translate.go
// assembly without a Go declaration. Their ABI is deliberately opaque
// to the Go type checker; keep a conservative declaration so the
// backend can still emit the trampoline. A ManualSig takes precedence.
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."

Comment thread go_translate.go
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.

Comment thread go_translate.go Outdated
// 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.
if rt := reflect.TypeOf(t); rt != nil && rt.Kind() == reflect.Ptr &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

reflect.TypeOf(t) plus the rt.Elem() string comparisons run on every call to this recursive, per-type hot-path function, including the common *types.Basic/*types.Pointer/*types.Named cases. Since *types.Alias is a distinct concrete type it never matches an existing case and would otherwise fall to default, so this probe can be moved into the default branch — behavior-identical, but the reflect + string compares no longer run for the types the switch already recognizes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant