mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* 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]>
247 lines
7.5 KiB
Go
247 lines
7.5 KiB
Go
package codegen
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"path"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// wasmFuncBodyName matches the identifiers codegen uses for wasm
|
|
// function bodies — `Fn<N>` in multi-package mode, `fn<N>` in
|
|
// single-package mode. Anything else (export wrappers, helpers,
|
|
// New, init functions) keeps its name unchanged and falls through
|
|
// to the shared file.
|
|
// The `l<blockID>` suffix names a loop outlined from that function
|
|
// (see internal/ssa/outline.go); those bodies route with their parent.
|
|
var wasmFuncBodyName = regexp.MustCompile(`^[Ff]n\d+(l\d+|rows)?$`)
|
|
|
|
// isLinknameTrampolineBody reports whether body is the single-stmt
|
|
// forwarding pattern emitLinknameForwards emits for cross-chunk
|
|
// trampolines. That pattern is either `return _x<Name>(...)` (for
|
|
// functions with a result) or `_x<Name>(...)` (for void functions),
|
|
// where `_x<Name>` is the linkname-aliased helper. The check is
|
|
// shape-only so it stays cheap and resists incidental matches.
|
|
func isLinknameTrampolineBody(body *ast.BlockStmt) bool {
|
|
if body == nil || len(body.List) != 1 {
|
|
return false
|
|
}
|
|
var call *ast.CallExpr
|
|
switch s := body.List[0].(type) {
|
|
case *ast.ReturnStmt:
|
|
if len(s.Results) != 1 {
|
|
return false
|
|
}
|
|
c, ok := s.Results[0].(*ast.CallExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
call = c
|
|
case *ast.ExprStmt:
|
|
c, ok := s.X.(*ast.CallExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
call = c
|
|
default:
|
|
return false
|
|
}
|
|
id, ok := call.Fun.(*ast.Ident)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(id.Name, "_x")
|
|
}
|
|
|
|
// splitForAsm parses the Go source the translator produced,
|
|
// partitions its top-level declarations into shared vs body, and
|
|
// re-emits each half as a self-contained Go file with the right
|
|
// build tag and minimum-required imports. fallbackTag is the
|
|
// `//go:build` directive for the body file: normally
|
|
// `//go:build !amd64 && !arm64` so it is dormant on the asm-target
|
|
// GOARCHs (where the asm files supply the bodies); in PureOnly mode
|
|
// the empty string, so the pure bodies compile everywhere.
|
|
func splitForAsm(src []byte, pkg string, fallbackTag string) (shared, fallback []byte, err error) {
|
|
fset := token.NewFileSet()
|
|
// SkipObjectResolution: this split only reshuffles top-level decls
|
|
// and never inspects ident.Obj / file.Scope, so the parser's
|
|
// (deprecated) identifier resolver is dead work here — and its
|
|
// hard-coded maxScopeDepth=1000 bailout would reject legitimately
|
|
// deep control flow (e.g. the nested dispatch setjmp/longjmp
|
|
// translation emits) that gc itself compiles fine.
|
|
file, err := parser.ParseFile(fset, "translate.go", src, parser.ParseComments|parser.SkipObjectResolution)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("parse: %w", err)
|
|
}
|
|
|
|
var sharedDecls, bodyDecls []ast.Decl
|
|
var importDecls []*ast.GenDecl
|
|
for _, d := range file.Decls {
|
|
// Pull import decls aside so we can recompute per-file
|
|
// later. Anything else slots into one of the two buckets.
|
|
if g, ok := d.(*ast.GenDecl); ok && g.Tok == token.IMPORT {
|
|
importDecls = append(importDecls, g)
|
|
continue
|
|
}
|
|
if fd, ok := d.(*ast.FuncDecl); ok && fd.Recv == nil && fd.Name != nil &&
|
|
wasmFuncBodyName.MatchString(fd.Name.Name) {
|
|
// Three Fn-named declarations exist in multi-pkg
|
|
// linkname-split mode:
|
|
// 1. Forward declarations (Body == nil) emitted by
|
|
// single-pkg linkname forwards (legacy path).
|
|
// 2. Cross-chunk trampolines (Body != nil; calls a
|
|
// _x-prefixed linkname helper) emitted by
|
|
// emitLinknameForwards. The local symbol must
|
|
// exist on every arch so asm `·Fn48(SB)` resolves
|
|
// to it.
|
|
// 3. Real own-chunk function bodies (Body != nil;
|
|
// arbitrary code). These belong only in the
|
|
// pure-Go fallback file; the asm bundle provides
|
|
// them on amd64/arm64.
|
|
//
|
|
// Detect trampolines by checking whether the body is a
|
|
// single statement that delegates to a `_x`-prefixed
|
|
// helper — every trampoline emitLinknameForwards
|
|
// produces matches this shape and nothing else does.
|
|
if fd.Body == nil || isLinknameTrampolineBody(fd.Body) {
|
|
sharedDecls = append(sharedDecls, fd)
|
|
} else {
|
|
bodyDecls = append(bodyDecls, fd)
|
|
}
|
|
continue
|
|
}
|
|
sharedDecls = append(sharedDecls, d)
|
|
}
|
|
|
|
imports := collectImports(importDecls)
|
|
|
|
sharedSrc, err := renderFile(fset, pkg, "", sharedDecls, imports)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("render shared: %w", err)
|
|
}
|
|
fallbackSrc, err := renderFile(fset, pkg, fallbackTag, bodyDecls, imports)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("render fallback: %w", err)
|
|
}
|
|
return sharedSrc, fallbackSrc, nil
|
|
}
|
|
|
|
// importInfo is one parsed import: its path and (when present) its
|
|
// alias. The alias is what the Go source refers to in
|
|
// `<alias>.<member>` selector expressions; an empty alias means the
|
|
// default package name, computed via path.Base.
|
|
type importInfo struct {
|
|
path string
|
|
alias string
|
|
}
|
|
|
|
func (ii importInfo) ident() string {
|
|
if ii.alias != "" {
|
|
return ii.alias
|
|
}
|
|
return path.Base(ii.path)
|
|
}
|
|
|
|
func collectImports(genDecls []*ast.GenDecl) []importInfo {
|
|
var out []importInfo
|
|
for _, g := range genDecls {
|
|
for _, spec := range g.Specs {
|
|
s, ok := spec.(*ast.ImportSpec)
|
|
if !ok || s.Path == nil {
|
|
continue
|
|
}
|
|
p, err := strconv.Unquote(s.Path.Value)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ii := importInfo{path: p}
|
|
if s.Name != nil {
|
|
ii.alias = s.Name.Name
|
|
}
|
|
out = append(out, ii)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// renderFile formats one file: build tag (if non-empty), package
|
|
// header, an import block restricted to the packages the file's
|
|
// decls actually reference, and the decls themselves.
|
|
//
|
|
// Imports are recomputed per file because Go rejects unused
|
|
// imports: a file that doesn't reference `math` cannot include
|
|
// its import even though the original combined file did.
|
|
//
|
|
// Blank-imported packages (alias `_`) are kept regardless of
|
|
// reference because their import is for side effects (init()
|
|
// functions) — they don't need to be referenced by name.
|
|
func renderFile(fset *token.FileSet, pkg, buildTag string, decls []ast.Decl, imports []importInfo) ([]byte, error) {
|
|
used := computeUsedIdents(decls)
|
|
var keep []importInfo
|
|
for _, ii := range imports {
|
|
if ii.alias == "_" {
|
|
keep = append(keep, ii)
|
|
continue
|
|
}
|
|
if used[ii.ident()] {
|
|
keep = append(keep, ii)
|
|
}
|
|
}
|
|
sort.Slice(keep, func(i, j int) bool { return keep[i].path < keep[j].path })
|
|
|
|
file := &ast.File{Name: ast.NewIdent(pkg)}
|
|
if len(keep) > 0 {
|
|
gd := &ast.GenDecl{Tok: token.IMPORT}
|
|
for _, ii := range keep {
|
|
spec := &ast.ImportSpec{
|
|
Path: &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(ii.path)},
|
|
}
|
|
if ii.alias != "" {
|
|
spec.Name = ast.NewIdent(ii.alias)
|
|
}
|
|
gd.Specs = append(gd.Specs, spec)
|
|
}
|
|
file.Decls = append(file.Decls, gd)
|
|
}
|
|
file.Decls = append(file.Decls, decls...)
|
|
|
|
var buf bytes.Buffer
|
|
if buildTag != "" {
|
|
fmt.Fprintf(&buf, "%s\n\n", buildTag)
|
|
}
|
|
if err := format.Node(&buf, fset, file); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// computeUsedIdents walks decls and returns the set of identifier
|
|
// names that appear on the left side of a SelectorExpr — i.e. the
|
|
// package qualifiers in `<id>.<member>` expressions. This is the
|
|
// set of imports the decls require.
|
|
func computeUsedIdents(decls []ast.Decl) map[string]bool {
|
|
used := map[string]bool{}
|
|
for _, d := range decls {
|
|
ast.Inspect(d, func(n ast.Node) bool {
|
|
sel, ok := n.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
id, ok := sel.X.(*ast.Ident)
|
|
if !ok {
|
|
return true
|
|
}
|
|
used[id.Name] = true
|
|
return true
|
|
})
|
|
}
|
|
return used
|
|
}
|