Files
9b2c9ccddd gcasm: fix cross-chunk symbol gaps in the direct-call path (#64)
* gcasm: fix cross-chunk symbol gaps in the direct-call path

Follow-ups to the direct cross-chunk asm calls change (#61):

- The direct-asm resolvers (CalleeSymbol / MemHelperSymbol) trimmed
  calleeSig's leading "·" and let asmgen's goAsmSymbol re-derive the
  CALL operand. A spelled remote symbol ("github·com∕...∕p2·Fn0")
  has no ASCII "." for goAsmSymbol to split on, so it was re-wrapped
  as a current-package reference ("CALL ·github·com∕...(SB)") that
  can never link — any direct-asm-retained function making a
  resolved cross-chunk call produced an undefined symbol at consumer
  link time. The resolver contract now carries the exact plan 9
  operand (without the "(SB)" suffix) and asmgen emits it verbatim.

- plan9AsmPathSafe accepted digit-leading import paths ("4d63.com/...",
  "0xacab.org/..."), but the plan 9 asm lexer accepts digits only
  after the first identifier rune, so the spelled CALL would fail to
  assemble. The predicate now requires a letter or "_" at position 0.

- That predicate existed as two byte-for-byte copies (codegen's
  isPlan9AsmSafe, gcasm's plan9AsmPathSafe) under a comment claiming
  an import cycle prevents sharing; no such cycle exists. Both
  packages now consult one exported asmgen.Plan9AsmPathSafe, so the
  linkname alias shape and the direct-CALL / gcasmABI0Keep anchor
  gates can never disagree.

- buildPkg now verifies every captured fn leaves the emission loop
  either with a TEXT body or registered as a pure-Go fallback. A
  future degrade path that forgets fallbackNames would otherwise
  surface only as a link error in the consumer build, with no
  Build-time diagnostic; it is a Build error naming the fn now.

The new cg_crosscall_directasm fixture keeps every op native (no
rotl/rotr, which lower to helper CALLs the direct-asm emitter's
ForbidCalls refuses) so the retained functions actually emit via
asmgen; its regression test fails against the pre-fix resolvers with
the mangled current-package spelling in the emitted bundle.

Co-Authored-By: Claude Fable 5 <[email protected]>

* asmgen: restructure the first-rune check as a switch to satisfy staticcheck

golangci-lint's staticcheck (QF1001) flagged the negated compound
condition in Plan9AsmPathSafe; the positive-switch form matches the
per-byte loop below it. No behavior change.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-09-02 20:02:20 +09:00

54 lines
1.8 KiB
Go

package asmgen
// Plan9AsmPathSafe reports whether an import path can be embedded in
// a plan 9 asm symbol operand (via the U+00B7 / U+2215 spelling that
// goAsmSymbol applies — see its doc comment for the lexer mechanics).
//
// The asm lexer (src/cmd/asm/internal/lex/tokenizer.go: isIdentRune)
// accepts letters, "_", U+00B7 and U+2215 as identifier runes, plus
// digits AFTER the first rune, and nothing else. Two consequences:
//
// - "." and "/" are the only ASCII punctuation an operand can
// carry (through the Unicode substitution); anything else Go
// module paths permit ("-", "+", "~", " ", ...) has no plan 9
// counterpart and is unrepresentable — there is no escape
// syntax.
//
// - A spelled path lexes as ONE identifier starting at the path's
// first byte, so a digit-leading host ("4d63.com/...",
// "0xacab.org/...") is unrepresentable too: position 0 of the
// identifier would be a digit and the token lexes as a number.
//
// The empty path is rejected so callers treat it as unsafe.
//
// This single function is the contract both halves of a generated
// bundle consult: internal/codegen keys the decl-only vs
// wrapper-pair linkname alias shape off it, and internal/gcasm keys
// the direct-CALL vs gcasmFwd forwarding shape plus the
// gcasmABI0Keep anchor off it. Keeping one copy is what guarantees
// the two decisions can never disagree.
func Plan9AsmPathSafe(path string) bool {
if path == "" {
return false
}
switch c := path[0]; {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c == '_':
default:
return false
}
for i := 1; i < len(path); i++ {
c := path[i]
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '_' || c == '.' || c == '/':
default:
return false
}
}
return true
}