diff --git a/cmd/gasm/asmhdr.go b/cmd/gasm/asmhdr.go new file mode 100644 index 0000000..275d02c --- /dev/null +++ b/cmd/gasm/asmhdr.go @@ -0,0 +1,354 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package main + +import ( + "errors" + "fmt" + "go/ast" + "go/build" + "go/constant" + "go/parser" + "go/token" + "go/types" + "os" + "path/filepath" + "regexp" + "strings" + + "sourcedock.dev/petrbalvin/gasm-devkit/arch" +) + +// go_asm.h is the header the Go compiler writes for every package that +// carries assembly (the compiler's -asmhdr output): "#define const_NAME +// value" for each package constant, and for each named struct type +// "#define TYPE__size size" plus one "#define TYPE_field offset" per field. +// GOROOT assembly includes it, and a standalone assembler has no compiler +// to have produced it, so gasm generates the equivalent itself: the package +// the .s file lives in is parsed and type-checked here, with the target +// architecture's own sizes, and the same defines are written out. +// +// The emitter mirrors cmd/compile's dumpasmhdr exactly: constants come out +// as "const_NAME", struct entries as "NAME__size" followed by the fields in +// declaration order, blank names are skipped, and float and complex +// constants are omitted (the assembler carries integers, bools and strings +// only). Aliases to structs are emitted, generic types are not: they have +// no fixed size. A define the assembly references but this header does not +// carry surfaces later as the assembler's own "undefined" diagnostic naming +// the define, which is the honest failure. + +// goAsmInclude matches the #include "go_asm.h" directive, tolerant of +// whitespace, so the wiring knows which files need a generated header +// before the preprocessor runs and would report the header as missing. +var goAsmInclude = regexp.MustCompile(`(?m)^\s*#\s*include\s+"go_asm\.h"`) + +// needsGoAsmHeader reports whether src includes go_asm.h. +func needsGoAsmHeader(src string) bool { + return goAsmInclude.MatchString(src) +} + +// goAsmHeaderResolved reports whether the include of go_asm.h from a file in +// asmDir already resolves: to a header in the package directory itself, or +// in one of the -I directories, the way the preprocessor searches. Only an +// unresolved include is generated for; a header someone placed by hand is +// the tool the author chose, and it also wins the preprocessor's own search +// order, so generating a second copy would be dead weight at best. +func goAsmHeaderResolved(asmDir string, dirs []string) bool { + candidates := []string{filepath.Join(asmDir, "go_asm.h")} + for _, d := range dirs { + candidates = append(candidates, filepath.Join(d, "go_asm.h")) + } + for _, candidate := range candidates { + if st, err := os.Stat(candidate); err == nil && !st.IsDir() { + return true + } + } + return false +} + +// generateGoAsmHeader type-checks the Go package in pkgDir for goarch, +// writes its go_asm.h equivalent into dir, and returns dir. The caller +// owns the directory and its removal. +func generateGoAsmHeader(pkgDir, goarch, dir string) (string, error) { + imp := newSourceImporter(goarch) + if imp.sizes == nil { + return "", fmt.Errorf("go_asm.h: unknown GOARCH %q", goarch) + } + bp, err := imp.ctxt.ImportDir(pkgDir, 0) + if err != nil { + return "", fmt.Errorf("go_asm.h for GOARCH %s in %s: %w", goarch, pkgDir, err) + } + files, errs := imp.parse(bp) + if len(errs) > 0 { + return "", fmt.Errorf("go_asm.h for GOARCH %s in %s: %s", goarch, pkgDir, errorList(errs)) + } + _, info, errs := imp.checkPackage(bp, files) + if len(errs) > 0 { + return "", fmt.Errorf("go_asm.h for GOARCH %s in %s: package does not type-check: %s", goarch, pkgDir, errorList(errs)) + } + + var b strings.Builder + fmt.Fprintf(&b, "// generated by gasm from package %s (GOARCH %s)\n\n", bp.Name, goarch) + // Files in the build's own order and declarations in source order: the + // same walk the compiler's reader makes, so the header reads the same + // way the toolchain's does. Order carries no meaning to the assembler + // (defines form a table), only to a human diffing against one. + for _, f := range files { + for _, decl := range f.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + for _, spec := range gd.Specs { + switch gd.Tok { + case token.CONST: + vs, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for _, name := range vs.Names { + emitConst(&b, info.Defs[name], name.Name) + } + case token.TYPE: + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + emitStruct(&b, imp.sizes, info.Defs[ts.Name], ts.Name.Name) + } + } + } + } + + if err := os.MkdirAll(dir, 0o755); err != nil { + return "", fmt.Errorf("go_asm.h for GOARCH %s in %s: %w", goarch, pkgDir, err) + } + out := filepath.Join(dir, "go_asm.h") + if err := os.WriteFile(out, []byte(b.String()), 0o644); err != nil { + return "", fmt.Errorf("go_asm.h for GOARCH %s in %s: %w", goarch, pkgDir, err) + } + return dir, nil +} + +// emitConst writes one const define, skipping what the toolchain skips: +// blank names, and float and complex values the assembler has no syntax for. +func emitConst(b *strings.Builder, obj types.Object, name string) { + c, ok := obj.(*types.Const) + if !ok || name == "_" { + return + } + switch c.Val().Kind() { + case constant.Float, constant.Complex, constant.Unknown: + return + } + fmt.Fprintf(b, "#define const_%s %s\n", name, c.Val().ExactString()) +} + +// emitStruct writes one named struct type's size and field offsets, +// skipping what the toolchain skips: blank names, non-struct types, and +// generic types, whose size depends on their instantiation. +func emitStruct(b *strings.Builder, sizes types.Sizes, obj types.Object, name string) { + tn, ok := obj.(*types.TypeName) + if !ok || name == "_" { + return + } + t := types.Unalias(tn.Type()) + // Generic types are spelled *types.Named with a type-parameter list; + // a plain struct type or an instantiated one carries none. + if named, ok := t.(*types.Named); ok && named.TypeParams().Len() > 0 { + return + } + st, ok := t.Underlying().(*types.Struct) + if !ok { + return + } + fmt.Fprintf(b, "#define %s__size %d\n", name, sizes.Sizeof(t)) + fields := make([]*types.Var, st.NumFields()) + for i := range st.NumFields() { + fields[i] = st.Field(i) + } + for i, off := range sizes.Offsetsof(fields) { + fld := fields[i] + if fld.Name() == "_" { + continue + } + fmt.Fprintf(b, "#define %s_%s %d\n", name, fld.Name(), off) + } +} + +// errorList renders at most three errors, enough to say what is wrong +// without burying the diagnostic the caller actually reads. +func errorList(errs []error) string { + if len(errs) > 3 { + errs = errs[:3] + } + msgs := make([]string, len(errs)) + for i, err := range errs { + msgs[i] = err.Error() + } + return strings.Join(msgs, "; ") +} + +// sourceImporter type-checks imported packages from source with the target +// architecture's sizes. go/importer's "source" importer pins the host +// GOARCH, which would lay out imported types (internal/cpu, internal/abi) +// for the wrong target on a cross-architecture header, so the recursion is +// carried here with one build context and one sizes instance per +// architecture. +type sourceImporter struct { + fset *token.FileSet + ctxt *build.Context + sizes types.Sizes + pkgs map[string]*types.Package +} + +// newSourceImporter returns the importer for one target architecture. +// Cgo is disabled so the file set is deterministic and independent of the +// host's C toolchain: cgo-tagged files drop out of the build exactly as +// they do from a CGO_ENABLED=0 build, whose assembly is what gasm targets. +func newSourceImporter(goarch string) *sourceImporter { + ctxt := new(build.Context) + *ctxt = build.Default + ctxt.GOARCH = goarch + ctxt.CgoEnabled = false + return &sourceImporter{ + fset: token.NewFileSet(), + ctxt: ctxt, + sizes: types.SizesFor("gc", goarch), + pkgs: map[string]*types.Package{}, + } +} + +// Import type-checks one imported package and memoises it. "unsafe" must +// resolve to go/types' own package, never to the source in GOROOT/src/unsafe: +// the source declares Sizeof and Offsetof as ordinary functions over +// ArbitraryType, and checking against that signature rejects half the +// unsafe arithmetic the gc compiler accepts, which is exactly the divergence +// srcimporter guards against the same way. +func (im *sourceImporter) Import(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + if p, ok := im.pkgs[path]; ok { + return p, nil + } + bp, err := im.ctxt.Import(path, "", 0) + if err != nil { + return nil, err + } + files, errs := im.parse(bp) + if len(errs) > 0 { + return nil, errors.New(errorList(errs)) + } + pkg, _, _ := im.checkPackage(bp, files) + im.pkgs[path] = pkg + return pkg, nil +} + +// parse reads the build package's Go files. Import-level failures (no Go +// files for the target, unreadable files) come back as errors, and the +// type-check decides the rest. +func (im *sourceImporter) parse(bp *build.Package) ([]*ast.File, []error) { + if len(bp.GoFiles) == 0 { + return nil, []error{fmt.Errorf("no Go source files for GOOS=%s GOARCH=%s", im.ctxt.GOOS, im.ctxt.GOARCH)} + } + var ( + files []*ast.File + errs []error + ) + for _, name := range bp.GoFiles { + f, err := parser.ParseFile(im.fset, filepath.Join(bp.Dir, name), nil, parser.SkipObjectResolution) + if err != nil { + errs = append(errs, err) + continue + } + files = append(files, f) + } + return files, errs +} + +// checkPackage type-checks one package's files with the importer's sizes, +// recording every error: a header from a package that does not type-check +// could silently mis-state an offset, so the caller refuses the header +// rather than trusting it. The returned Defs map backs the root package's +// emission walk; imports only need the checked package itself. +func (im *sourceImporter) checkPackage(bp *build.Package, files []*ast.File) (*types.Package, *types.Info, []error) { + var errs []error + conf := &types.Config{ + Importer: im, + Sizes: im.sizes, + Error: func(err error) { errs = append(errs, err) }, + } + info := &types.Info{Defs: map[*ast.Ident]types.Object{}} + pkg, _ := conf.Check(bp.ImportPath, im.fset, files, info) + return pkg, info, errs +} + +// asmhdrCache generates one go_asm.h per package directory and target +// architecture under one temp root, for callers that assemble many files +// (the corpus audit). Failures are cached too: a package that does not +// type-check must not be re-checked once per file. +type asmhdrCache struct { + root string + dirs map[string]string // "pkgDir\x00goarch" -> directory holding go_asm.h + errs map[string]error +} + +func newAsmhdrCache() (*asmhdrCache, error) { + root, err := os.MkdirTemp("", "gasm-asmhdr") + if err != nil { + return nil, err + } + return &asmhdrCache{root: root, dirs: map[string]string{}, errs: map[string]error{}}, nil +} + +// dirFor returns the directory holding the generated go_asm.h for pkgDir +// and goarch, generating it on first use. +func (c *asmhdrCache) dirFor(pkgDir, goarch string) (string, error) { + key := pkgDir + "\x00" + goarch + if dir, ok := c.dirs[key]; ok { + return dir, nil + } + if err, ok := c.errs[key]; ok { + return "", err + } + dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s", len(c.dirs), goarch)) + if _, err := generateGoAsmHeader(pkgDir, goarch, dir); err != nil { + c.errs[key] = err + return "", err + } + c.dirs[key] = dir + return dir, nil +} + +// close removes the temp root. +func (c *asmhdrCache) close() { os.RemoveAll(c.root) } + +// ensureGoAsmHeader prepares the include directory a file that includes +// go_asm.h needs: the generated header for the package in path's directory, +// for the file's target architecture. It reports a usage error when the +// architecture cannot be determined, and passes through the generator's +// diagnostics, which name the package. +func ensureGoAsmHeader(path string, target arch.Arch, cache *asmhdrCache) (string, func(), error) { + if path == "-" { + return "", nil, errors.New("cannot generate go_asm.h for standard input (no package directory)") + } + if target == arch.Unknown { + return "", nil, errors.New("a file that includes go_asm.h needs a target architecture: name the file _.s or pass -GOARCH") + } + if cache != nil { + dir, err := cache.dirFor(filepath.Dir(path), goarchName(target)) + return dir, func() {}, err + } + root, err := os.MkdirTemp("", "gasm-asmhdr") + if err != nil { + return "", nil, err + } + dir, err := generateGoAsmHeader(filepath.Dir(path), goarchName(target), root) + if err != nil { + os.RemoveAll(root) + return "", nil, err + } + return dir, func() { os.RemoveAll(root) }, nil +} diff --git a/cmd/gasm/asmhdr_test.go b/cmd/gasm/asmhdr_test.go new file mode 100644 index 0000000..370010e --- /dev/null +++ b/cmd/gasm/asmhdr_test.go @@ -0,0 +1,311 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// writePkg lays out a minimal Go package in a temp directory. +func writePkg(t *testing.T, files map[string]string) string { + t.Helper() + dir := t.TempDir() + for name, src := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + } + return dir +} + +// generateFor generates the header for dir and returns its text. +func generateFor(t *testing.T, dir, goarch string) string { + t.Helper() + hdrDir, err := generateGoAsmHeader(dir, goarch, t.TempDir()) + if err != nil { + t.Fatalf("generateGoAsmHeader(%q, %s): %v", dir, goarch, err) + } + b, err := os.ReadFile(filepath.Join(hdrDir, "go_asm.h")) + if err != nil { + t.Fatal(err) + } + return string(b) +} + +func TestGenerateGoAsmHeaderShape(t *testing.T) { + dir := writePkg(t, map[string]string{"sample.go": `package sample + +const bufSize = 1024 + +const ( + a = iota * 8 + b + c +) + +const ( + strConst = "hello" + boolConst = true + floatConst = 1.5 + _ = "the blank identifier is skipped" +) + +const shift = 1 << 20 + +type reader struct { + r int64 + w int64 + _ [4]byte + name string +} + +type scalar int + +type aliased struct { + k uint32 + v uint32 +} + +type alias = aliased +`}) + hdr := generateFor(t, dir, "amd64") + want := []string{ + "#define const_bufSize 1024", + // iota resolves through go/types, one define per name. + "#define const_a 0", + "#define const_b 8", + "#define const_c 16", + `#define const_strConst "hello"`, + "#define const_boolConst true", + // Floats are the toolchain's own skip, as are blank names. + "#define const_shift 1048576", + // The blank field still occupies its bytes: the pad after w runs to + // the string's 8-byte alignment. + "#define reader__size 40", + "#define reader_r 0", + "#define reader_w 8", + "#define reader_name 24", + // Non-struct named types carry no defines; aliases to structs do. + "#define aliased__size 8", + "#define aliased_k 0", + "#define aliased_v 4", + "#define alias__size 8", + "#define alias_k 0", + "#define alias_v 4", + } + for _, w := range want { + if !strings.Contains(hdr, w+"\n") { + t.Errorf("header misses %q\ngot:\n%s", w, hdr) + } + } + for _, banned := range []string{"#define const_floatConst", "#define _ ", "#define scalar"} { + if strings.Contains(hdr, banned) { + t.Errorf("header must not carry %s\ngot:\n%s", banned, hdr) + } + } +} + +func TestGenerateGoAsmHeaderPerArch(t *testing.T) { + dir := writePkg(t, map[string]string{ + "common.go": `package perarch + +type layout struct { + a int32 + p uintptr +} +`, + // The build-tagged file set is part of the contract: a per-arch + // package is exactly how internal/cpu declares its layouts. + "const_amd64.go": `//go:build amd64 + +package perarch + +const flavour = 1 +`, + "const_arm64.go": `//go:build arm64 + +package perarch + +const flavour = 2 +`, + }) + amd64 := generateFor(t, dir, "amd64") + arm64 := generateFor(t, dir, "arm64") + if !strings.Contains(amd64, "#define const_flavour 1\n") { + t.Errorf("amd64 header misses const_flavour 1:\n%s", amd64) + } + if !strings.Contains(arm64, "#define const_flavour 2\n") { + t.Errorf("arm64 header misses const_flavour 2:\n%s", arm64) + } + if strings.Contains(arm64, "#define const_flavour 1\n") { + t.Errorf("arm64 header must not carry the amd64 file's value") + } + // SizesFor makes the layout the target's: uintptr is 4 bytes wide on + // 386 and 8 on amd64, which must move p and grow the struct. + if !strings.Contains(amd64, "#define layout__size 16\n") || !strings.Contains(amd64, "#define layout_p 8\n") { + t.Errorf("amd64 layout wrong:\n%s", amd64) + } + w386 := generateFor(t, dir, "386") + if !strings.Contains(w386, "#define layout__size 8\n") || !strings.Contains(w386, "#define layout_p 4\n") { + t.Errorf("386 layout wrong:\n%s", w386) + } +} + +func TestGenerateGoAsmHeaderErrors(t *testing.T) { + t.Run("type error", func(t *testing.T) { + dir := writePkg(t, map[string]string{"bad.go": `package bad + +const x = undefinedIdent +`}) + _, err := generateGoAsmHeader(dir, "amd64", t.TempDir()) + if err == nil { + t.Fatal("generation must fail for a package that does not type-check") + } + if !strings.Contains(err.Error(), dir) { + t.Errorf("error must name the package directory: %v", err) + } + if !strings.Contains(err.Error(), "type-check") { + t.Errorf("error must say the package does not type-check: %v", err) + } + }) + t.Run("no go files", func(t *testing.T) { + dir := t.TempDir() + _, err := generateGoAsmHeader(dir, "amd64", t.TempDir()) + if err == nil { + t.Fatal("generation must fail without Go files") + } + if !strings.Contains(err.Error(), dir) { + t.Errorf("error must name the package directory: %v", err) + } + }) +} + +func TestNeedsGoAsmHeader(t *testing.T) { + yes := "#include \"go_asm.h\"\n#include \"textflag.h\"\n" + no := "#include \"textflag.h\"\n#include \"funcdata.h\"\n" + if !needsGoAsmHeader(yes) { + t.Error("needsGoAsmHeader(missing on a go_asm.h include)") + } + if needsGoAsmHeader(no) { + t.Error("needsGoAsmHeader claims other headers need generation") + } +} + +func TestGoAsmHeaderResolved(t *testing.T) { + dir := t.TempDir() + if goAsmHeaderResolved(dir, nil) { + t.Error("resolved with no header anywhere") + } + other := t.TempDir() + if goAsmHeaderResolved(dir, []string{other}) { + t.Error("resolved with an empty -I directory") + } + if err := os.WriteFile(filepath.Join(dir, "go_asm.h"), nil, 0o644); err != nil { + t.Fatal(err) + } + if !goAsmHeaderResolved(dir, nil) { + t.Error("not resolved with the header in the package directory") + } +} + +func TestOtherGOOSFile(t *testing.T) { + for path, want := range map[string]bool{ + "/x/sys_windows_amd64.s": true, + "/x/rt0_js_wasm.s": true, + "/x/sys_darwin_arm64.s": true, + "/x/sys_linux_amd64.s": false, + "/x/time_linux_amd64.s": false, + "/x/memmove_amd64.s": false, + "/x/generic.s": false, + } { + if got := otherGOOSFile(path); got != want { + t.Errorf("otherGOOSFile(%q) = %v, want %v", path, got, want) + } + } +} + +// TestRunCorpusAuditGoAsm covers the audit wiring end to end: a package +// beside its kernel, the kernel living off the generated defines, and the +// histogram recording a generation failure as its own reason. +func TestRunCorpusAuditGoAsm(t *testing.T) { + dir := t.TempDir() + write := func(name, src string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + } + write("pkg.go", `package corpus + +const pageSize = 4096 + +type header struct { + magic uint64 + flags uint64 +} +`) + write("kern_amd64.s", "#include \"go_asm.h\"\nTEXT \xc2\xb7f(SB), NOSPLIT, $0-16\n\tMOVQ\t$const_pageSize, AX\n\tMOVQ\t$header__size, BX\n\tRET\n") + // The defines live in the file's own package; a kernel in a directory + // without Go files has no package to generate from. + if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil { + t.Fatal(err) + } + write(filepath.Join("sub", "lonely_arm64.s"), "#include \"go_asm.h\"\nTEXT \xc2\xb7g(SB), NOSPLIT, $0-0\n\tRET\n") + + stats, err := runCorpusAudit(dir, nil) + if err != nil { + t.Fatalf("runCorpusAudit: %v", err) + } + get := func(name string) *corpusTally { + for i, tg := range stats.targets { + if tg.name == name { + return stats.tallies[i] + } + } + t.Fatalf("no tally for %s", name) + return nil + } + if a := get("amd64"); a.attempted != 1 || a.assembled != 1 { + t.Errorf("amd64 = %d/%d, want 1/1", a.assembled, a.attempted) + } + // lonely_arm64.s is an arm64 file whose package cannot be generated. + if a := get("arm64"); a.attempted != 1 || a.assembled != 0 { + t.Errorf("arm64 = %d/%d, want 0/1", a.assembled, a.attempted) + } + if r := get("arm64").reasons["go_asm.h generation failed"]; r != 1 { + t.Errorf("arm64 go_asm.h failure count = %d, want 1", r) + } +} + +// TestGenerateGoAsmHeaderRuntime pins the generator against the real thing: +// the runtime package, whose header the toolchain's own -asmhdr output was +// sampled from. Skipped in short mode: it type-checks the whole package. +func TestGenerateGoAsmHeaderRuntime(t *testing.T) { + if testing.Short() { + t.Skip("type-checks the whole runtime package") + } + dir, err := generateGoAsmHeader("/usr/local/go/src/runtime", "amd64", t.TempDir()) + if err != nil { + t.Fatalf("generateGoAsmHeader(runtime): %v", err) + } + b, err := os.ReadFile(dir + "/go_asm.h") + if err != nil { + t.Fatal(err) + } + hdr := string(b) + for _, want := range []string{ + "#define const_hashSize 8\n", + "#define const_avxSupported 1\n", + "#define const_pageSize 8192\n", + "#define g_stackguard0 16\n", + "#define m__size ", + } { + if !strings.Contains(hdr, want) { + t.Errorf("runtime header misses %q", want) + } + } +} diff --git a/cmd/gasm/audit.go b/cmd/gasm/audit.go index 9df6635..d38337f 100644 --- a/cmd/gasm/audit.go +++ b/cmd/gasm/audit.go @@ -418,7 +418,9 @@ func cmdAuditCorpus(args []string, dirs includeDirs) error { // The toolchain's shipped headers (funcdata.h and friends) define the // macros GOROOT files include; a corpus audit measures those files, so // the header directory joins the search path automatically. go_asm.h - // is compiler-generated per package and stays unresolvable on purpose. + // is compiler-generated per package, so it is not resolved from here: + // files that include it get one generated per target architecture, + // which runCorpusAudit arranges. if out, err := exec.Command("go", "env", "GOROOT").Output(); err == nil { pkgInclude := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include") if fi, err := os.Stat(pkgInclude); err == nil && fi.IsDir() { @@ -462,9 +464,15 @@ var goPortSuffixes = []string{ "s390x", "wasm", } -// otherPortFile reports whether the file's name carries a Go-architecture -// suffix gasm does not support. +// otherPortFile reports whether the file belongs to a build no supported +// target ever compiles: either its name carries a Go-architecture suffix +// gasm does not support, or, for a file with no architecture suffix at all, +// it names another GOOS, which go/build drops from the file set +// (rt0_js_wasm.s is a javascript build, not a generic one). func otherPortFile(path string) bool { + if otherGOOSFile(path) { + return true + } base := path if i := strings.LastIndexByte(base, '/'); i >= 0 { base = base[i+1:] @@ -477,6 +485,29 @@ func otherPortFile(path string) bool { return false } +// goOSNames are the GOOS values go/build recognises in file names. +var goOSNames = map[string]bool{ + "aix": true, "darwin": true, "dragonfly": true, "freebsd": true, + "ios": true, "js": true, "linux": true, "netbsd": true, + "openbsd": true, "plan9": true, "solaris": true, "wasip1": true, + "windows": true, +} + +// otherGOOSFile reports whether the file's name names a GOOS other than the +// host's, by go/build's file-name rules. +func otherGOOSFile(path string) bool { + base := path + if i := strings.LastIndexByte(base, '/'); i >= 0 { + base = base[i+1:] + } + for seg := range strings.SplitSeq(strings.TrimSuffix(base, ".s"), "_") { + if goOSNames[seg] && seg != runtime.GOOS { + return true + } + } + return false +} + func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) { files, err := asmFiles(root) if err != nil { @@ -497,12 +528,20 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) { // its name allows assembles it. full, generic, otherPort := 0, 0, 0 + // Header generation is created on first use, so a corpus with no + // go_asm.h includes never pays for a temp directory. + var hdr *asmhdrCache + defer func() { + if hdr != nil { + hdr.close() + } + }() + for _, path := range files { src, err := readSource(path) if err != nil { return nil, err } - f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs}) var wanted []int // indexes into targets if a := arch.FromFilename(path); a != arch.Unknown { @@ -513,10 +552,11 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) { } } else if otherPortFile(path) { // A file named for a Go port gasm does not support (arm, - // 386, s390x, ...) is compiled by no supported-arch build, - // so it is neither generic nor a per-arch attempt: counting - // it as generic would make the headline unreachably low - // for reasons no supported target can fix. + // 386, s390x, ...) or for another GOOS is compiled by no + // supported-arch build, so it is neither generic nor a + // per-arch attempt: counting it as generic would make the + // headline unreachably low for reasons no supported target + // can fix. otherPort++ } else { generic++ @@ -525,6 +565,54 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) { } } + // A file that includes go_asm.h parses against a per-target header: + // the defines differ per architecture (internal/cpu's layout, for + // one), so the parse cannot be shared the way a header-free file's + // can. A generation failure is a failure for every target, named + // for the package rather than a bare "include not found". A header + // already resolvable in the package directory or the -I list is + // left alone. + if len(wanted) > 0 && needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) { + if hdr == nil { + if hdr, err = newAsmhdrCache(); err != nil { + return nil, err + } + } + pkgDir := filepath.Dir(path) + ok := true + for _, i := range wanted { + tg, t := targets[i], tallies[i] + t.attempted++ + hdrDir, err := hdr.dirFor(pkgDir, goarchName(tg.a)) + if err != nil { + ok = false + t.fail(path, corpusReason(err)) + continue + } + f, errs := parser.ParseWithOptions(path, src, parser.Options{ + Expand: true, + IncludeDirs: append(slices.Clone(dirs), hdrDir), + }) + if len(errs) > 0 { + ok = false + t.fail(path, corpusReason(errs[0])) + continue + } + if _, err := assembleFile(tg.a, f); err != nil { + ok = false + t.fail(path, corpusReason(err)) + continue + } + t.assembled++ + } + if ok && len(wanted) > 0 { + full++ + } + continue + } + + f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs}) + ok := true for _, i := range wanted { tg, t := targets[i], tallies[i] @@ -581,6 +669,8 @@ func printCorpusStats(s *corpusStats) { func corpusReason(err error) string { msg := err.Error() switch { + case strings.Contains(msg, "go_asm.h for GOARCH"): + return "go_asm.h generation failed" case strings.Contains(msg, "unsupported"), strings.Contains(msg, "cannot encode"): return "instruction not encodable" case strings.Contains(msg, "undefined label"): diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index fa7c2f9..0ba70d7 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -503,6 +503,12 @@ system toolchain; goobj emits the Go toolchain's own object format, which cmd/link consumes directly (it requires -p, the package path, and the installed Go toolchain: the object preamble is captured from go tool asm and the format version from go version). + +A file that includes go_asm.h gets that header generated automatically from +the package it lives in (the .go files beside it, type-checked for the +target architecture, the toolchain's own defines), so GOROOT assembly +assembles without a compiler. A package that has no Go files for the +target or does not type-check is a hard error naming the package. `) out := fs.String("o", "", "write the output to this file") format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)") @@ -538,6 +544,23 @@ and the format version from go version). fmt.Fprintln(os.Stderr, "gasm:", err) return 1 } + // A file that includes go_asm.h cannot assemble without the package's + // defines, and without a compiler nothing else has generated them, so + // gasm produces the equivalent itself: automatic, because the compiler + // behaves the same way and a flag would only ever be forgotten. A + // generation failure is fatal and names the package: assembling against + // a missing header would fail later with a bare "undefined" instead. + // A go_asm.h that already resolves (placed by hand, or passed with -I) + // is left alone. + if needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) { + hdrDir, cleanup, err := ensureGoAsmHeader(path, targetArch, nil) + if err != nil { + fmt.Fprintln(os.Stderr, "gasm asm:", err) + return 1 + } + defer cleanup() + dirs = append(dirs, hdrDir) + } f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs}) for _, e := range errs { fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)