2026-07-06 09:49:50 +02:00
|
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
|
|
package parser
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-19 23:48:47 +02:00
|
|
|
|
"math"
|
2026-07-06 09:49:50 +02:00
|
|
|
|
"os"
|
2026-09-19 23:48:47 +02:00
|
|
|
|
"slices"
|
|
|
|
|
|
"strings"
|
2026-07-06 09:49:50 +02:00
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func mustParse(t *testing.T, path string) *ast.File {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
src, err := os.ReadFile(path)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("read %s: %v", path, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
file, errs := Parse(path, string(src))
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse %s: %v", path, errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
return file
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func texts(f *ast.File) []*ast.Text {
|
|
|
|
|
|
var out []*ast.Text
|
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
|
if t, ok := d.(*ast.Text); ok {
|
|
|
|
|
|
out = append(out, t)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 12:51:35 +02:00
|
|
|
|
// TestNegativeDisplacement is a regression test for a leading negative
|
|
|
|
|
|
// displacement with a base and index: the sign pushed the parenthesis one
|
|
|
|
|
|
// token further out than the lookahead expected, and the whole address used
|
|
|
|
|
|
// to parse empty.
|
|
|
|
|
|
func TestNegativeDisplacement(t *testing.T) {
|
|
|
|
|
|
f, errs := Parse("neg_amd64.s", `
|
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
|
LEAQ -4(DX)(R9*4), R9
|
|
|
|
|
|
MOVQ +8(AX), BX
|
|
|
|
|
|
RET
|
|
|
|
|
|
`)
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
fn := texts(f)[0]
|
|
|
|
|
|
var leaq, movq *ast.Instr
|
|
|
|
|
|
for _, s := range fn.Body {
|
|
|
|
|
|
if in, ok := s.(*ast.Instr); ok {
|
|
|
|
|
|
switch in.Mnemonic.Text {
|
|
|
|
|
|
case "LEAQ":
|
|
|
|
|
|
leaq = in
|
|
|
|
|
|
case "MOVQ":
|
|
|
|
|
|
movq = in
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if leaq == nil || movq == nil {
|
|
|
|
|
|
t.Fatalf("instructions not parsed: leaq=%v movq=%v", leaq, movq)
|
|
|
|
|
|
}
|
|
|
|
|
|
a := leaq.Operands[0].Addr
|
|
|
|
|
|
if a.Base != "DX" || a.Index != "R9" || a.Scale != 4 || a.Offset != -4 || !a.HasOff {
|
|
|
|
|
|
t.Errorf("LEAQ addr = %+v, want -4(DX)(R9*4)", a)
|
|
|
|
|
|
}
|
|
|
|
|
|
b := movq.Operands[0].Addr
|
|
|
|
|
|
if b.Base != "AX" || b.Offset != 8 || !b.HasOff {
|
|
|
|
|
|
t.Errorf("MOVQ addr = %+v, want +8(AX)", b)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
|
func TestParseSample(t *testing.T) {
|
|
|
|
|
|
f := mustParse(t, "../testdata/sample_amd64.s")
|
|
|
|
|
|
|
|
|
|
|
|
// Includes, GLOBL/DATA and two TEXT functions.
|
|
|
|
|
|
var includes, globls, datas int
|
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
|
switch d.(type) {
|
|
|
|
|
|
case *ast.Include:
|
|
|
|
|
|
includes++
|
|
|
|
|
|
case *ast.Globl:
|
|
|
|
|
|
globls++
|
|
|
|
|
|
case *ast.Data:
|
|
|
|
|
|
datas++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if includes != 1 {
|
|
|
|
|
|
t.Errorf("includes = %d, want 1", includes)
|
|
|
|
|
|
}
|
|
|
|
|
|
if globls != 2 {
|
|
|
|
|
|
t.Errorf("globls = %d, want 2", globls)
|
|
|
|
|
|
}
|
|
|
|
|
|
if datas != 4 {
|
|
|
|
|
|
t.Errorf("datas = %d, want 4", datas)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
txts := texts(f)
|
|
|
|
|
|
if len(txts) != 2 {
|
|
|
|
|
|
t.Fatalf("text functions = %d, want 2", len(txts))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn := txts[0]
|
|
|
|
|
|
if fn.Name.Name != "analyzeO1RangeAVX2" {
|
|
|
|
|
|
t.Errorf("name = %q, want analyzeO1RangeAVX2", fn.Name.Name)
|
|
|
|
|
|
}
|
|
|
|
|
|
if fn.Name.Pseudo != "SB" {
|
|
|
|
|
|
t.Errorf("pseudo = %q, want SB", fn.Name.Pseudo)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(fn.Flags) != 1 || fn.Flags[0] != "NOSPLIT" {
|
|
|
|
|
|
t.Errorf("flags = %v, want [NOSPLIT]", fn.Flags)
|
|
|
|
|
|
}
|
|
|
|
|
|
if fn.Frame == nil || !fn.Frame.Imm.HasVal || fn.Frame.Imm.Val != 0 {
|
|
|
|
|
|
t.Errorf("frame = %+v, want $0", fn.Frame)
|
|
|
|
|
|
}
|
|
|
|
|
|
if fn.Args == nil || fn.Args.Imm.Val != 65 {
|
|
|
|
|
|
t.Errorf("args = %+v, want 65", fn.Args)
|
|
|
|
|
|
}
|
|
|
|
|
|
if fn.Doc == "" {
|
|
|
|
|
|
t.Error("expected a doc comment on the first TEXT")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The body must contain the two labels vec1 and vec1done.
|
|
|
|
|
|
labels := map[string]bool{}
|
|
|
|
|
|
for _, s := range fn.Body {
|
|
|
|
|
|
if l, ok := s.(*ast.Label); ok {
|
|
|
|
|
|
labels[l.Name.Text] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, want := range []string{"vec1", "vec1done"} {
|
|
|
|
|
|
if !labels[want] {
|
|
|
|
|
|
t.Errorf("missing label %q", want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestOperandStructure(t *testing.T) {
|
|
|
|
|
|
f := mustParse(t, "../testdata/sample_amd64.s")
|
|
|
|
|
|
fn := texts(f)[0]
|
|
|
|
|
|
|
|
|
|
|
|
// Index instructions by mnemonic for targeted checks.
|
|
|
|
|
|
byMnem := map[string]*ast.Instr{}
|
|
|
|
|
|
for _, s := range fn.Body {
|
|
|
|
|
|
if in, ok := s.(*ast.Instr); ok {
|
|
|
|
|
|
byMnem[in.Mnemonic.Text] = in
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 23:12:31 +02:00
|
|
|
|
// MOVQ swin_base+0(FP), SI; the first MOVQ in the body.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
var mov *ast.Instr
|
|
|
|
|
|
for _, s := range fn.Body {
|
|
|
|
|
|
if in, ok := s.(*ast.Instr); ok && in.Mnemonic.Text == "MOVQ" {
|
|
|
|
|
|
mov = in
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if mov == nil {
|
|
|
|
|
|
t.Fatal("MOVQ not found")
|
|
|
|
|
|
}
|
|
|
|
|
|
src := mov.Operands[0]
|
|
|
|
|
|
if src.Kind != ast.OpAddr || src.Addr.Sym == nil {
|
|
|
|
|
|
t.Fatalf("src operand = %+v, want symbol address", src)
|
|
|
|
|
|
}
|
|
|
|
|
|
if src.Addr.Sym.Name != "swin_base" || src.Addr.Sym.Pseudo != "FP" || src.Addr.Sym.Offset != 0 {
|
|
|
|
|
|
t.Errorf("src symbol = %+v, want swin_base+0(FP)", src.Addr.Sym)
|
|
|
|
|
|
}
|
|
|
|
|
|
if mov.Operands[1].Addr.Sym.Name != "SI" {
|
|
|
|
|
|
t.Errorf("dst = %+v, want SI", mov.Operands[1].Addr)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LEAQ (SI)(BX*4), R9
|
|
|
|
|
|
leaq := byMnem["LEAQ"]
|
|
|
|
|
|
if leaq == nil {
|
|
|
|
|
|
t.Fatal("LEAQ not found")
|
|
|
|
|
|
}
|
|
|
|
|
|
mem := leaq.Operands[0].Addr
|
|
|
|
|
|
if mem.Base != "SI" || mem.Index != "BX" || mem.Scale != 4 {
|
|
|
|
|
|
t.Errorf("LEAQ addr = %+v, want base SI index BX scale 4", mem)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ANDQ $-8, R10
|
|
|
|
|
|
andq := byMnem["ANDQ"]
|
|
|
|
|
|
if andq == nil {
|
|
|
|
|
|
t.Fatal("ANDQ not found")
|
|
|
|
|
|
}
|
|
|
|
|
|
imm := andq.Operands[0]
|
|
|
|
|
|
if imm.Kind != ast.OpImmediate || !imm.Imm.Neg || imm.Imm.Val != 8 {
|
|
|
|
|
|
t.Errorf("ANDQ imm = %+v, want -8", imm.Imm)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAVX512Operands(t *testing.T) {
|
|
|
|
|
|
f := mustParse(t, "../testdata/sample_amd64.s")
|
|
|
|
|
|
fn := texts(f)[1]
|
|
|
|
|
|
byMnem := map[string]*ast.Instr{}
|
|
|
|
|
|
for _, s := range fn.Body {
|
|
|
|
|
|
if in, ok := s.(*ast.Instr); ok {
|
|
|
|
|
|
byMnem[in.Mnemonic.Text] = in
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 23:12:31 +02:00
|
|
|
|
// VALIGND $15, Z9, Z0, Z1; four operands.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
val := byMnem["VALIGND"]
|
|
|
|
|
|
if val == nil {
|
|
|
|
|
|
t.Fatal("VALIGND not found")
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(val.Operands) != 4 {
|
|
|
|
|
|
t.Errorf("VALIGND operands = %d, want 4", len(val.Operands))
|
|
|
|
|
|
}
|
|
|
|
|
|
if val.Operands[0].Kind != ast.OpImmediate || val.Operands[0].Imm.Val != 15 {
|
|
|
|
|
|
t.Errorf("VALIGND first operand = %+v, want $15", val.Operands[0])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// VMOVDQU32 Z0, 4(SI)(AX*1)
|
|
|
|
|
|
vmov := byMnem["VMOVDQU32"]
|
|
|
|
|
|
if vmov == nil {
|
|
|
|
|
|
t.Fatal("VMOVDQU32 not found")
|
|
|
|
|
|
}
|
|
|
|
|
|
dst := vmov.Operands[len(vmov.Operands)-1].Addr
|
|
|
|
|
|
if dst.Offset != 4 || dst.Base != "SI" || dst.Index != "AX" || dst.Scale != 1 {
|
|
|
|
|
|
t.Errorf("VMOVDQU32 dst = %+v, want 4(SI)(AX*1)", dst)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 23:12:31 +02:00
|
|
|
|
// KTESTW K1, K1; mask registers parse as bare names.
|
2026-07-06 09:49:50 +02:00
|
|
|
|
kt := byMnem["KTESTW"]
|
|
|
|
|
|
if kt == nil || len(kt.Operands) != 2 {
|
|
|
|
|
|
t.Fatalf("KTESTW = %+v, want two operands", kt)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestDataWidthAndStatic(t *testing.T) {
|
|
|
|
|
|
f := mustParse(t, "../testdata/sample_amd64.s")
|
|
|
|
|
|
var datas []*ast.Data
|
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
|
if dd, ok := d.(*ast.Data); ok {
|
|
|
|
|
|
datas = append(datas, dd)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if datas[0].Width != 4 {
|
|
|
|
|
|
t.Errorf("first DATA width = %d, want 4", datas[0].Width)
|
|
|
|
|
|
}
|
|
|
|
|
|
if datas[0].Name.Pseudo != "SB" || datas[0].Name.Offset != 0 {
|
|
|
|
|
|
t.Errorf("first DATA name = %+v, want +0(SB)", datas[0].Name)
|
|
|
|
|
|
}
|
|
|
|
|
|
if datas[0].Value.Kind != ast.OpImmediate || datas[0].Value.Imm.Val != 1 {
|
|
|
|
|
|
t.Errorf("first DATA value = %+v, want $1", datas[0].Value)
|
|
|
|
|
|
}
|
|
|
|
|
|
// The mask24<> entries are static.
|
|
|
|
|
|
if !datas[2].Name.Static {
|
|
|
|
|
|
t.Errorf("mask24 DATA should be static, got %+v", datas[2].Name)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-14 18:22:00 +02:00
|
|
|
|
|
|
|
|
|
|
// TestTruncatedFrameDollar is a regression test for a TEXT directive whose
|
|
|
|
|
|
// frame size is missing after the $: the parser used to slice past the end
|
|
|
|
|
|
// of the token slice and panic. It must report a diagnostic instead.
|
|
|
|
|
|
func TestTruncatedFrameDollar(t *testing.T) {
|
|
|
|
|
|
for _, src := range []string{
|
|
|
|
|
|
"TEXT $\n",
|
|
|
|
|
|
"TEXT \u00b7foo(SB), $\n",
|
|
|
|
|
|
"TEXT \u00b7foo(SB), NOSPLIT, $\n",
|
|
|
|
|
|
} {
|
|
|
|
|
|
var file *ast.File
|
|
|
|
|
|
func() {
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
|
t.Fatalf("Parse(%q) panicked: %v", src, r)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
file, _ = Parse("t.s", src)
|
|
|
|
|
|
}()
|
|
|
|
|
|
if file == nil {
|
|
|
|
|
|
t.Fatalf("Parse(%q) returned no file", src)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(file.Decls) != 1 {
|
|
|
|
|
|
t.Fatalf("Parse(%q) decls = %d, want 1", src, len(file.Decls))
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
if txt.Frame != nil {
|
|
|
|
|
|
t.Errorf("Parse(%q) frame = %v, want nil", src, txt.Frame)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestFrameAndArgs parses a well-formed TEXT header and checks that the
|
|
|
|
|
|
// frame and args operands are picked up.
|
|
|
|
|
|
func TestFrameAndArgs(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7foo(SB), $32-16\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
if txt.Frame == nil || !txt.Frame.Imm.HasVal || txt.Frame.Imm.Val != 32 {
|
|
|
|
|
|
t.Errorf("frame = %+v, want $32", txt.Frame)
|
|
|
|
|
|
}
|
|
|
|
|
|
if txt.Args == nil || !txt.Args.Imm.HasVal || txt.Args.Imm.Val != 16 {
|
|
|
|
|
|
t.Errorf("args = %+v, want -16", txt.Args)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestSignedZeroFrame covers the Go runtime's "$-0-24" spelling: a zero
|
|
|
|
|
|
// frame with an explicit sign plus the argument area.
|
|
|
|
|
|
func TestSignedZeroFrame(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7foo<ABIInternal>(SB), NOSPLIT, $-0-24\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
if txt.Frame == nil || !txt.Frame.Imm.HasVal || txt.Frame.Imm.Val != 0 {
|
|
|
|
|
|
t.Errorf("frame = %+v, want $-0", txt.Frame)
|
|
|
|
|
|
}
|
|
|
|
|
|
if txt.Args == nil || !txt.Args.Imm.HasVal || txt.Args.Imm.Val != 24 {
|
|
|
|
|
|
t.Errorf("args = %+v, want -24", txt.Args)
|
|
|
|
|
|
}
|
2026-09-19 23:48:47 +02:00
|
|
|
|
// The <ABIInternal> marker belongs to the symbol: the pseudo-register is
|
|
|
|
|
|
// consumed, the marker is recorded, and neither leaks into the flags.
|
|
|
|
|
|
if txt.Name.Pseudo != "SB" {
|
|
|
|
|
|
t.Errorf("pseudo = %q, want SB", txt.Name.Pseudo)
|
|
|
|
|
|
}
|
|
|
|
|
|
if txt.Name.ABI != "ABIInternal" {
|
|
|
|
|
|
t.Errorf("ABI = %q, want ABIInternal", txt.Name.ABI)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !strings.Contains(txt.Name.Raw, "<ABIInternal>") {
|
|
|
|
|
|
t.Errorf("Raw = %q, want it to contain <ABIInternal>", txt.Name.Raw)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := []string{"NOSPLIT"}; !slices.Equal(txt.Flags, want) {
|
|
|
|
|
|
t.Errorf("flags = %v, want %v", txt.Flags, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestPipedFlags covers TEXT and GLOBL flag lists joined by '|': the bars are
|
|
|
|
|
|
// their own token kind, skipped by the flag loop, and only the identifiers
|
|
|
|
|
|
// are collected as flags.
|
|
|
|
|
|
func TestPipedFlags(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT|NOFRAME|DUPOK, $0\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
if want := []string{"NOSPLIT", "NOFRAME", "DUPOK"}; !slices.Equal(txt.Flags, want) {
|
|
|
|
|
|
t.Errorf("flags = %v, want %v", txt.Flags, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g, errs := Parse("t.s", "GLOBL \u00b7mask(SB), RODATA|NOPTR, $8\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
gl := g.Decls[0].(*ast.Globl)
|
|
|
|
|
|
if want := []string{"RODATA", "NOPTR"}; !slices.Equal(gl.Flags, want) {
|
|
|
|
|
|
t.Errorf("flags = %v, want %v", gl.Flags, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestTextMissingSymbolKeepsDecl covers a TEXT with no symbol at all: the
|
|
|
|
|
|
// decl must stay in the tree with a non-nil placeholder name, because the
|
|
|
|
|
|
// linter and the LSP dereference Name on every parsed TEXT.
|
|
|
|
|
|
func TestTextMissingSymbolKeepsDecl(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "// func f(a int) int\nTEXT $0\n\tMOVQ AX, BX\n")
|
|
|
|
|
|
if len(errs) == 0 {
|
|
|
|
|
|
t.Fatal("expected a diagnostic for the missing symbol")
|
|
|
|
|
|
}
|
|
|
|
|
|
if file == nil || len(file.Decls) != 1 {
|
|
|
|
|
|
t.Fatalf("file = %v, want the TEXT decl kept", file)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
if txt.Name == nil {
|
|
|
|
|
|
t.Fatal("Name must never be nil: downstream tools dereference it")
|
|
|
|
|
|
}
|
|
|
|
|
|
if txt.Name.Name == "" {
|
|
|
|
|
|
t.Error("placeholder name is empty")
|
|
|
|
|
|
}
|
|
|
|
|
|
if txt.Frame == nil || !txt.Frame.Imm.HasVal || txt.Frame.Imm.Val != 0 {
|
|
|
|
|
|
t.Errorf("frame = %+v, want $0", txt.Frame)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(txt.Body) != 1 {
|
|
|
|
|
|
t.Errorf("body = %d statements, want 1", len(txt.Body))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestInt64MinimumImmediate covers $-0x8000000000000000: the digits overflow
|
|
|
|
|
|
// int64 when parsed directly, but the negated magnitude is exactly the int64
|
|
|
|
|
|
// minimum and must land in Val rather than the float fallback.
|
|
|
|
|
|
func TestInt64MinimumImmediate(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7f(SB), $0\n\tMOVQ $-0x8000000000000000, AX\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
instr := txt.Body[0].(*ast.Instr)
|
|
|
|
|
|
imm := instr.Operands[0].Imm
|
|
|
|
|
|
if !imm.HasVal || imm.Val != math.MinInt64 {
|
|
|
|
|
|
t.Errorf("imm = %+v, want Val = %d with HasVal set", imm, math.MinInt64)
|
|
|
|
|
|
}
|
|
|
|
|
|
if imm.Float != "" {
|
|
|
|
|
|
t.Errorf("imm.Float = %q, want empty", imm.Float)
|
|
|
|
|
|
}
|
2026-09-14 18:22:00 +02:00
|
|
|
|
}
|
2026-09-20 19:15:35 +02:00
|
|
|
|
|
|
|
|
|
|
// TestDivisionSlashPackagePath covers the runtime's package-path spelling:
|
|
|
|
|
|
// U+2215 DIVISION SLASH separates the elements of an import path inside a
|
|
|
|
|
|
// symbol (internal∕runtime∕atomic·Xchg), and the middle dot still separates
|
|
|
|
|
|
// the package from the name. The whole spelling must reach the symbol, not
|
|
|
|
|
|
// stop at the first slash.
|
|
|
|
|
|
func TestDivisionSlashPackagePath(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7f(SB), $0\n\tCALL internal∕runtime∕atomic·Xchg(SB)\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
instr := txt.Body[0].(*ast.Instr)
|
|
|
|
|
|
sym := instr.Operands[0].Addr.Sym
|
|
|
|
|
|
if sym == nil {
|
|
|
|
|
|
t.Fatal("operand carries no symbol")
|
|
|
|
|
|
}
|
|
|
|
|
|
if sym.Pkg != "internal∕runtime∕atomic" {
|
|
|
|
|
|
t.Errorf("pkg = %q, want internal∕runtime∕atomic", sym.Pkg)
|
|
|
|
|
|
}
|
|
|
|
|
|
if sym.Name != "Xchg" {
|
|
|
|
|
|
t.Errorf("name = %q, want Xchg", sym.Name)
|
|
|
|
|
|
}
|
|
|
|
|
|
if sym.Raw != "internal∕runtime∕atomic·Xchg(SB)" {
|
|
|
|
|
|
t.Errorf("raw = %q", sym.Raw)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestSemicolonStatements covers the plain parse path: ';' separates
|
|
|
|
|
|
// statements on one line exactly as it does inside macro expansion, and a
|
|
|
|
|
|
// ';' inside a comment is comment text.
|
|
|
|
|
|
func TestSemicolonStatements(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7f(SB), $0\n\tROLQ $3, DI; ROLQ $13, DI\n\tMOVQ AX, BX // note; still comment\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
if len(txt.Body) != 4 {
|
|
|
|
|
|
t.Fatalf("body = %d statements, want 4", len(txt.Body))
|
|
|
|
|
|
}
|
|
|
|
|
|
first := txt.Body[0].(*ast.Instr)
|
|
|
|
|
|
if first.Mnemonic.Text != "ROLQ" || len(first.Operands) != 2 {
|
|
|
|
|
|
t.Errorf("first statement = %+v, want ROLQ with two operands", first.Mnemonic)
|
|
|
|
|
|
}
|
|
|
|
|
|
second := txt.Body[1].(*ast.Instr)
|
|
|
|
|
|
if second.Mnemonic.Text != "ROLQ" || len(second.Operands) != 2 {
|
|
|
|
|
|
t.Errorf("second statement = %s, want ROLQ with two operands", second.Mnemonic.Text)
|
|
|
|
|
|
}
|
|
|
|
|
|
// The trailing comment belongs to the second MOVQ, semicolon included.
|
|
|
|
|
|
third := txt.Body[2].(*ast.Instr)
|
|
|
|
|
|
if third.Mnemonic.Text != "MOVQ" || third.Comment != "note; still comment" {
|
|
|
|
|
|
t.Errorf("third = %s, comment %q", third.Mnemonic.Text, third.Comment)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestSemicolonAfterLabel covers a label sharing its line with two
|
|
|
|
|
|
// statements.
|
|
|
|
|
|
func TestSemicolonAfterLabel(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7f(SB), $0\nloop: NOP; NOP\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse errors: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
txt := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
if len(txt.Body) != 4 {
|
|
|
|
|
|
t.Fatalf("body = %d statements, want 4 (label, two instructions, RET)", len(txt.Body))
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := txt.Body[0].(*ast.Label); !ok {
|
|
|
|
|
|
t.Errorf("first statement = %T, want *ast.Label", txt.Body[0])
|
|
|
|
|
|
}
|
|
|
|
|
|
for i, want := range []string{"NOP", "NOP", "RET"} {
|
|
|
|
|
|
in, ok := txt.Body[i+1].(*ast.Instr)
|
|
|
|
|
|
if !ok || in.Mnemonic.Text != want {
|
|
|
|
|
|
t.Errorf("statement %d = %v, want %s", i+1, txt.Body[i+1], want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestParseEqualsZeroOptions pins the contract that ParseWithOptions with
|
|
|
|
|
|
// the zero Options reproduces Parse, here for the semicolon split.
|
|
|
|
|
|
func TestParseEqualsZeroOptions(t *testing.T) {
|
|
|
|
|
|
src := "TEXT \u00b7f(SB), $0\n\tNOP; NOP\n\tRET\n"
|
|
|
|
|
|
a, errsA := Parse("t.s", src)
|
|
|
|
|
|
b, errsB := ParseWithOptions("t.s", src, Options{})
|
|
|
|
|
|
if len(errsA) > 0 || len(errsB) > 0 {
|
|
|
|
|
|
t.Fatalf("errors: %v / %v", errsA, errsB)
|
|
|
|
|
|
}
|
|
|
|
|
|
ta, tb := texts(a), texts(b)
|
|
|
|
|
|
if len(ta) != len(tb) {
|
|
|
|
|
|
t.Fatalf("decl counts differ: %d vs %d", len(ta), len(tb))
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := range ta {
|
|
|
|
|
|
if len(ta[i].Body) != len(tb[i].Body) {
|
|
|
|
|
|
t.Fatalf("TEXT %d: body lengths differ: %d vs %d", i, len(ta[i].Body), len(tb[i].Body))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-20 22:02:19 +02:00
|
|
|
|
|
|
|
|
|
|
// 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])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-21 00:44:47 +02:00
|
|
|
|
|
|
|
|
|
|
// TestSignedParenDisplacement pins a sign before a parenthesised
|
|
|
|
|
|
// displacement expression: -(24+8)(X6) negates the folded value and keeps
|
|
|
|
|
|
// the base group, the shape GOROOT's riscv64 and loong64 files use.
|
|
|
|
|
|
func TestSignedParenDisplacement(t *testing.T) {
|
|
|
|
|
|
file, errs := Parse("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0-0\n\tMOV X7, -(24+8)(X6)\n\tMOV X7, +(16)(X6)\n\tRET\n")
|
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
|
}
|
|
|
|
|
|
text := file.Decls[0].(*ast.Text)
|
|
|
|
|
|
ins := text.Body[0].(*ast.Instr)
|
|
|
|
|
|
op := ins.Operands[1] // Plan 9 order: the destination address is last
|
|
|
|
|
|
if !op.Addr.HasOff || op.Addr.Offset != -32 {
|
|
|
|
|
|
t.Errorf("-(24+8): offset = %v hasOff=%v, want -32 true", op.Addr.Offset, op.Addr.HasOff)
|
|
|
|
|
|
}
|
|
|
|
|
|
if op.Addr.Base != "X6" {
|
|
|
|
|
|
t.Errorf("-(24+8): base = %q, want X6", op.Addr.Base)
|
|
|
|
|
|
}
|
|
|
|
|
|
ins = text.Body[1].(*ast.Instr)
|
|
|
|
|
|
op = ins.Operands[1]
|
|
|
|
|
|
if !op.Addr.HasOff || op.Addr.Offset != 16 || op.Addr.Base != "X6" {
|
|
|
|
|
|
t.Errorf("+(16): offset = %v hasOff=%v base=%q, want 16 true X6", op.Addr.Offset, op.Addr.HasOff, op.Addr.Base)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|