Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf05b9b384 | ||
|
|
a7744c24bd | ||
|
|
522e6f2ae8 |
@@ -846,6 +846,14 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Op
|
||||
case ast.OpAddr:
|
||||
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
|
||||
// symbol, not the address displacement.
|
||||
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
|
||||
}
|
||||
// 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.
|
||||
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
|
||||
if r, ok := ParseReg(a.Sym.Name); ok {
|
||||
|
||||
+17
-7
@@ -151,11 +151,21 @@ type Immediate struct {
|
||||
// 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.
|
||||
type Address struct {
|
||||
Sym *Symbol // name reference (bare ident, or name+off(pseudo))
|
||||
Base string // base register, from (base)
|
||||
Index string // index register, from (index*scale)
|
||||
Scale int // index scale; 0 when absent
|
||||
Offset int64 // leading displacement, from off(base)
|
||||
HasOff bool // a leading displacement is present
|
||||
Shift string // verbatim arm64 shift suffix, e.g. "<< 2"
|
||||
Sym *Symbol // name reference (bare ident, or name+off(pseudo))
|
||||
Base string // base register, from (base)
|
||||
Index string // index register, from (index*scale)
|
||||
Scale int // index scale; 0 when absent
|
||||
Offset int64 // leading displacement, from off(base)
|
||||
HasOff bool // a leading displacement is present
|
||||
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
@@ -27,7 +27,10 @@ import (
|
||||
// GOROOT assembly includes it, and a standalone assembler has no compiler
|
||||
// to have produced it, so gasm generates the equivalent itself: the package
|
||||
// the .s file lives in is parsed and type-checked here, with the target
|
||||
// architecture's own sizes, and the same defines are written out.
|
||||
// architecture's own sizes, and the same defines are written out. The
|
||||
// type-checking GOOS is selected by the caller: a GOOS-specific file
|
||||
// (sys_darwin_arm64.s) needs its platform's defines, which a header from
|
||||
// the ambient GOOS silently omits.
|
||||
//
|
||||
// The emitter mirrors cmd/compile's dumpasmhdr exactly: constants come out
|
||||
// as "const_NAME", struct entries as "NAME__size" followed by the fields in
|
||||
@@ -67,11 +70,15 @@ func goAsmHeaderResolved(asmDir string, dirs []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// generateGoAsmHeader type-checks the Go package in pkgDir for goarch,
|
||||
// writes its go_asm.h equivalent into dir, and returns dir. The caller
|
||||
// owns the directory and its removal.
|
||||
func generateGoAsmHeader(pkgDir, goarch, dir string) (string, error) {
|
||||
imp := newSourceImporter(goarch)
|
||||
// generateGoAsmHeader type-checks the Go package in pkgDir for goos and
|
||||
// goarch, writes its go_asm.h equivalent into dir, and returns dir. An
|
||||
// empty goos means the ambient one. The caller owns the directory and its
|
||||
// removal.
|
||||
func generateGoAsmHeader(pkgDir, goos, goarch, dir string) (string, error) {
|
||||
if goos == "" {
|
||||
goos = build.Default.GOOS
|
||||
}
|
||||
imp := newSourceImporter(goos, goarch)
|
||||
if imp.sizes == nil {
|
||||
return "", fmt.Errorf("go_asm.h: unknown GOARCH %q", goarch)
|
||||
}
|
||||
@@ -89,7 +96,7 @@ func generateGoAsmHeader(pkgDir, goarch, dir string) (string, error) {
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "// generated by gasm from package %s (GOARCH %s)\n\n", bp.Name, goarch)
|
||||
fmt.Fprintf(&b, "// generated by gasm from package %s (GOOS %s, GOARCH %s)\n\n", bp.Name, goos, goarch)
|
||||
// Files in the build's own order and declarations in source order: the
|
||||
// same walk the compiler's reader makes, so the header reads the same
|
||||
// way the toolchain's does. Order carries no meaning to the assembler
|
||||
@@ -203,13 +210,14 @@ type sourceImporter struct {
|
||||
pkgs map[string]*types.Package
|
||||
}
|
||||
|
||||
// newSourceImporter returns the importer for one target architecture.
|
||||
// newSourceImporter returns the importer for one target GOOS and GOARCH.
|
||||
// Cgo is disabled so the file set is deterministic and independent of the
|
||||
// host's C toolchain: cgo-tagged files drop out of the build exactly as
|
||||
// they do from a CGO_ENABLED=0 build, whose assembly is what gasm targets.
|
||||
func newSourceImporter(goarch string) *sourceImporter {
|
||||
func newSourceImporter(goos, goarch string) *sourceImporter {
|
||||
ctxt := new(build.Context)
|
||||
*ctxt = build.Default
|
||||
ctxt.GOOS = goos
|
||||
ctxt.GOARCH = goarch
|
||||
ctxt.CgoEnabled = false
|
||||
return &sourceImporter{
|
||||
@@ -291,7 +299,7 @@ func (im *sourceImporter) checkPackage(bp *build.Package, files []*ast.File) (*t
|
||||
// type-check must not be re-checked once per file.
|
||||
type asmhdrCache struct {
|
||||
root string
|
||||
dirs map[string]string // "pkgDir\x00goarch" -> directory holding go_asm.h
|
||||
dirs map[string]string // "pkgDir\x00goos\x00goarch" -> directory holding go_asm.h
|
||||
errs map[string]error
|
||||
}
|
||||
|
||||
@@ -304,17 +312,22 @@ func newAsmhdrCache() (*asmhdrCache, error) {
|
||||
}
|
||||
|
||||
// dirFor returns the directory holding the generated go_asm.h for pkgDir
|
||||
// and goarch, generating it on first use.
|
||||
func (c *asmhdrCache) dirFor(pkgDir, goarch string) (string, error) {
|
||||
key := pkgDir + "\x00" + goarch
|
||||
// under goos and goarch, generating it on first use. An empty goos means
|
||||
// the ambient one, resolved here so that one package cannot generate twice
|
||||
// under an explicit and an implicit spelling of the same GOOS.
|
||||
func (c *asmhdrCache) dirFor(pkgDir, goos, goarch string) (string, error) {
|
||||
if goos == "" {
|
||||
goos = build.Default.GOOS
|
||||
}
|
||||
key := pkgDir + "\x00" + goos + "\x00" + goarch
|
||||
if dir, ok := c.dirs[key]; ok {
|
||||
return dir, nil
|
||||
}
|
||||
if err, ok := c.errs[key]; ok {
|
||||
return "", err
|
||||
}
|
||||
dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s", len(c.dirs), goarch))
|
||||
if _, err := generateGoAsmHeader(pkgDir, goarch, dir); err != nil {
|
||||
dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s_%s", len(c.dirs), goos, goarch))
|
||||
if _, err := generateGoAsmHeader(pkgDir, goos, goarch, dir); err != nil {
|
||||
c.errs[key] = err
|
||||
return "", err
|
||||
}
|
||||
@@ -327,10 +340,10 @@ func (c *asmhdrCache) close() { os.RemoveAll(c.root) }
|
||||
|
||||
// ensureGoAsmHeader prepares the include directory a file that includes
|
||||
// go_asm.h needs: the generated header for the package in path's directory,
|
||||
// for the file's target architecture. It reports a usage error when the
|
||||
// architecture cannot be determined, and passes through the generator's
|
||||
// diagnostics, which name the package.
|
||||
func ensureGoAsmHeader(path string, target arch.Arch, cache *asmhdrCache) (string, func(), error) {
|
||||
// for the file's target GOOS and architecture. It reports a usage error
|
||||
// when the architecture cannot be determined, and passes through the
|
||||
// generator's diagnostics, which name the package.
|
||||
func ensureGoAsmHeader(path string, target arch.Arch, goos string, cache *asmhdrCache) (string, func(), error) {
|
||||
if path == "-" {
|
||||
return "", nil, errors.New("cannot generate go_asm.h for standard input (no package directory)")
|
||||
}
|
||||
@@ -338,14 +351,14 @@ func ensureGoAsmHeader(path string, target arch.Arch, cache *asmhdrCache) (strin
|
||||
return "", nil, errors.New("a file that includes go_asm.h needs a target architecture: name the file _<arch>.s or pass -GOARCH")
|
||||
}
|
||||
if cache != nil {
|
||||
dir, err := cache.dirFor(filepath.Dir(path), goarchName(target))
|
||||
dir, err := cache.dirFor(filepath.Dir(path), goos, goarchName(target))
|
||||
return dir, func() {}, err
|
||||
}
|
||||
root, err := os.MkdirTemp("", "gasm-asmhdr")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
dir, err := generateGoAsmHeader(filepath.Dir(path), goarchName(target), root)
|
||||
dir, err := generateGoAsmHeader(filepath.Dir(path), goos, goarchName(target), root)
|
||||
if err != nil {
|
||||
os.RemoveAll(root)
|
||||
return "", nil, err
|
||||
|
||||
+68
-9
@@ -5,6 +5,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -288,6 +289,11 @@ func probeShapes(a arch.Arch) []string {
|
||||
"V1.B16, [V2.B16], V3.B16", "V1.B8, [V2.B16, V3.B16], V4.B8",
|
||||
"$4, V1.B16, V2.B16, V3.B16", "$15, V1", "V1, V2, p2",
|
||||
"R0, R1, $1, $4, p2",
|
||||
// The landing-pad kind, the compiler's PCDATA
|
||||
// bookkeeping and the four-operand bitfield
|
||||
// insert/extract family, as the toolchain's own
|
||||
// testdata spells them.
|
||||
"C", "$1, $0", "$0, R1, $1, R2",
|
||||
}
|
||||
case arch.RISCV:
|
||||
return []string{
|
||||
@@ -307,6 +313,9 @@ func probeShapes(a arch.Arch) []string {
|
||||
"X5, X6, p2", "R5, R6, p2",
|
||||
"X5, E8, M8, TA, MA, X6", "$4, E32, M1, TA, MA, X1",
|
||||
"(X5), X6, V1, V2",
|
||||
// The CSR immediate forms the toolchain's testdata spells:
|
||||
// immediate, CSR name, destination.
|
||||
"$2, TIME, X5",
|
||||
"",
|
||||
}
|
||||
case arch.LOONG64:
|
||||
@@ -323,6 +332,12 @@ func probeShapes(a arch.Arch) []string {
|
||||
"V1, V2, V3", "X1, X2, X3", "V1, V2", "X1, X2", "V1", "X1",
|
||||
// The vector compare-to-flag forms land in an FCC register.
|
||||
"V1, FCC0", "X1, FCC0",
|
||||
// The compiler's bookkeeping pair and the raw spellings the
|
||||
// toolchain's own testdata carries: JIRL rd, rj, offset (the
|
||||
// form RET lowers to), the prefetch with a 32-bit address and
|
||||
// hint, and the byte-shuffle quads.
|
||||
"$1, $0", "R1, R5, 0", "0(R7), $5, $0", "(R7), $5, $0",
|
||||
"V1, V2, V3, V4", "X1, X2, X3, X4",
|
||||
"",
|
||||
}
|
||||
}
|
||||
@@ -487,10 +502,47 @@ func otherPortFile(path string) bool {
|
||||
|
||||
// goOSNames are the GOOS values go/build recognises in file names.
|
||||
var goOSNames = map[string]bool{
|
||||
"aix": true, "darwin": true, "dragonfly": true, "freebsd": true,
|
||||
"ios": true, "js": true, "linux": true, "netbsd": true,
|
||||
"aix": true, "android": true, "darwin": true, "dragonfly": true,
|
||||
"freebsd": true, "hurd": true, "illumos": true, "ios": true,
|
||||
"js": true, "linux": true, "nacl": true, "netbsd": true,
|
||||
"openbsd": true, "plan9": true, "solaris": true, "wasip1": true,
|
||||
"windows": true,
|
||||
"windows": true, "zos": true,
|
||||
}
|
||||
|
||||
// resolveGOOS validates a -GOOS flag value, mirroring the architecture
|
||||
// check's surface: a usage error naming what the tool accepts.
|
||||
func resolveGOOS(name string) (string, error) {
|
||||
lower := strings.ToLower(name)
|
||||
if goOSNames[lower] {
|
||||
return lower, nil
|
||||
}
|
||||
return "", &usageError{fmt.Errorf("unknown GOOS %q: want one of %s", name, strings.Join(slices.Sorted(maps.Keys(goOSNames)), ", "))}
|
||||
}
|
||||
|
||||
// goosFromFilename returns the GOOS the file's name carries, by go/build's
|
||||
// goodOSArchFile rule: the GOOS segment sits last, or last before the
|
||||
// architecture segment (sys_darwin_arm64.s, vlop_arm.s carries none). An
|
||||
// empty result means the name names no GOOS and the ambient one applies.
|
||||
func goosFromFilename(path string) string {
|
||||
base := path
|
||||
if i := strings.LastIndexByte(base, '/'); i >= 0 {
|
||||
base = base[i+1:]
|
||||
}
|
||||
base = strings.TrimSuffix(base, ".s")
|
||||
// go/build ignores everything before the first underscore, so a GOOS
|
||||
// segment is only ever looked for from there on.
|
||||
i := strings.IndexByte(base, '_')
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
segs := strings.Split(base[i:], "_")
|
||||
if n := len(segs); n >= 2 && goOSNames[segs[n-2]] && slices.Contains(goPortSuffixes, segs[n-1]) {
|
||||
return segs[n-2]
|
||||
}
|
||||
if goOSNames[segs[len(segs)-1]] {
|
||||
return segs[len(segs)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// otherGOOSFile reports whether the file's name names a GOOS other than the
|
||||
@@ -543,6 +595,12 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The GOOS the header generation type-checks under follows the
|
||||
// file's name when the name carries one; the ambient GOOS is the
|
||||
// honest guess otherwise (a build tag naming another GOOS is
|
||||
// invisible to a file-name rule).
|
||||
goos := goosFromFilename(path)
|
||||
|
||||
var wanted []int // indexes into targets
|
||||
if a := arch.FromFilename(path); a != arch.Unknown {
|
||||
for i, tg := range targets {
|
||||
@@ -567,11 +625,12 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
||||
|
||||
// A file that includes go_asm.h parses against a per-target header:
|
||||
// the defines differ per architecture (internal/cpu's layout, for
|
||||
// one), so the parse cannot be shared the way a header-free file's
|
||||
// can. A generation failure is a failure for every target, named
|
||||
// for the package rather than a bare "include not found". A header
|
||||
// already resolvable in the package directory or the -I list is
|
||||
// left alone.
|
||||
// one) and per GOOS (sys_darwin_arm64.s's trampoline constants,
|
||||
// for another), so the parse cannot be shared the way a
|
||||
// header-free file's can. A generation failure is a failure for
|
||||
// every target, named for the package rather than a bare "include
|
||||
// not found". A header already resolvable in the package
|
||||
// directory or the -I list is left alone.
|
||||
if len(wanted) > 0 && needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) {
|
||||
if hdr == nil {
|
||||
if hdr, err = newAsmhdrCache(); err != nil {
|
||||
@@ -583,7 +642,7 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
||||
for _, i := range wanted {
|
||||
tg, t := targets[i], tallies[i]
|
||||
t.attempted++
|
||||
hdrDir, err := hdr.dirFor(pkgDir, goarchName(tg.a))
|
||||
hdrDir, err := hdr.dirFor(pkgDir, goos, goarchName(tg.a))
|
||||
if err != nil {
|
||||
ok = false
|
||||
t.fail(path, corpusReason(err))
|
||||
|
||||
+18
-5
@@ -486,7 +486,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
|
||||
}
|
||||
|
||||
func cmdAsm(args []string) int {
|
||||
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] <file>", `
|
||||
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] <file>", `
|
||||
Assemble FILE without the Go toolchain: every TEXT function is encoded to
|
||||
machine code and printed as a hex dump. Supported architectures: amd64
|
||||
(including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP,
|
||||
@@ -507,18 +507,22 @@ and the format version from go version).
|
||||
A file that includes go_asm.h gets that header generated automatically from
|
||||
the package it lives in (the .go files beside it, type-checked for the
|
||||
target architecture, the toolchain's own defines), so GOROOT assembly
|
||||
assembles without a compiler. A package that has no Go files for the
|
||||
target or does not type-check is a hard error naming the package.
|
||||
assembles without a compiler. -GOOS selects the type-checking GOOS for
|
||||
that header: a GOOS-specific file (sys_darwin_arm64.s) needs its platform's
|
||||
defines, which a header from the ambient GOOS silently omits. A package
|
||||
that has no Go files for the target or does not type-check is a hard error
|
||||
naming the package.
|
||||
`)
|
||||
out := fs.String("o", "", "write the output to this file")
|
||||
format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)")
|
||||
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
|
||||
archName := fs.String("GOARCH", "", "target architecture: amd64, arm64, riscv64 or loong64 (overrides the file-name suffix)")
|
||||
goosName := fs.String("GOOS", "", "operating system for go_asm.h generation: a GOOS go/build recognises (default: the host's)")
|
||||
var dirs includeDirs
|
||||
fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)")
|
||||
fs.Parse(args)
|
||||
if fs.NArg() != 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] <file>")
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] <file>")
|
||||
return 2
|
||||
}
|
||||
// The format is validated before anything else, so a bogus value exits 2
|
||||
@@ -539,6 +543,15 @@ target or does not type-check is a hard error naming the package.
|
||||
}
|
||||
targetArch = a
|
||||
}
|
||||
goos := ""
|
||||
if *goosName != "" {
|
||||
g, err := resolveGOOS(*goosName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm asm: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
goos = g
|
||||
}
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||
@@ -553,7 +566,7 @@ target or does not type-check is a hard error naming the package.
|
||||
// A go_asm.h that already resolves (placed by hand, or passed with -I)
|
||||
// is left alone.
|
||||
if needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) {
|
||||
hdrDir, cleanup, err := ensureGoAsmHeader(path, targetArch, nil)
|
||||
hdrDir, cleanup, err := ensureGoAsmHeader(path, targetArch, goos, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm asm:", err)
|
||||
return 1
|
||||
|
||||
+12
-2
@@ -141,7 +141,7 @@ gasm lint kernel_amd64.s
|
||||
## asm
|
||||
|
||||
```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 |
|
||||
@@ -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 |
|
||||
| `-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 |
|
||||
| `-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 |
|
||||
|
||||
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
|
||||
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
|
||||
parameterised) expand at the point of use, `#undef`, `#ifdef`, `#ifndef`,
|
||||
`#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.
|
||||
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
|
||||
`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
|
||||
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
|
||||
|
||||
+14
-1
@@ -2,7 +2,7 @@
|
||||
.SH NAME
|
||||
gasm-asm \- assemble Plan 9 assembly without the Go toolchain
|
||||
.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
|
||||
Assemble FILE without the Go toolchain: every TEXT function is encoded
|
||||
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
|
||||
morestack block, byte-identical to the toolchain's output, so split
|
||||
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
|
||||
.TP
|
||||
.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
|
||||
files (cpu_x86.s, stub.s, ...) become assemblable.
|
||||
.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
|
||||
Write the output to this file instead of a hex dump on stdout.
|
||||
.SH EXIT STATUS
|
||||
|
||||
@@ -241,6 +241,13 @@ func renderInstr(line []token.Token, width int) string {
|
||||
if line[0].Kind != token.Ident {
|
||||
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) {
|
||||
width = len(mnem)
|
||||
}
|
||||
|
||||
@@ -364,6 +364,17 @@ func TestSemicolonSeparators(t *testing.T) {
|
||||
in: "TEXT ·f(SB), $0\nBYTE $1;\nRET\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 {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
+60
-6
@@ -304,7 +304,7 @@ func (p *state) parseGlobl(line []token.Token) *ast.Globl {
|
||||
rest = rest[1:]
|
||||
}
|
||||
if len(rest) > 0 && rest[0].Kind == token.Dollar {
|
||||
g.Size = parseOperand(rest)
|
||||
g.Size = parseOperand(rest, false)
|
||||
}
|
||||
return g
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func (p *state) parseData(line []token.Token) *ast.Data {
|
||||
d.Name = sym
|
||||
d.Width = width
|
||||
if len(valuePart) > 0 {
|
||||
d.Value = parseOperand(stripComment(valuePart))
|
||||
d.Value = parseOperand(stripComment(valuePart), false)
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -332,8 +332,13 @@ func (p *state) parseInstr(line []token.Token) {
|
||||
return
|
||||
}
|
||||
instr := &ast.Instr{Mnemonic: body[0], Comment: comment}
|
||||
for _, grp := range splitOperands(body[1:]) {
|
||||
if op := parseOperand(grp); op != nil {
|
||||
grps := splitOperands(body[1:])
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -417,8 +422,10 @@ func setName(raw string, sym *ast.Symbol) {
|
||||
|
||||
// --- operand parsing --------------------------------------------------------
|
||||
|
||||
// parseOperand parses one operand group into an Operand.
|
||||
func parseOperand(g []token.Token) *ast.Operand {
|
||||
// parseOperand parses one operand group into an Operand. allowBare marks
|
||||
// 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)
|
||||
if len(g) == 0 {
|
||||
return nil
|
||||
@@ -431,9 +438,25 @@ func parseOperand(g []token.Token) *ast.Operand {
|
||||
}
|
||||
op.Kind = ast.OpAddr
|
||||
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
|
||||
}
|
||||
|
||||
// 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 '$'.
|
||||
func parseImmediate(g []token.Token) ast.Immediate {
|
||||
var imm ast.Immediate
|
||||
@@ -497,6 +520,14 @@ func parseAddress(g []token.Token) ast.Address {
|
||||
if len(g) == 0 {
|
||||
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).
|
||||
// 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
|
||||
@@ -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.
|
||||
if i < len(g) && g[i].Kind == token.LParen {
|
||||
i++
|
||||
@@ -633,6 +674,19 @@ func findPseudoParen(g []token.Token) int {
|
||||
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 ----------------------------------------------------------
|
||||
|
||||
// splitOperands splits a token slice on top-level commas (commas outside any
|
||||
|
||||
@@ -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
@@ -349,7 +349,7 @@ func (pp *preproc) expandTokens(in []token.Token) []token.Token {
|
||||
consecutive = 0
|
||||
continue
|
||||
}
|
||||
def := pp.macros[t.Text]
|
||||
def, suffix := pp.macroFor(t.Text)
|
||||
if def == nil {
|
||||
i++
|
||||
consecutive = 0
|
||||
@@ -363,7 +363,14 @@ func (pp *preproc) expandTokens(in []token.Token) []token.Token {
|
||||
return 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
|
||||
}
|
||||
// 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)...)
|
||||
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)
|
||||
}
|
||||
@@ -402,6 +420,75 @@ func (pp *preproc) expandTokens(in []token.Token) []token.Token {
|
||||
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
|
||||
// parenthesis is at start. Commas separate arguments except inside nested
|
||||
// parentheses. A nil result means the list was unterminated, which is a
|
||||
|
||||
@@ -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) {
|
||||
// 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{})
|
||||
|
||||
Vendored
+21
@@ -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
|
||||
@@ -123,9 +123,11 @@ func TestGroundTruthAMD64(t *testing.T) {
|
||||
"../testdata/verify/sse_amd64.s",
|
||||
"../testdata/verify/avx_amd64.s",
|
||||
"../testdata/verify/pfx_amd64.s",
|
||||
"../testdata/verify/vsib_amd64.s",
|
||||
"../testdata/verify/rawdata_amd64.s",
|
||||
"../testdata/verify/avx512_amd64.s",
|
||||
"../testdata/verify/pfx_amd64.s",
|
||||
"../testdata/verify/vsib_amd64.s",
|
||||
"../testdata/verify/rawdata_amd64.s",
|
||||
"../testdata/verify/avx512_amd64.s",
|
||||
"../testdata/verify/doubleshift_amd64.s",
|
||||
|
||||
Reference in New Issue
Block a user