diff --git a/asm/assemble.go b/asm/assemble.go index 8891665..6def8ea 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -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 { diff --git a/ast/ast.go b/ast/ast.go index 2861f2c..f6c3112 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -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 } diff --git a/parser/parser.go b/parser/parser.go index 1e77d5f..8771a3e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -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 diff --git a/parser/parser_test.go b/parser/parser_test.go index 501239b..4c18551 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -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]) + } +} diff --git a/testdata/verify/vsib_amd64.s b/testdata/verify/vsib_amd64.s new file mode 100644 index 0000000..bfb296f --- /dev/null +++ b/testdata/verify/vsib_amd64.s @@ -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 diff --git a/verify/groundtruth_test.go b/verify/groundtruth_test.go index 6816eee..7112f56 100644 --- a/verify/groundtruth_test.go +++ b/verify/groundtruth_test.go @@ -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",