feat(cmd): GOOS-aware headers, audit battery shapes and semicolon spacing
Test / test (push) Failing after 2m21s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-20 22:02:46 +02:00
parent a7744c24bd
commit ec1c521187
8 changed files with 286 additions and 49 deletions
+34 -21
View File
@@ -27,7 +27,10 @@ import (
// 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.
// architecture's own sizes, and the same defines are written out. The
// type-checking GOOS is selected by the caller: a GOOS-specific file
// (sys_darwin_arm64.s) needs its platform's defines, which a header from
// the ambient GOOS silently omits.
//
// The emitter mirrors cmd/compile's dumpasmhdr exactly: constants come out
// as "const_NAME", struct entries as "NAME__size" followed by the fields in
@@ -67,11 +70,15 @@ func goAsmHeaderResolved(asmDir string, dirs []string) bool {
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)
// generateGoAsmHeader type-checks the Go package in pkgDir for goos and
// goarch, writes its go_asm.h equivalent into dir, and returns dir. An
// empty goos means the ambient one. The caller owns the directory and its
// removal.
func generateGoAsmHeader(pkgDir, goos, goarch, dir string) (string, error) {
if goos == "" {
goos = build.Default.GOOS
}
imp := newSourceImporter(goos, goarch)
if imp.sizes == nil {
return "", fmt.Errorf("go_asm.h: unknown GOARCH %q", goarch)
}
@@ -89,7 +96,7 @@ func generateGoAsmHeader(pkgDir, goarch, dir string) (string, error) {
}
var b strings.Builder
fmt.Fprintf(&b, "// generated by gasm from package %s (GOARCH %s)\n\n", bp.Name, goarch)
fmt.Fprintf(&b, "// generated by gasm from package %s (GOOS %s, GOARCH %s)\n\n", bp.Name, goos, 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
@@ -203,13 +210,14 @@ type sourceImporter struct {
pkgs map[string]*types.Package
}
// newSourceImporter returns the importer for one target architecture.
// newSourceImporter returns the importer for one target GOOS and GOARCH.
// 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 {
func newSourceImporter(goos, goarch string) *sourceImporter {
ctxt := new(build.Context)
*ctxt = build.Default
ctxt.GOOS = goos
ctxt.GOARCH = goarch
ctxt.CgoEnabled = false
return &sourceImporter{
@@ -291,7 +299,7 @@ func (im *sourceImporter) checkPackage(bp *build.Package, files []*ast.File) (*t
// 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
dirs map[string]string // "pkgDir\x00goos\x00goarch" -> directory holding go_asm.h
errs map[string]error
}
@@ -304,17 +312,22 @@ func newAsmhdrCache() (*asmhdrCache, error) {
}
// 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
// under goos and goarch, generating it on first use. An empty goos means
// the ambient one, resolved here so that one package cannot generate twice
// under an explicit and an implicit spelling of the same GOOS.
func (c *asmhdrCache) dirFor(pkgDir, goos, goarch string) (string, error) {
if goos == "" {
goos = build.Default.GOOS
}
key := pkgDir + "\x00" + goos + "\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 {
dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s_%s", len(c.dirs), goos, goarch))
if _, err := generateGoAsmHeader(pkgDir, goos, goarch, dir); err != nil {
c.errs[key] = err
return "", err
}
@@ -327,10 +340,10 @@ 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) {
// for the file's target GOOS and 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, goos string, cache *asmhdrCache) (string, func(), error) {
if path == "-" {
return "", nil, errors.New("cannot generate go_asm.h for standard input (no package directory)")
}
@@ -338,14 +351,14 @@ func ensureGoAsmHeader(path string, target arch.Arch, cache *asmhdrCache) (strin
return "", nil, errors.New("a file that includes go_asm.h needs a target architecture: name the file _<arch>.s or pass -GOARCH")
}
if cache != nil {
dir, err := cache.dirFor(filepath.Dir(path), goarchName(target))
dir, err := cache.dirFor(filepath.Dir(path), goos, 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)
dir, err := generateGoAsmHeader(filepath.Dir(path), goos, goarchName(target), root)
if err != nil {
os.RemoveAll(root)
return "", nil, err
+122 -11
View File
@@ -22,12 +22,13 @@ func writePkg(t *testing.T, files map[string]string) string {
return dir
}
// generateFor generates the header for dir and returns its text.
func generateFor(t *testing.T, dir, goarch string) string {
// generateFor generates the header for dir and returns its text. An empty
// goos means the ambient one.
func generateFor(t *testing.T, dir, goos, goarch string) string {
t.Helper()
hdrDir, err := generateGoAsmHeader(dir, goarch, t.TempDir())
hdrDir, err := generateGoAsmHeader(dir, goos, goarch, t.TempDir())
if err != nil {
t.Fatalf("generateGoAsmHeader(%q, %s): %v", dir, goarch, err)
t.Fatalf("generateGoAsmHeader(%q, %s, %s): %v", dir, goos, goarch, err)
}
b, err := os.ReadFile(filepath.Join(hdrDir, "go_asm.h"))
if err != nil {
@@ -72,7 +73,7 @@ type aliased struct {
type alias = aliased
`})
hdr := generateFor(t, dir, "amd64")
hdr := generateFor(t, dir, "", "amd64")
want := []string{
"#define const_bufSize 1024",
// iota resolves through go/types, one define per name.
@@ -133,8 +134,8 @@ package perarch
const flavour = 2
`,
})
amd64 := generateFor(t, dir, "amd64")
arm64 := generateFor(t, dir, "arm64")
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)
}
@@ -149,19 +150,90 @@ const flavour = 2
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")
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)
}
}
// TestGenerateGoAsmHeaderGOOS pins the GOOS half of the target: only the
// platform's own files type-check into the header, which is why
// sys_darwin_arm64.s cannot assemble against a linux-generated one.
func TestGenerateGoAsmHeaderGOOS(t *testing.T) {
dir := writePkg(t, map[string]string{
"common.go": `package goosaware
type shared struct {
a int32
}
`,
"plat_darwin.go": `//go:build darwin
package goosaware
type platform struct {
trampoline_numer int64
}
`,
"plat_windows.go": `//go:build windows
package goosaware
type platform struct {
callbackArgs__size int32
}
`,
})
darwin := generateFor(t, dir, "darwin", "arm64")
if !strings.Contains(darwin, "#define platform__size 8\n") || !strings.Contains(darwin, "#define platform_trampoline_numer 0\n") {
t.Errorf("darwin header misses the darwin layout:\n%s", darwin)
}
if strings.Contains(darwin, "callbackArgs") {
t.Errorf("darwin header must not carry the windows layout:\n%s", darwin)
}
windows := generateFor(t, dir, "windows", "arm64")
if !strings.Contains(windows, "#define platform_callbackArgs__size 0\n") {
t.Errorf("windows header misses the windows layout:\n%s", windows)
}
if strings.Contains(windows, "trampoline_numer") {
t.Errorf("windows header must not carry the darwin layout:\n%s", windows)
}
// The ambient GOOS is neither of the two, so only shared's defines are
// emitted; the shared type keeps its layout there.
ambient := generateFor(t, dir, "", "arm64")
if !strings.Contains(ambient, "#define shared__size 4\n") {
t.Errorf("ambient header misses the shared layout:\n%s", ambient)
}
if strings.Contains(ambient, "#define platform_") {
t.Errorf("ambient header must not carry either platform layout:\n%s", ambient)
}
}
func TestGoosFromFilename(t *testing.T) {
for path, want := range map[string]string{
"/x/sys_darwin_arm64.s": "darwin",
"/x/sys_windows_arm64.s": "windows",
"/x/asm_linux_amd64.s": "linux",
"/x/rt0_darwin_arm64.s": "darwin",
"/x/vgetrandom_zos_s390x.s": "zos",
"/x/rt0_js_wasm.s": "js",
"/x/memmove_amd64.s": "",
"/x/vlop_arm.s": "",
"/x/stubs.s": "",
} {
if got := goosFromFilename(path); got != want {
t.Errorf("goosFromFilename(%q) = %q, want %q", path, got, want)
}
}
}
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())
_, err := generateGoAsmHeader(dir, "", "amd64", t.TempDir())
if err == nil {
t.Fatal("generation must fail for a package that does not type-check")
}
@@ -174,7 +246,7 @@ const x = undefinedIdent
})
t.Run("no go files", func(t *testing.T) {
dir := t.TempDir()
_, err := generateGoAsmHeader(dir, "amd64", t.TempDir())
_, err := generateGoAsmHeader(dir, "", "amd64", t.TempDir())
if err == nil {
t.Fatal("generation must fail without Go files")
}
@@ -281,6 +353,45 @@ type header struct {
}
}
// TestRunCorpusAuditGOOS covers the filename-derived GOOS end to end: a
// kernel whose name names darwin must have its header type-checked with
// GOOS=darwin, so the darwin-only constant it offsets with is defined. The
// operand mirrors sys_darwin_arm64.s's trampoline, where a missing define
// leaves an unexpanded symbol in the offset and fails.
func TestRunCorpusAuditGOOS(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\n")
write("plat_darwin.go", "//go:build darwin\n\npackage corpus\n\nconst trampolineNumer = 8\n")
write("kern_darwin_arm64.s", "#include \"go_asm.h\"\n"+
"GLOBL timebase<>(SB), NOPTR, $16\n"+
"TEXT \xc2\xb7g(SB), NOSPLIT, $0-0\n"+
"\tMOVD\ttimebase<>+const_trampolineNumer(SB), R0\n"+
"\tRET\n")
stats, err := runCorpusAudit(dir, nil)
if err != nil {
t.Fatalf("runCorpusAudit: %v", err)
}
var arm *corpusTally
for i, tg := range stats.targets {
if tg.name == "arm64" {
arm = stats.tallies[i]
}
}
if arm == nil {
t.Fatal("no arm64 tally")
}
if arm.attempted != 1 || arm.assembled != 1 {
t.Errorf("arm64 = %d/%d, want 1/1; reasons: %v", arm.assembled, arm.attempted, arm.reasons)
}
}
// 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.
@@ -288,7 +399,7 @@ 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())
dir, err := generateGoAsmHeader("/usr/local/go/src/runtime", "", "amd64", t.TempDir())
if err != nil {
t.Fatalf("generateGoAsmHeader(runtime): %v", err)
}
+68 -9
View File
@@ -5,6 +5,7 @@ package main
import (
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
@@ -288,6 +289,11 @@ func probeShapes(a arch.Arch) []string {
"V1.B16, [V2.B16], V3.B16", "V1.B8, [V2.B16, V3.B16], V4.B8",
"$4, V1.B16, V2.B16, V3.B16", "$15, V1", "V1, V2, p2",
"R0, R1, $1, $4, p2",
// The landing-pad kind, the compiler's PCDATA
// bookkeeping and the four-operand bitfield
// insert/extract family, as the toolchain's own
// testdata spells them.
"C", "$1, $0", "$0, R1, $1, R2",
}
case arch.RISCV:
return []string{
@@ -307,6 +313,9 @@ func probeShapes(a arch.Arch) []string {
"X5, X6, p2", "R5, R6, p2",
"X5, E8, M8, TA, MA, X6", "$4, E32, M1, TA, MA, X1",
"(X5), X6, V1, V2",
// The CSR immediate forms the toolchain's testdata spells:
// immediate, CSR name, destination.
"$2, TIME, X5",
"",
}
case arch.LOONG64:
@@ -323,6 +332,12 @@ func probeShapes(a arch.Arch) []string {
"V1, V2, V3", "X1, X2, X3", "V1, V2", "X1, X2", "V1", "X1",
// The vector compare-to-flag forms land in an FCC register.
"V1, FCC0", "X1, FCC0",
// The compiler's bookkeeping pair and the raw spellings the
// toolchain's own testdata carries: JIRL rd, rj, offset (the
// form RET lowers to), the prefetch with a 32-bit address and
// hint, and the byte-shuffle quads.
"$1, $0", "R1, R5, 0", "0(R7), $5, $0", "(R7), $5, $0",
"V1, V2, V3, V4", "X1, X2, X3, X4",
"",
}
}
@@ -487,10 +502,47 @@ func otherPortFile(path string) bool {
// 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,
"aix": true, "android": true, "darwin": true, "dragonfly": true,
"freebsd": true, "hurd": true, "illumos": true, "ios": true,
"js": true, "linux": true, "nacl": true, "netbsd": true,
"openbsd": true, "plan9": true, "solaris": true, "wasip1": true,
"windows": true,
"windows": true, "zos": true,
}
// resolveGOOS validates a -GOOS flag value, mirroring the architecture
// check's surface: a usage error naming what the tool accepts.
func resolveGOOS(name string) (string, error) {
lower := strings.ToLower(name)
if goOSNames[lower] {
return lower, nil
}
return "", &usageError{fmt.Errorf("unknown GOOS %q: want one of %s", name, strings.Join(slices.Sorted(maps.Keys(goOSNames)), ", "))}
}
// goosFromFilename returns the GOOS the file's name carries, by go/build's
// goodOSArchFile rule: the GOOS segment sits last, or last before the
// architecture segment (sys_darwin_arm64.s, vlop_arm.s carries none). An
// empty result means the name names no GOOS and the ambient one applies.
func goosFromFilename(path string) string {
base := path
if i := strings.LastIndexByte(base, '/'); i >= 0 {
base = base[i+1:]
}
base = strings.TrimSuffix(base, ".s")
// go/build ignores everything before the first underscore, so a GOOS
// segment is only ever looked for from there on.
i := strings.IndexByte(base, '_')
if i < 0 {
return ""
}
segs := strings.Split(base[i:], "_")
if n := len(segs); n >= 2 && goOSNames[segs[n-2]] && slices.Contains(goPortSuffixes, segs[n-1]) {
return segs[n-2]
}
if goOSNames[segs[len(segs)-1]] {
return segs[len(segs)-1]
}
return ""
}
// otherGOOSFile reports whether the file's name names a GOOS other than the
@@ -543,6 +595,12 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
return nil, err
}
// The GOOS the header generation type-checks under follows the
// file's name when the name carries one; the ambient GOOS is the
// honest guess otherwise (a build tag naming another GOOS is
// invisible to a file-name rule).
goos := goosFromFilename(path)
var wanted []int // indexes into targets
if a := arch.FromFilename(path); a != arch.Unknown {
for i, tg := range targets {
@@ -567,11 +625,12 @@ 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.
// one) and per GOOS (sys_darwin_arm64.s's trampoline constants,
// for another), 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 {
@@ -583,7 +642,7 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
for _, i := range wanted {
tg, t := targets[i], tallies[i]
t.attempted++
hdrDir, err := hdr.dirFor(pkgDir, goarchName(tg.a))
hdrDir, err := hdr.dirFor(pkgDir, goos, goarchName(tg.a))
if err != nil {
ok = false
t.fail(path, corpusReason(err))
+18 -5
View File
@@ -486,7 +486,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
}
func cmdAsm(args []string) int {
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] <file>", `
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] <file>", `
Assemble FILE without the Go toolchain: every TEXT function is encoded to
machine code and printed as a hex dump. Supported architectures: amd64
(including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP,
@@ -507,18 +507,22 @@ 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.
assembles without a compiler. -GOOS selects the type-checking GOOS for
that header: a GOOS-specific file (sys_darwin_arm64.s) needs its platform's
defines, which a header from the ambient GOOS silently omits. 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)")
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
archName := fs.String("GOARCH", "", "target architecture: amd64, arm64, riscv64 or loong64 (overrides the file-name suffix)")
goosName := fs.String("GOOS", "", "operating system for go_asm.h generation: a GOOS go/build recognises (default: the host's)")
var dirs includeDirs
fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)")
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] <file>")
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] <file>")
return 2
}
// The format is validated before anything else, so a bogus value exits 2
@@ -539,6 +543,15 @@ target or does not type-check is a hard error naming the package.
}
targetArch = a
}
goos := ""
if *goosName != "" {
g, err := resolveGOOS(*goosName)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm asm: %v\n", err)
return 2
}
goos = g
}
src, err := readSource(path)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
@@ -553,7 +566,7 @@ target or does not type-check is a hard error naming the package.
// 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)
hdrDir, cleanup, err := ensureGoAsmHeader(path, targetArch, goos, nil)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm asm:", err)
return 1