feat(parser): bracket register ranges, index-only VSIB and bare trailing immediates
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -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 {
|
||||
|
||||
+10
@@ -158,4 +158,14 @@ type Address struct {
|
||||
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
|
||||
}
|
||||
|
||||
+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])
|
||||
}
|
||||
}
|
||||
|
||||
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