Compare commits

..
3 Commits
Author SHA1 Message Date
petrbalvin cf05b9b384 feat(cmd): GOOS-aware headers, audit battery shapes and semicolon spacing
Test / test (push) Canceled after 1m8s
Assisted-by: GLM 5.3 Flash
2026-09-20 22:02:19 +02:00
petrbalvin a7744c24bd fix(parser): substitute macro parameters behind element selectors
Assisted-by: GLM 5.3 Flash
2026-09-20 22:02:19 +02:00
petrbalvin 522e6f2ae8 feat(parser): bracket register ranges, index-only VSIB and bare trailing immediates
Assisted-by: GLM 5.3 Flash
2026-09-20 22:02:19 +02:00
15 changed files with 629 additions and 53 deletions
+18
View File
@@ -846,6 +846,14 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Op
case ast.OpAddr: case ast.OpAddr:
a := op.Addr a := op.Addr
// A bracketed register range, [Z0-Z3]: the four-register source of
// the 4FMAPS/4VNNIW families. The EVEX quad-register emit path
// needs an encoder operand of its own, so the shape stays a named
// gap rather than an encoding.
if a.Range != nil {
return nil, fmt.Errorf("register range %q needs quad-register encoder support", op.Raw)
}
// FP-relative: x+N(FP) → (N + fpAdjust)(SP). The offset N lives in the // FP-relative: x+N(FP) → (N + fpAdjust)(SP). The offset N lives in the
// symbol, not the address displacement. // symbol, not the address displacement.
if a.Sym != nil && a.Sym.Pseudo == "FP" { if a.Sym != nil && a.Sym.Pseudo == "FP" {
@@ -893,6 +901,16 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Op
} }
return m, nil return m, nil
} }
// Index-only memory: the VSIB form the gather/scatter families
// read, 8(X4*1). A scaled vector index addresses memory with no
// base register; the mod=00 SIB with base field 101 carries it.
if a.Index != "" {
idx, ok := ParseReg(a.Index)
if !ok {
return nil, fmt.Errorf("unknown index register %q", a.Index)
}
return Mem{Index: idx, Scale: a.Scale, Disp: a.Offset, HasIndex: true, Size: size}, nil
}
// Bare register. // Bare register.
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" { if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
if r, ok := ParseReg(a.Sym.Name); ok { if r, ok := ParseReg(a.Sym.Name); ok {
+17 -7
View File
@@ -151,11 +151,21 @@ type Immediate struct {
// Address is a non-immediate operand: a register, a memory reference, a symbol // Address is a non-immediate operand: a register, a memory reference, a symbol
// reference or a label. Fields are populated best-effort from the syntax. // reference or a label. Fields are populated best-effort from the syntax.
type Address struct { type Address struct {
Sym *Symbol // name reference (bare ident, or name+off(pseudo)) Sym *Symbol // name reference (bare ident, or name+off(pseudo))
Base string // base register, from (base) Base string // base register, from (base)
Index string // index register, from (index*scale) Index string // index register, from (index*scale)
Scale int // index scale; 0 when absent Scale int // index scale; 0 when absent
Offset int64 // leading displacement, from off(base) Offset int64 // leading displacement, from off(base)
HasOff bool // a leading displacement is present HasOff bool // a leading displacement is present
Shift string // verbatim arm64 shift suffix, e.g. "<< 2" Shift string // verbatim arm64 shift suffix, e.g. "<< 2"
Range *RegRange // bracketed register range; nil for every other form
}
// RegRange is a bracketed register range, [Z0-Z3]: the amd64 spelling of
// the four-register source of the 4FMAPS/4VNNIW families. Lo and Hi carry
// the verbatim register spellings; the range is inclusive at both ends.
type RegRange struct {
Lo string
Hi string
Pos token.Position
} }
+34 -21
View File
@@ -27,7 +27,10 @@ import (
// GOROOT assembly includes it, and a standalone assembler has no compiler // GOROOT assembly includes it, and a standalone assembler has no compiler
// to have produced it, so gasm generates the equivalent itself: the package // 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 // 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 // The emitter mirrors cmd/compile's dumpasmhdr exactly: constants come out
// as "const_NAME", struct entries as "NAME__size" followed by the fields in // 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 return false
} }
// generateGoAsmHeader type-checks the Go package in pkgDir for goarch, // generateGoAsmHeader type-checks the Go package in pkgDir for goos and
// writes its go_asm.h equivalent into dir, and returns dir. The caller // goarch, writes its go_asm.h equivalent into dir, and returns dir. An
// owns the directory and its removal. // empty goos means the ambient one. The caller owns the directory and its
func generateGoAsmHeader(pkgDir, goarch, dir string) (string, error) { // removal.
imp := newSourceImporter(goarch) func generateGoAsmHeader(pkgDir, goos, goarch, dir string) (string, error) {
if goos == "" {
goos = build.Default.GOOS
}
imp := newSourceImporter(goos, goarch)
if imp.sizes == nil { if imp.sizes == nil {
return "", fmt.Errorf("go_asm.h: unknown GOARCH %q", goarch) 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 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 // 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 // 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 // way the toolchain's does. Order carries no meaning to the assembler
@@ -203,13 +210,14 @@ type sourceImporter struct {
pkgs map[string]*types.Package 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 // 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 // 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. // 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 := new(build.Context)
*ctxt = build.Default *ctxt = build.Default
ctxt.GOOS = goos
ctxt.GOARCH = goarch ctxt.GOARCH = goarch
ctxt.CgoEnabled = false ctxt.CgoEnabled = false
return &sourceImporter{ 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-check must not be re-checked once per file.
type asmhdrCache struct { type asmhdrCache struct {
root string 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 errs map[string]error
} }
@@ -304,17 +312,22 @@ func newAsmhdrCache() (*asmhdrCache, error) {
} }
// dirFor returns the directory holding the generated go_asm.h for pkgDir // dirFor returns the directory holding the generated go_asm.h for pkgDir
// and goarch, generating it on first use. // under goos and goarch, generating it on first use. An empty goos means
func (c *asmhdrCache) dirFor(pkgDir, goarch string) (string, error) { // the ambient one, resolved here so that one package cannot generate twice
key := pkgDir + "\x00" + goarch // 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 { if dir, ok := c.dirs[key]; ok {
return dir, nil return dir, nil
} }
if err, ok := c.errs[key]; ok { if err, ok := c.errs[key]; ok {
return "", err return "", err
} }
dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s", len(c.dirs), goarch)) dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s_%s", len(c.dirs), goos, goarch))
if _, err := generateGoAsmHeader(pkgDir, goarch, dir); err != nil { if _, err := generateGoAsmHeader(pkgDir, goos, goarch, dir); err != nil {
c.errs[key] = err c.errs[key] = err
return "", err return "", err
} }
@@ -327,10 +340,10 @@ func (c *asmhdrCache) close() { os.RemoveAll(c.root) }
// ensureGoAsmHeader prepares the include directory a file that includes // ensureGoAsmHeader prepares the include directory a file that includes
// go_asm.h needs: the generated header for the package in path's directory, // 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 // for the file's target GOOS and architecture. It reports a usage error
// architecture cannot be determined, and passes through the generator's // when the architecture cannot be determined, and passes through the
// diagnostics, which name the package. // generator's diagnostics, which name the package.
func ensureGoAsmHeader(path string, target arch.Arch, cache *asmhdrCache) (string, func(), error) { func ensureGoAsmHeader(path string, target arch.Arch, goos string, cache *asmhdrCache) (string, func(), error) {
if path == "-" { if path == "-" {
return "", nil, errors.New("cannot generate go_asm.h for standard input (no package directory)") 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") 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 { 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 return dir, func() {}, err
} }
root, err := os.MkdirTemp("", "gasm-asmhdr") root, err := os.MkdirTemp("", "gasm-asmhdr")
if err != nil { if err != nil {
return "", nil, err 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 { if err != nil {
os.RemoveAll(root) os.RemoveAll(root)
return "", nil, err return "", nil, err
+68 -9
View File
@@ -5,6 +5,7 @@ package main
import ( import (
"fmt" "fmt"
"maps"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "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", "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", "$4, V1.B16, V2.B16, V3.B16", "$15, V1", "V1, V2, p2",
"R0, R1, $1, $4, 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: case arch.RISCV:
return []string{ return []string{
@@ -307,6 +313,9 @@ func probeShapes(a arch.Arch) []string {
"X5, X6, p2", "R5, R6, p2", "X5, X6, p2", "R5, R6, p2",
"X5, E8, M8, TA, MA, X6", "$4, E32, M1, TA, MA, X1", "X5, E8, M8, TA, MA, X6", "$4, E32, M1, TA, MA, X1",
"(X5), X6, V1, V2", "(X5), X6, V1, V2",
// The CSR immediate forms the toolchain's testdata spells:
// immediate, CSR name, destination.
"$2, TIME, X5",
"", "",
} }
case arch.LOONG64: 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", "V1, V2, V3", "X1, X2, X3", "V1, V2", "X1, X2", "V1", "X1",
// The vector compare-to-flag forms land in an FCC register. // The vector compare-to-flag forms land in an FCC register.
"V1, FCC0", "X1, FCC0", "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. // goOSNames are the GOOS values go/build recognises in file names.
var goOSNames = map[string]bool{ var goOSNames = map[string]bool{
"aix": true, "darwin": true, "dragonfly": true, "freebsd": true, "aix": true, "android": true, "darwin": true, "dragonfly": true,
"ios": true, "js": true, "linux": true, "netbsd": 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, "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 // 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 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 var wanted []int // indexes into targets
if a := arch.FromFilename(path); a != arch.Unknown { if a := arch.FromFilename(path); a != arch.Unknown {
for i, tg := range targets { 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: // A file that includes go_asm.h parses against a per-target header:
// the defines differ per architecture (internal/cpu's layout, for // the defines differ per architecture (internal/cpu's layout, for
// one), so the parse cannot be shared the way a header-free file's // one) and per GOOS (sys_darwin_arm64.s's trampoline constants,
// can. A generation failure is a failure for every target, named // for another), so the parse cannot be shared the way a
// for the package rather than a bare "include not found". A header // header-free file's can. A generation failure is a failure for
// already resolvable in the package directory or the -I list is // every target, named for the package rather than a bare "include
// left alone. // 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 len(wanted) > 0 && needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) {
if hdr == nil { if hdr == nil {
if hdr, err = newAsmhdrCache(); err != nil { if hdr, err = newAsmhdrCache(); err != nil {
@@ -583,7 +642,7 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
for _, i := range wanted { for _, i := range wanted {
tg, t := targets[i], tallies[i] tg, t := targets[i], tallies[i]
t.attempted++ t.attempted++
hdrDir, err := hdr.dirFor(pkgDir, goarchName(tg.a)) hdrDir, err := hdr.dirFor(pkgDir, goos, goarchName(tg.a))
if err != nil { if err != nil {
ok = false ok = false
t.fail(path, corpusReason(err)) 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 { 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 Assemble FILE without the Go toolchain: every TEXT function is encoded to
machine code and printed as a hex dump. Supported architectures: amd64 machine code and printed as a hex dump. Supported architectures: amd64
(including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP, (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 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 the package it lives in (the .go files beside it, type-checked for the
target architecture, the toolchain's own defines), so GOROOT assembly target architecture, the toolchain's own defines), so GOROOT assembly
assembles without a compiler. A package that has no Go files for the assembles without a compiler. -GOOS selects the type-checking GOOS for
target or does not type-check is a hard error naming the package. 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") 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)")
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)") 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)") 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 var dirs includeDirs
fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)") fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)")
fs.Parse(args) fs.Parse(args)
if fs.NArg() != 1 { 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 return 2
} }
// The format is validated before anything else, so a bogus value exits 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 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) src, err := readSource(path)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err) 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) // A go_asm.h that already resolves (placed by hand, or passed with -I)
// is left alone. // is left alone.
if needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) { 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 { if err != nil {
fmt.Fprintln(os.Stderr, "gasm asm:", err) fmt.Fprintln(os.Stderr, "gasm asm:", err)
return 1 return 1
+12 -2
View File
@@ -141,7 +141,7 @@ gasm lint kernel_amd64.s
## asm ## asm
```text ```text
Usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] <file> Usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] <file>
``` ```
| Flag | Default | Effect | | Flag | Default | Effect |
@@ -150,6 +150,7 @@ Usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o ou
| `-I` | empty | directory to search for `#include` files; may be repeated, searched in order after the source directory | | `-I` | empty | directory to search for `#include` files; may be repeated, searched in order after the source directory |
| `-p` | empty | package path for `--format goobj`, qualifying the exported symbols | | `-p` | empty | package path for `--format goobj`, qualifying the exported symbols |
| `-GOARCH` | empty | target architecture: `amd64`, `arm64`, `riscv64` or `loong64`; overrides the file-name suffix | | `-GOARCH` | empty | target architecture: `amd64`, `arm64`, `riscv64` or `loong64`; overrides the file-name suffix |
| `-GOOS` | empty | operating system for the generated `go_asm.h`: any GOOS `go/build` recognises in file names; default is the host's |
| `-o` | empty | write the output to this file instead of a hex dump on stdout | | `-o` | empty | write the output to this file instead of a hex dump on stdout |
Supported architectures: amd64 (VEX/AVX2 and EVEX/AVX-512 included), arm64, Supported architectures: amd64 (VEX/AVX2 and EVEX/AVX-512 included), arm64,
@@ -163,6 +164,13 @@ system toolchain; `goobj` emits the Go toolchain's own object format, which
installed: the object preamble is captured from `go tool asm` and the format installed: the object preamble is captured from `go tool asm` and the format
version from `go version`. `raw` and `elf` need no toolchain at all. version from `go version`. `raw` and `elf` need no toolchain at all.
A file that includes `go_asm.h` gets that header generated from the Go
files beside it, type-checked for the target. `-GOOS` selects the
type-checking GOOS for that header, because a GOOS-specific file needs its
platform's defines: `sys_darwin_arm64.s` fails against the ambient GOOS
(`machTimebaseInfo_numer` is missing from a linux type-check) and assembles
with `-GOOS darwin`.
Assembly preprocessing matches the toolchain's: `#define` macros (object and Assembly preprocessing matches the toolchain's: `#define` macros (object and
parameterised) expand at the point of use, `#undef`, `#ifdef`, `#ifndef`, parameterised) expand at the point of use, `#undef`, `#ifdef`, `#ifndef`,
`#else` and `#endif` behave as in `go tool asm`, `;` separates statements, `#else` and `#endif` behave as in `go tool asm`, `;` separates statements,
@@ -390,7 +398,9 @@ With `--corpus` the audit changes shape: it assembles every `.s` file under
DIR (default `GOROOT/src`) with the gasm encoder only, no toolchain probing. DIR (default `GOROOT/src`) with the gasm encoder only, no toolchain probing.
A file whose name carries a recognisable `_arch` suffix is attempted for that A file whose name carries a recognisable `_arch` suffix is attempted for that
architecture; a file without one is attempted for all four, exactly as a architecture; a file without one is attempted for all four, exactly as a
`GOARCH` build would compile it. The report gives the headline number (files `GOARCH` build would compile it, and a name that names a GOOS
(`sys_darwin_arm64.s`) type-checks its generated `go_asm.h` for that GOOS.
The report gives the headline number (files
that assemble for every target architecture), the per-architecture pass rates that assemble for every target architecture), the per-architecture pass rates
and the most common failure reasons with one representative file each, which and the most common failure reasons with one representative file each, which
drive the encodability backlog by frequency rather than by table order. A run drive the encodability backlog by frequency rather than by table order. A run
+14 -1
View File
@@ -2,7 +2,7 @@
.SH NAME .SH NAME
gasm-asm \- assemble Plan 9 assembly without the Go toolchain gasm-asm \- assemble Plan 9 assembly without the Go toolchain
.SH SYNOPSIS .SH SYNOPSIS
.B gasm asm [\-\-format raw|elf|goobj] [\-I dir] [\-p pkg] [\-GOARCH arch] [\-o out] <file> .B gasm asm [\-\-format raw|elf|goobj] [\-I dir] [\-p pkg] [\-GOARCH arch] [\-GOOS os] [\-o out] <file>
.SH DESCRIPTION .SH DESCRIPTION
Assemble FILE without the Go toolchain: every TEXT function is encoded Assemble FILE without the Go toolchain: every TEXT function is encoded
to machine code and printed as a hex dump. Supported architectures: to machine code and printed as a hex dump. Supported architectures:
@@ -42,6 +42,15 @@ need no toolchain at all.
Framed functions receive the stack-split guard and the trailing Framed functions receive the stack-split guard and the trailing
morestack block, byte-identical to the toolchain's output, so split morestack block, byte-identical to the toolchain's output, so split
functions link too. functions link too.
.PP
A file that includes go_asm.h gets that header generated from the Go
files beside it, type-checked for the target.
.B \-GOOS
selects the type-checking GOOS for that header, because a GOOS-specific
file needs its platform's defines: sys_darwin_arm64.s fails against the
ambient GOOS (machTimebaseInfo_numer is missing from a linux type-check)
and assembles with
.BR "\-GOOS darwin" .
.SH OPTIONS .SH OPTIONS
.TP .TP
.B \-\-format \fIraw|elf|goobj\fR .B \-\-format \fIraw|elf|goobj\fR
@@ -59,6 +68,10 @@ Target architecture: amd64, arm64, riscv64 or loong64; overrides the
file-name suffix, which is how the suffix-less majority of GOROOT's file-name suffix, which is how the suffix-less majority of GOROOT's
files (cpu_x86.s, stub.s, ...) become assemblable. files (cpu_x86.s, stub.s, ...) become assemblable.
.TP .TP
.B \-GOOS \fIos\fR
Operating system for the generated go_asm.h: any GOOS go/build
recognises in file names; the default is the host's.
.TP
.B \-o \fIfile\fR .B \-o \fIfile\fR
Write the output to this file instead of a hex dump on stdout. Write the output to this file instead of a hex dump on stdout.
.SH EXIT STATUS .SH EXIT STATUS
+7
View File
@@ -241,6 +241,13 @@ func renderInstr(line []token.Token, width int) string {
if line[0].Kind != token.Ident { if line[0].Kind != token.Ident {
return "\t" + mnem + " " + ops return "\t" + mnem + " " + ops
} }
// A statement separator belongs to the statement it ends: when the
// operands open with a ';', the alignment padding would land between
// the mnemonic and its own separator (REP ; MOVSQ), so such a line
// renders with a single space whatever the function's width.
if strings.HasPrefix(ops, ";") {
return "\t" + mnem + " " + ops
}
if width < len(mnem) { if width < len(mnem) {
width = len(mnem) width = len(mnem)
} }
+11
View File
@@ -364,6 +364,17 @@ func TestSemicolonSeparators(t *testing.T) {
in: "TEXT ·f(SB), $0\nBYTE $1;\nRET\n", in: "TEXT ·f(SB), $0\nBYTE $1;\nRET\n",
want: "TEXT ·f(SB), $0\n\tBYTE $1;\n\tRET\n", want: "TEXT ·f(SB), $0\n\tBYTE $1;\n\tRET\n",
}, },
{
// The REP shape: a prefix-style zero-operand statement
// followed by the instruction it prefixes. The separator
// belongs to the statement it ends, so the function's
// alignment width (MOVSQ is the widest mnemonic here) must
// not open a gap before it: one space after the mnemonic
// whatever the neighbours' lengths.
name: "after a prefix-style statement",
in: "TEXT ·f(SB), $0\nMOVQ AX, BX\nREP; MOVSQ\nRET\n",
want: "TEXT ·f(SB), $0\n\tMOVQ AX, BX\n\tREP ; MOVSQ\n\tRET\n",
},
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
+60 -6
View File
@@ -304,7 +304,7 @@ func (p *state) parseGlobl(line []token.Token) *ast.Globl {
rest = rest[1:] rest = rest[1:]
} }
if len(rest) > 0 && rest[0].Kind == token.Dollar { if len(rest) > 0 && rest[0].Kind == token.Dollar {
g.Size = parseOperand(rest) g.Size = parseOperand(rest, false)
} }
return g return g
} }
@@ -321,7 +321,7 @@ func (p *state) parseData(line []token.Token) *ast.Data {
d.Name = sym d.Name = sym
d.Width = width d.Width = width
if len(valuePart) > 0 { if len(valuePart) > 0 {
d.Value = parseOperand(stripComment(valuePart)) d.Value = parseOperand(stripComment(valuePart), false)
} }
return d return d
} }
@@ -332,8 +332,13 @@ func (p *state) parseInstr(line []token.Token) {
return return
} }
instr := &ast.Instr{Mnemonic: body[0], Comment: comment} instr := &ast.Instr{Mnemonic: body[0], Comment: comment}
for _, grp := range splitOperands(body[1:]) { grps := splitOperands(body[1:])
if op := parseOperand(grp); op != nil { for i, grp := range grps {
// Only the final operand slot may carry a bare constant: the
// toolchain reads the trailing 1 of CMPSD X1, X0, 1 as $1
// (math/floor_amd64.s), while an earlier bare number names an
// absolute address, a form this parser keeps out of the tree.
if op := parseOperand(grp, i == len(grps)-1); op != nil {
instr.Operands = append(instr.Operands, op) instr.Operands = append(instr.Operands, op)
} }
} }
@@ -417,8 +422,10 @@ func setName(raw string, sym *ast.Symbol) {
// --- operand parsing -------------------------------------------------------- // --- operand parsing --------------------------------------------------------
// parseOperand parses one operand group into an Operand. // parseOperand parses one operand group into an Operand. allowBare marks
func parseOperand(g []token.Token) *ast.Operand { // the final operand slot of an instruction, where the toolchain reads a
// bare constant expression as an immediate.
func parseOperand(g []token.Token, allowBare bool) *ast.Operand {
g = stripComment(g) g = stripComment(g)
if len(g) == 0 { if len(g) == 0 {
return nil return nil
@@ -431,9 +438,25 @@ func parseOperand(g []token.Token) *ast.Operand {
} }
op.Kind = ast.OpAddr op.Kind = ast.OpAddr
op.Addr = parseAddress(g) op.Addr = parseAddress(g)
// A trailing bare constant leaves every address field empty: the
// grammar sees no register, memory reference or symbol, and the closed
// constant expression is the whole group. Read it as the immediate it
// names, exactly what the $ spelling would produce.
if allowBare && isEmptyAddress(op.Addr) {
if v, rest, ok := foldExpr(g); ok && len(rest) == 0 {
op.Kind = ast.OpImmediate
op.Imm = ast.Immediate{Val: v, HasVal: true}
}
}
return op return op
} }
// isEmptyAddress reports whether parseAddress populated nothing, its sign
// that the group is no register, memory reference, symbol or register range.
func isEmptyAddress(a ast.Address) bool {
return a.Sym == nil && a.Base == "" && a.Index == "" && a.Range == nil && a.Shift == ""
}
// parseImmediate parses the tokens following a '$'. // parseImmediate parses the tokens following a '$'.
func parseImmediate(g []token.Token) ast.Immediate { func parseImmediate(g []token.Token) ast.Immediate {
var imm ast.Immediate var imm ast.Immediate
@@ -497,6 +520,14 @@ func parseAddress(g []token.Token) ast.Address {
if len(g) == 0 { if len(g) == 0 {
return addr return addr
} }
// A bracketed register range, [Z0-Z3]: the amd64 4FMAPS/4VNNIW
// multi-source operand. The bracket runes arrive as Illegal tokens
// (the lexer has no bracket kind), so the shape matches on their text.
if isBracket(g[0], "[") && len(g) == 5 && g[1].Kind == token.Ident &&
g[2].Kind == token.Minus && g[3].Kind == token.Ident && isBracket(g[4], "]") {
addr.Range = &ast.RegRange{Lo: g[1].Text, Hi: g[3].Text, Pos: g[0].Pos}
return addr
}
// Symbol-with-pseudo form: name[<>][+off](PSEUDO). // Symbol-with-pseudo form: name[<>][+off](PSEUDO).
// When the prefix is not a valid symbol name (e.g. a bare number like // When the prefix is not a valid symbol name (e.g. a bare number like
// 0(SP) in RISC-V), sym is nil, and we fall through to regular memory // 0(SP) in RISC-V), sym is nil, and we fall through to regular memory
@@ -577,6 +608,16 @@ func parseAddress(g []token.Token) ast.Address {
} }
} }
} }
// A lone (index*scale) group is the VSIB index-only form: the
// gather/scatter families address memory through a scaled vector index
// with no base register, 8(X4*1). The two-group grammar below reads
// (base)(index*scale), so a first group whose member carries a scale
// factor can only be an index.
if isIndexGroup(g[i:]) {
addr.Index = g[i+1].Text
addr.Scale = int(parseInt(g[i+3].Text))
i += 5
}
// First parenthesised group: the base register. // First parenthesised group: the base register.
if i < len(g) && g[i].Kind == token.LParen { if i < len(g) && g[i].Kind == token.LParen {
i++ i++
@@ -633,6 +674,19 @@ func findPseudoParen(g []token.Token) int {
return -1 return -1
} }
// isBracket reports whether t is a square bracket. The lexer has no bracket
// kind, so '[' and ']' arrive as Illegal tokens.
func isBracket(t token.Token, text string) bool {
return t.Kind == token.Illegal && t.Text == text
}
// isIndexGroup reports whether g begins with a complete (index*scale) group:
// one identifier followed by a scale factor, all inside a single parenthesis.
func isIndexGroup(g []token.Token) bool {
return len(g) >= 5 && g[0].Kind == token.LParen && g[1].Kind == token.Ident &&
g[2].Kind == token.Star && g[3].Kind == token.Number && g[4].Kind == token.RParen
}
// --- token helpers ---------------------------------------------------------- // --- token helpers ----------------------------------------------------------
// splitOperands splits a token slice on top-level commas (commas outside any // splitOperands splits a token slice on top-level commas (commas outside any
+137
View File
@@ -497,3 +497,140 @@ func TestParseEqualsZeroOptions(t *testing.T) {
} }
} }
} }
// TestBracketRegisterRange pins the amd64 multi-source operand of the
// 4FMAPS/4VNNIW families: the bracket group [Z0-Z3] names four consecutive
// source registers and must reach the AST as a register range instead of an
// empty address.
func TestBracketRegisterRange(t *testing.T) {
file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tV4FMADDPS 17(SP), [Z0-Z3], K2, Z0\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse errors: %v", errs)
}
fn := file.Decls[0].(*ast.Text)
in := fn.Body[0].(*ast.Instr)
if len(in.Operands) != 4 {
t.Fatalf("operands = %d, want 4", len(in.Operands))
}
rng := in.Operands[1]
if rng.Kind != ast.OpAddr {
t.Errorf("range operand kind = %v, want OpAddr", rng.Kind)
}
if rng.Addr.Range == nil {
t.Fatalf("range operand = %+v, want a register range", rng.Addr)
}
if rng.Addr.Range.Lo != "Z0" || rng.Addr.Range.Hi != "Z3" {
t.Errorf("range = %s-%s, want Z0-Z3", rng.Addr.Range.Lo, rng.Addr.Range.Hi)
}
if rng.Addr.Sym != nil || rng.Addr.Base != "" || rng.Addr.Index != "" || rng.Addr.Shift != "" {
t.Errorf("range operand carries stray address fields: %+v", rng.Addr)
}
if rng.Raw != "[ Z0 - Z3 ]" {
t.Errorf("range raw = %q, want the verbatim spelling", rng.Raw)
}
}
// TestBracketRegisterRangeNotList pins that arm64-style register lists, whose
// members carry arrangements, stay out of the simple range shape: they remain
// plain bracketed groups the arm64 encoder reads from Raw. A comma inside
// brackets is a top-level comma, so a multi-member list spans several
// operands, exactly the shape the arm64 encoder's list scan stitches back.
func TestBracketRegisterRangeNotList(t *testing.T) {
file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tVLD1 (R2), [V21.B16]\n\tVLD1 (R1), [V2.B16, V3.B16]\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse errors: %v", errs)
}
fn := file.Decls[0].(*ast.Text)
for i, want := range []string{"[ V21.B16 ]", "V3.B16 ]"} {
in := fn.Body[i].(*ast.Instr)
op := in.Operands[len(in.Operands)-1]
if op.Addr.Range != nil {
t.Errorf("%s: range = %v, want nil", in.Mnemonic.Text, op.Addr.Range)
}
if op.Raw != want {
t.Errorf("operand %d raw = %q, want %q", i, op.Raw, want)
}
}
}
// TestVSIBIndexOnly pins the gather/scatter memory operand with a scaled
// vector index and no base register: 8(X4*1) must carry index and scale and
// leave the base empty, not strand the scale in the shift suffix.
func TestVSIBIndexOnly(t *testing.T) {
file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tVPGATHERDQ Y0, 8(X4*1), Y6\n\tVPGATHERDQ Y0, (X4*2), Y6\n\tVPGATHERDQ Y0, -8(X4*1), Y6\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse errors: %v", errs)
}
fn := file.Decls[0].(*ast.Text)
want := []ast.Address{
{Index: "X4", Scale: 1, Offset: 8, HasOff: true},
{Index: "X4", Scale: 2},
{Index: "X4", Scale: 1, Offset: -8, HasOff: true},
}
for i, w := range want {
in := fn.Body[i].(*ast.Instr)
a := in.Operands[1].Addr
if a.Base != "" || a.Index != w.Index || a.Scale != w.Scale || a.Offset != w.Offset || a.HasOff != w.HasOff || a.Shift != "" {
t.Errorf("operand %d = %+v, want %+v", i, a, w)
}
}
}
// TestVSIBTwoGroupKeepsBase pins that the ordinary (base)(index*scale)
// grammar is untouched by the index-only recognition.
func TestVSIBTwoGroupKeepsBase(t *testing.T) {
file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tVP4DPWSSD 7(SI)(DI*1), [Z2-Z5], K4, Z17\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse errors: %v", errs)
}
fn := file.Decls[0].(*ast.Text)
in := fn.Body[0].(*ast.Instr)
a := in.Operands[0].Addr
if a.Base != "SI" || a.Index != "DI" || a.Scale != 1 || a.Offset != 7 || !a.HasOff {
t.Errorf("address = %+v, want base SI index DI scale 1 offset 7", a)
}
if in.Operands[1].Addr.Range == nil || in.Operands[1].Addr.Range.Lo != "Z2" || in.Operands[1].Addr.Range.Hi != "Z5" {
t.Errorf("second operand = %+v, want range Z2-Z5", in.Operands[1].Addr)
}
}
// TestBareTrailingImmediate pins the toolchain's bare constant spelling in
// the final operand slot: CMPSD X1, X0, 1 reads as $1 (math/floor_amd64.s).
// Earlier slots keep the strict grammar, so a bare number there stays an
// address rather than becoming an immediate.
func TestBareTrailingImmediate(t *testing.T) {
file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tCMPSD X1, X0, 1\n\tCMPSD X1, X0, -1\n\tADDQ AX, 1+2\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse errors: %v", errs)
}
fn := file.Decls[0].(*ast.Text)
for i, want := range []int64{1, -1, 3} {
in := fn.Body[i].(*ast.Instr)
last := in.Operands[len(in.Operands)-1]
if last.Kind != ast.OpImmediate || !last.Imm.HasVal || last.Imm.Val != want {
t.Errorf("operand %d = %+v, want immediate %d", i, last, want)
}
}
// A bare number outside the final slot is not an immediate.
file2, errs2 := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tADDQ 1, AX\n\tRET\n")
if len(errs2) > 0 {
t.Fatalf("parse errors: %v", errs2)
}
fn2 := file2.Decls[0].(*ast.Text)
first := fn2.Body[0].(*ast.Instr).Operands[0]
if first.Kind != ast.OpAddr {
t.Errorf("non-final bare number kind = %v, want OpAddr", first.Kind)
}
// A bare name in the final slot stays a symbol: labels are names, not
// constants, and jump targets depend on the distinction.
file3, errs3 := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tJMP loop\nloop: NOP\n\tRET\n")
if len(errs3) > 0 {
t.Fatalf("parse errors: %v", errs3)
}
fn3 := file3.Decls[0].(*ast.Text)
jmp := fn3.Body[0].(*ast.Instr)
if jmp.Operands[0].Kind != ast.OpAddr || jmp.Operands[0].Addr.Sym == nil || jmp.Operands[0].Addr.Sym.Name != "loop" {
t.Errorf("jump target = %+v, want label loop", jmp.Operands[0])
}
}
+89 -2
View File
@@ -349,7 +349,7 @@ func (pp *preproc) expandTokens(in []token.Token) []token.Token {
consecutive = 0 consecutive = 0
continue continue
} }
def := pp.macros[t.Text] def, suffix := pp.macroFor(t.Text)
if def == nil { if def == nil {
i++ i++
consecutive = 0 consecutive = 0
@@ -363,7 +363,14 @@ func (pp *preproc) expandTokens(in []token.Token) []token.Token {
return nil return nil
} }
if def.args == nil { if def.args == nil {
s = append(s[:i], append(restamp(def.body, t.Pos), s[i+1:]...)...) body := restamp(def.body, t.Pos)
if suffix != "" {
// The macro was reached only through a compound spelling
// (ACC0.B16 over "#define ACC0 V8"), so the selector has
// to travel with the expansion.
body = appendSelector(body, suffix, t.Pos)
}
s = append(s[:i], append(body, s[i+1:]...)...)
continue continue
} }
// A parameterised macro invoked without its parentheses stands // A parameterised macro invoked without its parentheses stands
@@ -394,6 +401,17 @@ func (pp *preproc) expandTokens(in []token.Token) []token.Token {
sub = append(sub, restamp(args[k], t.Pos)...) sub = append(sub, restamp(args[k], t.Pos)...)
continue continue
} }
// A parameter used with an element or lane selector: the
// lexer folds A.S4 into one identifier, so the whole-token
// match above cannot see the parameter. The toolchain
// lexes the period separately and substitutes the name
// alone; splitting at the FIRST period and pasting the
// argument back in front of the selector is the equivalent
// for this lexer.
if k, sel := parameterSelector(bt.Text, def.args); k >= 0 {
sub = append(sub, restamp(pasteSelector(args[k], sel), t.Pos)...)
continue
}
} }
sub = append(sub, bt) sub = append(sub, bt)
} }
@@ -402,6 +420,75 @@ func (pp *preproc) expandTokens(in []token.Token) []token.Token {
return s return s
} }
// macroFor finds the macro a use names. The lexer folds NAME.selector into
// one identifier token, so a macro written behind a selector suffix
// (ACC0.B16 over "#define ACC0 V8") never matches a whole-token table
// lookup; the toolchain splits on the period and reads the two halves, so
// the prefix before the FIRST period is tried here as well and the caller
// re-attaches the suffix to whatever the macro expands to. Only a whole
// name counts: AB.S4 does not reach a macro named A, and a parameterised
// macro is not hidden behind a selector, because its invocation would need
// the parentheses to follow the bare name.
func (pp *preproc) macroFor(text string) (*macroDef, string) {
if def := pp.macros[text]; def != nil {
return def, ""
}
if j := strings.IndexByte(text, '.'); j > 0 {
if def := pp.macros[text[:j]]; def != nil && def.args == nil {
return def, text[j:]
}
}
return nil, ""
}
// appendSelector glues a selector suffix onto an object macro's expansion:
// the selector binds to the identifier the expansion ends with, the way the
// toolchain's operand parser reads V0 and .B16 back as one register
// spelling. An expansion that does not end in an identifier carries the
// selector as its own token, which the parser then reports where it cannot
// parse it.
func appendSelector(body []token.Token, suffix string, pos token.Position) []token.Token {
if n := len(body); n > 0 && body[n-1].Kind == token.Ident {
body[n-1].Text += suffix
return body
}
return append(body, token.Token{Kind: token.Ident, Text: suffix, Pos: pos, End: pos})
}
// parameterSelector reports the argument a compound body token names: the
// parameter whose whole name occupies the text before the token's FIRST
// period, with the selector that follows. k is negative when no parameter
// matches, which leaves tokens like AB.S4 untouched even though a parameter
// A is bound.
func parameterSelector(text string, args []string) (int, string) {
j := strings.IndexByte(text, '.')
if j <= 0 {
return -1, ""
}
if k := slices.Index(args, text[:j]); k >= 0 {
return k, text[j:]
}
return -1, ""
}
// pasteSelector joins an argument with the selector a compound body token
// carries, textually: the selector binds to the identifier the argument
// ends with, so A.S4 over the argument V0.B16 spells V0.B16.S4, exactly the
// operand the toolchain's split-then-substitute leaves behind. An argument
// with no trailing identifier carries the selector as a separate token,
// which the parser then reports where it cannot parse it.
func pasteSelector(val []token.Token, suffix string) []token.Token {
if len(val) == 0 {
return []token.Token{{Kind: token.Ident, Text: suffix}}
}
out := slices.Clone(val)
if n := len(out); out[n-1].Kind == token.Ident {
out[n-1].Text += suffix
return out
}
return append(out, token.Token{Kind: token.Ident, Text: suffix})
}
// collectArgs reads the actual argument tokens of an invocation; the opening // collectArgs reads the actual argument tokens of an invocation; the opening
// parenthesis is at start. Commas separate arguments except inside nested // parenthesis is at start. Commas separate arguments except inside nested
// parentheses. A nil result means the list was unterminated, which is a // parentheses. A nil result means the list was unterminated, which is a
+121
View File
@@ -536,6 +536,127 @@ func TestConstantExpressionFoldsWithoutExpand(t *testing.T) {
} }
} }
func TestParameterWithSelectorSubstitutes(t *testing.T) {
// The lexer folds A.S4 into one identifier token, so a parameter used
// with an element or lane selector never matched the whole-token
// substitution; the toolchain's lexer splits on the period and its
// substitution sees the name alone. Several parameters carry selectors
// in one body here, which is the chacha8_arm64.s QR shape in miniature.
_, got := expand(t, `
#define QR(A, B, C, D) VADD A.S4, B.S4, C.S4; VEOR D.B16, A.B16, D.B16
TEXT ·f(SB), NOSPLIT, $0
QR(V0, V1, V2, V3)
RET
`)
wantLines(t, got,
"VADD V0.S4, V1.S4, V2.S4",
"VEOR V3.B16, V0.B16, V3.B16",
"RET",
)
}
func TestSelectorWithCompoundArgumentPastesTextually(t *testing.T) {
// An argument that is itself one compound identifier pastes verbatim:
// A.S4 over V0.B16 spells V0.B16.S4, the operand the toolchain's
// split-then-substitute leaves behind.
_, got := expand(t, `
#define M(A) VADD A.S4, A.S4, A.S4
TEXT ·f(SB), NOSPLIT, $0
M(V0.B16)
RET
`)
wantLines(t, got, "VADD V0.B16.S4, V0.B16.S4, V0.B16.S4", "RET")
}
func TestSelectorAlongsideBareParameter(t *testing.T) {
// A body may use the parameter bare and suffixed, and the argument may
// itself end in a selector; neither disturbs the other.
_, got := expand(t, `
#define M(A) VADD A, A.S4, A
TEXT ·f(SB), NOSPLIT, $0
M(V0)
M(V1.B16)
RET
`)
wantLines(t, got,
"VADD V0, V0.S4, V0",
"VADD V1.B16, V1.B16.S4, V1.B16",
"RET",
)
}
func TestSelectorKeepsNonParameterPrefixes(t *testing.T) {
// The prefix before the period must be the whole parameter name:
// AB.S4 never reaches a parameter A.
_, got := expand(t, `
#define M(A) VADD AB.S4, A.S4, AB.S4
TEXT ·f(SB), NOSPLIT, $0
M(V0)
RET
`)
wantLines(t, got, "VADD AB.S4, V0.S4, AB.S4", "RET")
}
func TestSelectorExpandsMacroValuedArgument(t *testing.T) {
// gcm_arm64.s invokes mulRound(B1) where B1 is itself an object macro:
// the paste stays rescannable, so B1.D1 still expands to V1.D1 the way
// the toolchain's rescan of substituted tokens does.
_, got := expand(t, `
#define B1 V1
#define mulRound(X) VPMULL X.D1, T1.D1, T3.Q1
TEXT ·f(SB), NOSPLIT, $0
mulRound(B1)
RET
`)
wantLines(t, got, "VPMULL V1.D1, T1.D1, T3.Q1", "RET")
}
func TestObjectMacroBehindSelectorExpands(t *testing.T) {
// Ordinary code writes ACC0.B16 where ACC0 is an object macro; the
// toolchain expands the alias because its lexer reads the selector as
// its own token, and the lookup here must reach the macro through the
// compound spelling the same way.
_, got := expand(t, `
#define ACC0 V8
TEXT ·f(SB), NOSPLIT, $0
VEOR ACC0.B16, ACC0.B16, ACC0.B16
RET
`)
wantLines(t, got, "VEOR V8.B16, V8.B16, V8.B16", "RET")
}
func TestChacha8QRMacroExpands(t *testing.T) {
// The real QR round of chacha8_arm64.s end to end: every parameter
// carries a selector somewhere, and the round is sixteen instructions.
_, got := expand(t, `
#define QR(A, B, C, D) \
VADD A.S4, B.S4, A.S4; VEOR D.B16, A.B16, D.B16; VREV32 D.H8, D.H8; \
VADD C.S4, D.S4, C.S4; VEOR B.B16, C.B16, V30.B16; VSHL $12, V30.S4, B.S4; VSRI $20, V30.S4, B.S4; \
VADD A.S4, B.S4, A.S4; VEOR D.B16, A.B16, D.B16; VTBL V31.B16, [D.B16], D.B16; \
VADD C.S4, D.S4, C.S4; VEOR B.B16, C.B16, V30.B16; VSHL $7, V30.S4, B.S4; VSRI $25, V30.S4, B.S4
TEXT ·f(SB), NOSPLIT, $0
QR(V0, V1, V2, V3)
RET
`)
wantLines(t, got,
"VADD V0.S4, V1.S4, V0.S4",
"VEOR V3.B16, V0.B16, V3.B16",
"VREV32 V3.H8, V3.H8",
"VADD V2.S4, V3.S4, V2.S4",
"VEOR V1.B16, V2.B16, V30.B16",
"VSHL $12, V30.S4, V1.S4",
"VSRI $20, V30.S4, V1.S4",
"VADD V0.S4, V1.S4, V0.S4",
"VEOR V3.B16, V0.B16, V3.B16",
"VTBL V31.B16, [V3.B16], V3.B16",
"VADD V2.S4, V3.S4, V2.S4",
"VEOR V1.B16, V2.B16, V30.B16",
"VSHL $7, V30.S4, V1.S4",
"VSRI $25, V30.S4, V1.S4",
"RET",
)
}
func TestNotAnExpressionFallsBack(t *testing.T) { func TestNotAnExpressionFallsBack(t *testing.T) {
// Symbol immediates and floats must keep their ordinary parse. // Symbol immediates and floats must keep their ordinary parse.
f, errs := ParseWithOptions("t_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ $1.5, AX\n\tMOVQ $·sym(SB), AX\n\tRET\n", Options{}) f, errs := ParseWithOptions("t_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ $1.5, AX\n\tMOVQ $·sym(SB), AX\n\tRET\n", Options{})
+21
View File
@@ -0,0 +1,21 @@
#include "textflag.h"
// The index-only VSIB shapes from amd64enc_extra.s: a scaled vector index
// addresses memory with no base register. The comments record the bytes the
// Go assembler emits.
TEXT asmtest(SB), DUPOK|NOSPLIT, $0
VPGATHERDQ Y0, 8(X4*1), Y6 // c4e2fd90342508000000
VPGATHERDQ Y0,-8(X4*1), Y6 // c4e2fd903425f8ffffff
VPGATHERDQ Y0, 0(X4*1), Y6 // c4e2fd90342500000000
VPGATHERDQ Y0, 664(X4*1), Y6 // c4e2fd90342598020000
VPGATHERDQ Y0, 8(X14*1), Y6 // c4a2fd90343508000000
VPGATHERDQ Y0, 8(X4*8), Y6 // c4e2fd9034e508000000
VGATHERDPD Y0, 8(X4*1), Y6 // c4e2fd92342508000000
VPGATHERDD X0, 8(X4*1), X6 // c4e27d90342508000000
VPGATHERDD Y0, 8(Y4*1), Y6 // c4e2fd90342508000000
// The bare trailing immediate of math/floor_amd64.s: the toolchain
// reads the final 1 as $1.
CMPSD X1, X0, 1 // f20fc2c101
CMPSD X1, X0, 2 // f20fc2c102
RET
+2
View File
@@ -123,9 +123,11 @@ func TestGroundTruthAMD64(t *testing.T) {
"../testdata/verify/sse_amd64.s", "../testdata/verify/sse_amd64.s",
"../testdata/verify/avx_amd64.s", "../testdata/verify/avx_amd64.s",
"../testdata/verify/pfx_amd64.s", "../testdata/verify/pfx_amd64.s",
"../testdata/verify/vsib_amd64.s",
"../testdata/verify/rawdata_amd64.s", "../testdata/verify/rawdata_amd64.s",
"../testdata/verify/avx512_amd64.s", "../testdata/verify/avx512_amd64.s",
"../testdata/verify/pfx_amd64.s", "../testdata/verify/pfx_amd64.s",
"../testdata/verify/vsib_amd64.s",
"../testdata/verify/rawdata_amd64.s", "../testdata/verify/rawdata_amd64.s",
"../testdata/verify/avx512_amd64.s", "../testdata/verify/avx512_amd64.s",
"../testdata/verify/doubleshift_amd64.s", "../testdata/verify/doubleshift_amd64.s",