feat(cmd): generate go_asm.h for package-context assembly
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,354 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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 _<arch>.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
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
-8
@@ -418,7 +418,9 @@ func cmdAuditCorpus(args []string, dirs includeDirs) error {
|
|||||||
// The toolchain's shipped headers (funcdata.h and friends) define the
|
// The toolchain's shipped headers (funcdata.h and friends) define the
|
||||||
// macros GOROOT files include; a corpus audit measures those files, so
|
// macros GOROOT files include; a corpus audit measures those files, so
|
||||||
// the header directory joins the search path automatically. go_asm.h
|
// 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 {
|
if out, err := exec.Command("go", "env", "GOROOT").Output(); err == nil {
|
||||||
pkgInclude := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
|
pkgInclude := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
|
||||||
if fi, err := os.Stat(pkgInclude); err == nil && fi.IsDir() {
|
if fi, err := os.Stat(pkgInclude); err == nil && fi.IsDir() {
|
||||||
@@ -462,9 +464,15 @@ var goPortSuffixes = []string{
|
|||||||
"s390x", "wasm",
|
"s390x", "wasm",
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherPortFile reports whether the file's name carries a Go-architecture
|
// otherPortFile reports whether the file belongs to a build no supported
|
||||||
// suffix gasm does not support.
|
// 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 {
|
func otherPortFile(path string) bool {
|
||||||
|
if otherGOOSFile(path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
base := path
|
base := path
|
||||||
if i := strings.LastIndexByte(base, '/'); i >= 0 {
|
if i := strings.LastIndexByte(base, '/'); i >= 0 {
|
||||||
base = base[i+1:]
|
base = base[i+1:]
|
||||||
@@ -477,6 +485,29 @@ func otherPortFile(path string) bool {
|
|||||||
return false
|
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) {
|
func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
||||||
files, err := asmFiles(root)
|
files, err := asmFiles(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -497,12 +528,20 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
// its name allows assembles it.
|
// its name allows assembles it.
|
||||||
full, generic, otherPort := 0, 0, 0
|
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 {
|
for _, path := range files {
|
||||||
src, err := readSource(path)
|
src, err := readSource(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs})
|
|
||||||
|
|
||||||
var wanted []int // indexes into targets
|
var wanted []int // indexes into targets
|
||||||
if a := arch.FromFilename(path); a != arch.Unknown {
|
if a := arch.FromFilename(path); a != arch.Unknown {
|
||||||
@@ -513,10 +552,11 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
}
|
}
|
||||||
} else if otherPortFile(path) {
|
} else if otherPortFile(path) {
|
||||||
// A file named for a Go port gasm does not support (arm,
|
// A file named for a Go port gasm does not support (arm,
|
||||||
// 386, s390x, ...) is compiled by no supported-arch build,
|
// 386, s390x, ...) or for another GOOS is compiled by no
|
||||||
// so it is neither generic nor a per-arch attempt: counting
|
// supported-arch build, so it is neither generic nor a
|
||||||
// it as generic would make the headline unreachably low
|
// per-arch attempt: counting it as generic would make the
|
||||||
// for reasons no supported target can fix.
|
// headline unreachably low for reasons no supported target
|
||||||
|
// can fix.
|
||||||
otherPort++
|
otherPort++
|
||||||
} else {
|
} else {
|
||||||
generic++
|
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
|
ok := true
|
||||||
for _, i := range wanted {
|
for _, i := range wanted {
|
||||||
tg, t := targets[i], tallies[i]
|
tg, t := targets[i], tallies[i]
|
||||||
@@ -581,6 +669,8 @@ func printCorpusStats(s *corpusStats) {
|
|||||||
func corpusReason(err error) string {
|
func corpusReason(err error) string {
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
switch {
|
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"):
|
case strings.Contains(msg, "unsupported"), strings.Contains(msg, "cannot encode"):
|
||||||
return "instruction not encodable"
|
return "instruction not encodable"
|
||||||
case strings.Contains(msg, "undefined label"):
|
case strings.Contains(msg, "undefined label"):
|
||||||
|
|||||||
@@ -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
|
cmd/link consumes directly (it requires -p, the package path, and the
|
||||||
installed Go toolchain: the object preamble is captured from go tool asm
|
installed Go toolchain: the object preamble is captured from go tool asm
|
||||||
and the format version from go version).
|
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")
|
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)")
|
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)
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||||
return 1
|
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})
|
f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs})
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||||
|
|||||||
Reference in New Issue
Block a user