Compare commits

..
3 Commits
Author SHA1 Message Date
petrbalvin 0f3146ff2c feat(asm): add EVEX masking, zeroing and the AVX-512 F/BW integer set
Assisted-by: Qwen 3.8 Max Preview
2026-07-14 21:03:26 +02:00
petrbalvin 9370f9c3ee feat(cli): standard --help and --version with per-command usage
Assisted-by: Qwen 3.8 Max Preview
2026-07-13 19:50:38 +02:00
petrbalvin e98680597d feat(fmt): go-fmt-style recursive formatting and canonical blank-line layout
Assisted-by: Qwen 3.8 Max Preview
2026-07-12 21:24:41 +02:00
12 changed files with 734 additions and 89 deletions
+10
View File
@@ -156,6 +156,16 @@ func (t *Table) Lookup(mnemonic string) (Instr, bool) {
} }
} }
} }
// amd64 EVEX instructions take a .Z zeroing suffix (masking is written as
// an explicit K operand rather than a suffix); strip it so the base
// instruction is still recognised.
if t.Arch == AMD64 {
if base, ok := strings.CutSuffix(key, ".Z"); ok {
if in, found := t.instrs[base]; found {
return in, true
}
}
}
return Instr{}, false return Instr{}, false
} }
+18 -5
View File
@@ -51,9 +51,16 @@ func (e *enc) encode(mnem string, ops []Operand) error {
// VEX (AVX/AVX2) and EVEX (AVX-512) instructions: the trailing // VEX (AVX/AVX2) and EVEX (AVX-512) instructions: the trailing
// B/W/L/Q/D is part of the mnemonic, not a size suffix, so dispatch // B/W/L/Q/D is part of the mnemonic, not a size suffix, so dispatch
// before splitSize. // before splitSize. A ".Z" suffix requests EVEX zeroing.
if isVex(upper) || isEvex(upper) || upper == "KMOVW" { base, zeroing, err := stripEvexSuffix(upper)
return e.encodeVec(upper, ops) if err != nil {
return err
}
if isVex(base) || isEvex(base) || base == "KMOVW" {
return e.encodeVec(base, ops, zeroing)
}
if zeroing {
return fmt.Errorf("%s: the .Z suffix requires an EVEX instruction", mnem)
} }
// CMOVcc and SETcc carry the condition in the mnemonic (CMOVLGT, SETNE). // CMOVcc and SETcc carry the condition in the mnemonic (CMOVLGT, SETNE).
@@ -121,14 +128,20 @@ func splitSize(upper string) (base string, size int) {
// its own direction-dependent opcodes; KTESTW is always VEX; everything else // its own direction-dependent opcodes; KTESTW is always VEX; everything else
// takes EVEX when an operand demands it (a ZMM or K register, or an // takes EVEX when an operand demands it (a ZMM or K register, or an
// EVEX-only mnemonic) and VEX otherwise. // EVEX-only mnemonic) and VEX otherwise.
func (e *enc) encodeVec(upper string, ops []Operand) error { func (e *enc) encodeVec(upper string, ops []Operand, zeroing bool) error {
if upper == "KMOVW" { if upper == "KMOVW" {
if zeroing {
return fmt.Errorf("KMOVW takes no .Z suffix")
}
return e.encodeKmovw(ops) return e.encodeKmovw(ops)
} }
if upper == "KTESTW" || !evexRequired(upper, ops) { if upper == "KTESTW" || !evexRequired(upper, ops) {
if zeroing {
return fmt.Errorf("%s: the .Z suffix requires an EVEX instruction", upper)
}
return e.encodeVex(upper, ops) return e.encodeVex(upper, ops)
} }
return e.encodeEvex(upper, ops) return e.encodeEvex(upper, ops, zeroing)
} }
// --- instruction components ------------------------------------------------- // --- instruction components -------------------------------------------------
+186 -31
View File
@@ -3,7 +3,10 @@
package asm package asm
import "fmt" import (
"fmt"
"strings"
)
// This file implements EVEX (AVX-512) instruction encoding: the four-byte // This file implements EVEX (AVX-512) instruction encoding: the four-byte
// EVEX prefix with 5-bit vector register fields (Z0–Z31, X/Y 16–31), the // EVEX prefix with 5-bit vector register fields (Z0–Z31, X/Y 16–31), the
@@ -74,6 +77,48 @@ var evexTable = map[string]evexSpec{
"VPMULLQ": {2, 0x40, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}}, "VPMULLQ": {2, 0x40, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3, [3]int{0, 32, 64}}, "VPERMD": {2, 0x36, 0, 1, -1, vexNDS3, [3]int{0, 32, 64}},
// EVEX.128/256/512 — the wider integer set (AVX-512 F/BW): byte/word
// arithmetic, the bitwise ops with D/Q suffixes, min/max, averages and
// variable shifts. All NDS form; W distinguishes element size.
"VPADDB": {1, 0xFC, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPADDW": {1, 0xFD, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSUBB": {1, 0xF8, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSUBW": {1, 0xF9, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMULLW": {1, 0xD5, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPAVGB": {1, 0xE0, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPAVGW": {1, 0xE3, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINUB": {1, 0xDA, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXUB": {1, 0xDE, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINSW": {1, 0xEA, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXSW": {1, 0xEE, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPANDD": {1, 0xDB, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPANDQ": {1, 0xDB, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPANDND": {1, 0xDF, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPANDNQ": {1, 0xDF, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINSB": {2, 0x38, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXSB": {2, 0x3C, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINSQ": {2, 0x39, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXSQ": {2, 0x3D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINUW": {2, 0x3A, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXUW": {2, 0x3E, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINSD": {2, 0x39, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXSD": {2, 0x3D, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINUD": {2, 0x3B, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXUD": {2, 0x3F, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMINUQ": {2, 0x3B, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPMAXUQ": {2, 0x3F, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSLLVD": {2, 0x47, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSLLVQ": {2, 0x47, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSRLVD": {2, 0x45, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSRLVQ": {2, 0x45, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSRAVD": {2, 0x46, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
"VPSRAVQ": {2, 0x46, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
// EVEX forms of instructions that also exist in VEX (selected when a ZMM
// or K register, or indices 16–31, demand EVEX).
"VPSHUFD": {1, 0x70, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
"VPSHUFB": {2, 0x00, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
// EVEX.66.0F — immediate shift (VPSLLD /6). // EVEX.66.0F — immediate shift (VPSLLD /6).
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm, [3]int{16, 32, 64}}, "VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
@@ -117,6 +162,13 @@ var evexMoveTable = map[string]evexMoveSpec{
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}}, "VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
// EVEX.128/256/512.F3.0F.W1 — unaligned qword move. // EVEX.128/256/512.F3.0F.W1 — unaligned qword move.
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}}, "VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
// EVEX.128/256/512.F2.0F.W0 — unaligned byte move (byte/word moves use the
// F2 prefix, dword/qword moves F3; the element size only changes the tuple
// semantics).
"VMOVDQU8": {1, 3, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
// EVEX.128/256/512.F2.0F.W1 — unaligned word move (shares the qword
// encoding).
"VMOVDQU16": {1, 3, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
// EVEX.128/256/512.66.0F.W1 — unaligned packed double move. // EVEX.128/256/512.66.0F.W1 — unaligned packed double move.
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}}, "VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}},
} }
@@ -151,13 +203,75 @@ func evexRequired(upper string, ops []Operand) bool {
return false return false
} }
// encodeEvex encodes an EVEX instruction with operands in Plan 9 order. // stripEvexSuffix splits a ".Z" zeroing suffix off the mnemonic. It is the
func (e *enc) encodeEvex(mnemUpper string, ops []Operand) error { // only EVEX suffix supported; Go writes masking as an explicit K operand, not
// a suffix.
func stripEvexSuffix(mnem string) (base string, zeroing bool, err error) {
i := strings.LastIndexByte(mnem, '.')
if i < 0 {
return mnem, false, nil
}
if mnem[i+1:] == "Z" {
return mnem[:i], true, nil
}
return "", false, fmt.Errorf("unsupported EVEX suffix %q", mnem[i+1:])
}
// splitMask extracts an explicit mask register (K1–K7) from the operand list,
// returning the remaining operands and the mask index. K0 is not a usable
// mask (aaa = 0 means "no mask"), matching the assembler.
func splitMask(ops []Operand) ([]Operand, int, error) {
var rest []Operand
mask := 0
for _, op := range ops {
if r, ok := op.(Reg); ok && r.mask {
if mask != 0 {
return nil, 0, fmt.Errorf("at most one mask register operand")
}
if r.idx == 0 {
return nil, 0, fmt.Errorf("K0 is not a usable mask register")
}
mask = r.idx
continue
}
rest = append(rest, op)
}
return rest, mask, nil
}
// encodeEvex encodes an EVEX instruction with operands in Plan 9 order. The
// mask, when present, is an explicit K1–K7 operand anywhere among the
// operands; zeroing comes from the .Z mnemonic suffix and requires a mask.
func (e *enc) encodeEvex(mnemUpper string, ops []Operand, zeroing bool) error {
// Mask-destination comparisons (VPCMPEQD …, K1): the last operand is the
// destination K register, and any mask sits among the preceding operands.
if spec, ok := evexTable[mnemUpper]; ok && spec.form == vexNDS3 && len(ops) > 0 {
if dst, ok := ops[len(ops)-1].(Reg); ok && dst.mask {
rest, mask, err := splitMask(ops[:len(ops)-1])
if err != nil {
return err
}
if zeroing && mask == 0 {
return fmt.Errorf("%s: zeroing (.Z) requires a mask register", mnemUpper)
}
return e.encodeEvexNDS3(spec, append(rest, dst), mask, zeroing)
}
}
rest, mask, err := splitMask(ops)
if err != nil {
return err
}
if zeroing && mask == 0 {
return fmt.Errorf("%s: zeroing (.Z) requires a mask register", mnemUpper)
}
ops = rest
if bs, ok := evexBcastTable[mnemUpper]; ok { if bs, ok := evexBcastTable[mnemUpper]; ok {
return e.encodeEvexBcast(bs, ops) return e.encodeEvexBcast(bs, ops, mask, zeroing)
} }
if ms, ok := evexMoveTable[mnemUpper]; ok { if ms, ok := evexMoveTable[mnemUpper]; ok {
return e.encodeEvexMove(mnemUpper, ms, ops) return e.encodeEvexMove(mnemUpper, ms, ops, mask, zeroing)
} }
spec, ok := evexTable[mnemUpper] spec, ok := evexTable[mnemUpper]
if !ok { if !ok {
@@ -165,17 +279,19 @@ func (e *enc) encodeEvex(mnemUpper string, ops []Operand) error {
} }
switch spec.form { switch spec.form {
case vexNDS3: case vexNDS3:
return e.encodeEvexNDS3(spec, ops) return e.encodeEvexNDS3(spec, ops, mask, zeroing)
case vexRM: case vexRM:
return e.encodeEvexRM(spec, ops) return e.encodeEvexRM(spec, ops, mask, zeroing)
case vexRMRev: case vexRMRev:
return e.encodeEvexRMRev(spec, ops) return e.encodeEvexRMRev(spec, ops, mask, zeroing)
case vexImmRM:
return e.encodeEvexImmRM(spec, ops, mask, zeroing)
case vexShiftImm: case vexShiftImm:
return e.encodeEvexShiftImm(spec, ops) return e.encodeEvexShiftImm(spec, ops, mask, zeroing)
case vexNDS3Imm: case vexNDS3Imm:
return e.encodeEvexNDS3Imm(spec, ops) return e.encodeEvexNDS3Imm(spec, ops, mask, zeroing)
case vexExtract: case vexExtract:
return e.encodeEvexExtract(spec, ops) return e.encodeEvexExtract(spec, ops, mask, zeroing)
} }
return fmt.Errorf("unhandled EVEX form for %s", mnemUpper) return fmt.Errorf("unhandled EVEX form for %s", mnemUpper)
} }
@@ -183,7 +299,7 @@ func (e *enc) encodeEvex(mnemUpper string, ops []Operand) error {
// encodeEvexNDS3 encodes the three-operand NDS form: OP src2, src1, dst. The // encodeEvexNDS3 encodes the three-operand NDS form: OP src2, src1, dst. The
// destination may be an opmask register (VPCMPEQD), in which case the vector // destination may be an opmask register (VPCMPEQD), in which case the vector
// length comes from the sources. // length comes from the sources.
func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand) error { func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 3 { if len(ops) != 3 {
return fmt.Errorf("EVEX NDS instruction expects 3 operands, got %d", len(ops)) return fmt.Errorf("EVEX NDS instruction expects 3 operands, got %d", len(ops))
} }
@@ -203,12 +319,12 @@ func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand) error {
ll = r.vecLenBit() ll = r.vecLenBit()
} }
} }
return e.emitEvexFields(spec, ll, dstReg.idx, vvvvReg.idx, src2) return e.emitEvexFields(spec, ll, dstReg.idx, vvvvReg.idx, src2, mask, zeroing)
} }
// encodeEvexRM encodes the two-operand form: OP src, dst (reg=dst, rm=src, // encodeEvexRM encodes the two-operand form: OP src, dst (reg=dst, rm=src,
// no vvvv), e.g. VCVTQQ2PD. // no vvvv), e.g. VCVTQQ2PD.
func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand) error { func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 2 { if len(ops) != 2 {
return fmt.Errorf("EVEX two-operand instruction expects 2 operands, got %d", len(ops)) return fmt.Errorf("EVEX two-operand instruction expects 2 operands, got %d", len(ops))
} }
@@ -217,12 +333,42 @@ func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand) error {
if !ok || !dstReg.isVec() { if !ok || !dstReg.isVec() {
return fmt.Errorf("EVEX destination must be a vector register") return fmt.Errorf("EVEX destination must be a vector register")
} }
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src) return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src, mask, zeroing)
}
// encodeEvexImmRM encodes the immediate shuffle form: OP $imm, src, dst
// (reg = dst, rm = src, imm8), e.g. VPSHUFD.
func (e *enc) encodeEvexImmRM(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 3 {
return fmt.Errorf("shuffle expects 3 operands ($imm, src, dst), got %d", len(ops))
}
imm, src, dst := ops[0], ops[1], ops[2]
immVal, ok := imm.(Imm)
if !ok {
return fmt.Errorf("shuffle control must be an immediate")
}
dstReg, ok := dst.(Reg)
if !ok || !dstReg.isVec() {
return fmt.Errorf("shuffle destination must be a vector register")
}
ll := dstReg.vecLenBit()
if r, ok := src.(Reg); ok && r.isVec() {
ll = r.vecLenBit()
}
immByte, err := imm8(int64(immVal))
if err != nil {
return err
}
if err := e.emitEvexFields(spec, ll, dstReg.idx, -1, src, mask, zeroing); err != nil {
return err
}
e.out = append(e.out, immByte)
return nil
} }
// encodeEvexShiftImm encodes an immediate shift: OP $imm, src, dst // encodeEvexShiftImm encodes an immediate shift: OP $imm, src, dst
// (ModRM.reg = /digit, vvvv = dst, rm = src, imm8), e.g. VPSRAD $31, Z3, Z5. // (ModRM.reg = /digit, vvvv = dst, rm = src, imm8), e.g. VPSRAD $31, Z3, Z5.
func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand) error { func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 3 { if len(ops) != 3 {
return fmt.Errorf("EVEX shift expects 3 operands ($imm, src, dst), got %d", len(ops)) return fmt.Errorf("EVEX shift expects 3 operands ($imm, src, dst), got %d", len(ops))
} }
@@ -243,7 +389,7 @@ func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand) error {
if err != nil { if err != nil {
return err return err
} }
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), spec.opdigit, dstReg.idx, srcReg); err != nil { if err := e.emitEvexFields(spec, dstReg.vecLenBit(), spec.opdigit, dstReg.idx, srcReg, mask, zeroing); err != nil {
return err return err
} }
e.out = append(e.out, immByte) e.out = append(e.out, immByte)
@@ -252,7 +398,7 @@ func (e *enc) encodeEvexShiftImm(spec evexSpec, ops []Operand) error {
// encodeEvexNDS3Imm encodes OP $imm, src2, src1, dst (reg=dst, vvvv=src1, // encodeEvexNDS3Imm encodes OP $imm, src2, src1, dst (reg=dst, vvvv=src1,
// rm=src2, imm8), e.g. VALIGND. // rm=src2, imm8), e.g. VALIGND.
func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand) error { func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 4 { if len(ops) != 4 {
return fmt.Errorf("instruction expects 4 operands ($imm, src2, src1, dst), got %d", len(ops)) return fmt.Errorf("instruction expects 4 operands ($imm, src2, src1, dst), got %d", len(ops))
} }
@@ -273,7 +419,7 @@ func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand) error {
if err != nil { if err != nil {
return err return err
} }
if err := e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, vvvvReg.idx, src2); err != nil { if err := e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, vvvvReg.idx, src2, mask, zeroing); err != nil {
return err return err
} }
e.out = append(e.out, immByte) e.out = append(e.out, immByte)
@@ -282,7 +428,7 @@ func (e *enc) encodeEvexNDS3Imm(spec evexSpec, ops []Operand) error {
// encodeEvexExtract encodes OP $imm, zsrc, ydst (reg=ZMM source, rm=YMM/memory // encodeEvexExtract encodes OP $imm, zsrc, ydst (reg=ZMM source, rm=YMM/memory
// destination, imm8), e.g. VEXTRACTI64X4. // destination, imm8), e.g. VEXTRACTI64X4.
func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand) error { func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 3 { if len(ops) != 3 {
return fmt.Errorf("extract expects 3 operands ($imm, zsrc, ydst), got %d", len(ops)) return fmt.Errorf("extract expects 3 operands ($imm, zsrc, ydst), got %d", len(ops))
} }
@@ -299,7 +445,7 @@ func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand) error {
if err != nil { if err != nil {
return err return err
} }
if err := e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst); err != nil { if err := e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst, mask, zeroing); err != nil {
return err return err
} }
e.out = append(e.out, immByte) e.out = append(e.out, immByte)
@@ -309,7 +455,7 @@ func (e *enc) encodeEvexExtract(spec evexSpec, ops []Operand) error {
// encodeEvexMove encodes a two-operand EVEX move; a vector→vector move uses // encodeEvexMove encodes a two-operand EVEX move; a vector→vector move uses
// the store-form opcode (reg = source, rm = destination), matching the Go // the store-form opcode (reg = source, rm = destination), matching the Go
// assembler. // assembler.
func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand) error { func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 2 { if len(ops) != 2 {
return fmt.Errorf("EVEX move expects 2 operands, got %d", len(ops)) return fmt.Errorf("EVEX move expects 2 operands, got %d", len(ops))
} }
@@ -338,7 +484,7 @@ func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand) error
return fmt.Errorf("%s needs a vector register operand", mnem) return fmt.Errorf("%s needs a vector register operand", mnem)
} }
spec := evexSpec{mapSel: ms.mapSel, opcode: op, w: ms.w, pp: ms.pp, opdigit: -1, n: ms.n} spec := evexSpec{mapSel: ms.mapSel, opcode: op, w: ms.w, pp: ms.pp, opdigit: -1, n: ms.n}
return e.emitEvexFields(spec, reg.vecLenBit(), reg.idx, -1, rm) return e.emitEvexFields(spec, reg.vecLenBit(), reg.idx, -1, rm, mask, zeroing)
} }
// memOperand reports whether op is a memory reference (including a // memOperand reports whether op is a memory reference (including a
@@ -353,7 +499,7 @@ func memOperand(op Operand) bool {
// encodeEvexRMRev encodes the narrowing-store form: OP src, dst with the wide // encodeEvexRMRev encodes the narrowing-store form: OP src, dst with the wide
// source in the reg field and the narrow destination in r/m (VPMOVDW/QD). // source in the reg field and the narrow destination in r/m (VPMOVDW/QD).
func (e *enc) encodeEvexRMRev(spec evexSpec, ops []Operand) error { func (e *enc) encodeEvexRMRev(spec evexSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 2 { if len(ops) != 2 {
return fmt.Errorf("EVEX store instruction expects 2 operands, got %d", len(ops)) return fmt.Errorf("EVEX store instruction expects 2 operands, got %d", len(ops))
} }
@@ -362,12 +508,12 @@ func (e *enc) encodeEvexRMRev(spec evexSpec, ops []Operand) error {
if !ok || !srcReg.isVec() { if !ok || !srcReg.isVec() {
return fmt.Errorf("EVEX source must be a vector register") return fmt.Errorf("EVEX source must be a vector register")
} }
return e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst) return e.emitEvexFields(spec, srcReg.vecLenBit(), srcReg.idx, -1, dst, mask, zeroing)
} }
// encodeEvexBcast encodes VPBROADCASTD/Q: OP src, dst with the GPR or memory // encodeEvexBcast encodes VPBROADCASTD/Q: OP src, dst with the GPR or memory
// source broadcast to every lane of the vector destination. // source broadcast to every lane of the vector destination.
func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand) error { func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand, mask int, zeroing bool) error {
if len(ops) != 2 { if len(ops) != 2 {
return fmt.Errorf("broadcast expects 2 operands, got %d", len(ops)) return fmt.Errorf("broadcast expects 2 operands, got %d", len(ops))
} }
@@ -386,14 +532,15 @@ func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand) error {
default: default:
return fmt.Errorf("broadcast source must be a register or memory") return fmt.Errorf("broadcast source must be a register or memory")
} }
return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src) return e.emitEvexFields(spec, dstReg.vecLenBit(), dstReg.idx, -1, src, mask, zeroing)
} }
// emitEvexFields emits the EVEX prefix, opcode, ModR/M, SIB and displacement // emitEvexFields emits the EVEX prefix, opcode, ModR/M, SIB and displacement
// (disp8×N compressed) for the given precomputed fields. regIdx is the // (disp8×N compressed) for the given precomputed fields. regIdx is the
// unextended reg-field register index, or a /digit (0–7); vvvvIdx is the // unextended reg-field register index, or a /digit (0–7); vvvvIdx is the
// vvvv register index, or -1 when unused. // vvvv register index, or -1 when unused. mask (K1–K7, 0 = unmasked) and
func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand) error { // zeroing fill the aaa and z bits of the P2 byte.
func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand, mask int, zeroing bool) error {
if ll > 2 { if ll > 2 {
return fmt.Errorf("invalid vector length") return fmt.Errorf("invalid vector length")
} }
@@ -420,7 +567,8 @@ func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand)
var sb *sbRef var sb *sbRef
switch r := rm.(type) { switch r := rm.(type) {
case Reg: case Reg:
// ModRM.mod = 11: rm[3] extends via B̄, rm[4] via X̄. // ModRM.mod = 11: rm[3] extends via B̄, and rm[4] via X̄ (the EVEX
// register-register quirk).
modrm = 0xC0 | (regIdx&7)<<3 | (r.idx & 7) modrm = 0xC0 | (regIdx&7)<<3 | (r.idx & 7)
sib = -1 sib = -1
if r.idx&8 != 0 { if r.idx&8 != 0 {
@@ -429,6 +577,9 @@ func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand)
if r.idx&16 != 0 { if r.idx&16 != 0 {
xBar = 0 xBar = 0
} }
if r.idx&16 != 0 {
xBar = 0
}
case Mem: case Mem:
var err error var err error
modrm, sib, disp, xBar, bBar, err = memComponentsEvex(regIdx&7, r, spec.n[ll]) modrm, sib, disp, xBar, bBar, err = memComponentsEvex(regIdx&7, r, spec.n[ll])
@@ -451,9 +602,13 @@ func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand)
return fmt.Errorf("invalid EVEX r/m operand") return fmt.Errorf("invalid EVEX r/m operand")
} }
z := 0
if zeroing {
z = 1
}
p0 := byte(rBar<<7 | xBar<<6 | bBar<<5 | rPrimeBar<<4 | spec.mapSel) p0 := byte(rBar<<7 | xBar<<6 | bBar<<5 | rPrimeBar<<4 | spec.mapSel)
p1 := byte(spec.w<<7 | vBar<<3 | 1<<2 | spec.pp) p1 := byte(spec.w<<7 | vBar<<3 | 1<<2 | spec.pp)
p2 := byte(ll<<5 | vPrimeBar<<3) // z = 0, b = 0, aaa = 0 p2 := byte(z<<7 | ll<<5 | vPrimeBar<<3 | mask) // z, L'L, b=0, V', aaa
e.out = append(e.out, 0x62, p0, p1, p2, spec.opcode, byte(modrm)) e.out = append(e.out, 0x62, p0, p1, p2, spec.opcode, byte(modrm))
if sib >= 0 { if sib >= 0 {
e.out = append(e.out, byte(sib)) e.out = append(e.out, byte(sib))
+93 -1
View File
@@ -65,6 +65,23 @@ func TestEvexGroundTruth(t *testing.T) {
{"VMOVDQU64 (SI)(R15*4),Z3", "VMOVDQU64", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b1fe486f1cbe"}, {"VMOVDQU64 (SI)(R15*4),Z3", "VMOVDQU64", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b1fe486f1cbe"},
{"VMOVDQU64 Z0,4(SI)(AX*1)", "VMOVDQU64", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f1fe487f840604000000"}, {"VMOVDQU64 Z0,4(SI)(AX*1)", "VMOVDQU64", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f1fe487f840604000000"},
{"VMOVDQU64 Z1,Z2", "VMOVDQU64", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487fca"}, {"VMOVDQU64 Z1,Z2", "VMOVDQU64", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487fca"},
// The wider AVX-512 F/BW integer set.
{"VPADDB Z1,Z2,Z3", "VPADDB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48fcd9"},
{"VPSUBW Z1,Z2,Z3", "VPSUBW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48f9d9"},
{"VPANDQ Z1,Z2,Z3", "VPANDQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed48dbd9"},
{"VPANDND Z1,Z2,Z3", "VPANDND", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48dfd9"},
{"VPMULLW Z1,Z2,Z3", "VPMULLW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48d5d9"},
{"VPMINUB Z1,Z2,Z3", "VPMINUB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48dad9"},
{"VPMAXUQ Z1,Z2,Z3", "VPMAXUQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f2ed483fd9"},
{"VPAVGW Z1,Z2,Z3", "VPAVGW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48e3d9"},
{"VPSLLVQ Z3,Z1,Z2", "VPSLLVQ", []Operand{vreg(t, "Z3"), vreg(t, "Z1"), vreg(t, "Z2")}, "62f2f54847d3"},
{"VPSRAVQ Z3,Z1,Z2", "VPSRAVQ", []Operand{vreg(t, "Z3"), vreg(t, "Z1"), vreg(t, "Z2")}, "62f2f54846d3"},
{"VPSHUFD $0x1B,Z1,Z2", "VPSHUFD", []Operand{Imm(0x1B), vreg(t, "Z1"), vreg(t, "Z2")}, "62f17d4870d11b"},
{"VPSHUFB Z1,Z2,Z3", "VPSHUFB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d4800d9"},
{"VMOVDQU8 Z1,Z2", "VMOVDQU8", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17f487fca"},
{"VMOVDQU16 Z1,Z2", "VMOVDQU16", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1ff487fca"},
// Indices 16–31: rm[4] rides in X̄ for register operands.
{"VPSHUFD $1,X16,X17", "VPSHUFD", []Operand{Imm(1), vreg(t, "X16"), vreg(t, "X17")}, "62a17d0870c801"},
{"VMOVUPD (DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 0, 64), vreg(t, "Z14")}, "6271fd481037"}, {"VMOVUPD (DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 0, 64), vreg(t, "Z14")}, "6271fd481037"},
{"VMOVUPD 64(DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 64, 64), vreg(t, "Z14")}, "6271fd48107701"}, {"VMOVUPD 64(DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 64, 64), vreg(t, "Z14")}, "6271fd48107701"},
// Conversions and narrowing stores (reg = wide source). // Conversions and narrowing stores (reg = wide source).
@@ -110,6 +127,81 @@ func TestEvexGroundTruth(t *testing.T) {
} }
} }
// TestEvexMasking checks the AVX-512 mask operand (K1–K7, placed freely among
// the operands) and the .Z zeroing suffix, byte for byte against the Go
// assembler.
func TestEvexMasking(t *testing.T) {
cases := []struct {
name string
mnem string
ops []Operand
want string
}{
// Masked arithmetic: K anywhere among the operands; .Z sets the z bit.
{"VPADDD.Z merging+zeroing", "VPADDD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K2"), vreg(t, "Z3")}, "62f16dcafed9"},
{"VPADDD merging", "VPADDD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f16d49fed9"},
{"VADDPD.Z", "VADDPD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K2"), vreg(t, "Z3")}, "62f1edca58d9"},
{"VPMINSD.Z", "VPMINSD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K5"), vreg(t, "Z3")}, "62f26dcd39d9"},
{"VPMINSQ.Z", "VPMINSQ.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K5"), vreg(t, "Z3")}, "62f2edcd39d9"},
// Masked immediate shift (K before the destination).
{"VPSRAD.Z", "VPSRAD.Z", []Operand{Imm(1), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f165c972e201"},
{"VPSLLD merge", "VPSLLD", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Z3")}, "62f1654a72f104"},
// Masked align.
{"VALIGND", "VALIGND", []Operand{Imm(12), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3"), vreg(t, "Z4")}, "62f36d4b03e10c"},
// Masked conversion and extract.
{"VCVTQQ2PD.Z", "VCVTQQ2PD.Z", []Operand{vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Z3")}, "62f1fecae6d9"},
{"VEXTRACTI64X4", "VEXTRACTI64X4", []Operand{Imm(1), vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Y3")}, "62f3fd4a3bcb01"},
// Masked moves: K sits between the register and memory operands.
{"VMOVDQU8 store", "VMOVDQU8", []Operand{vreg(t, "Z1"), vreg(t, "K3"), Ptr(SI, 0, 64)}, "62f17f4b7f0e"},
{"VMOVDQU32 load", "VMOVDQU32", []Operand{Ptr(SI, 0, 64), vreg(t, "K4"), vreg(t, "Z1")}, "62f17e4c6f0e"},
{"VMOVDQU32 store", "VMOVDQU32", []Operand{vreg(t, "Z1"), vreg(t, "K4"), Ptr(DI, 0, 64)}, "62f17e4c7f0f"},
// Masked comparison with a K destination: dst K1, mask K2.
{"VPCMPEQD k-dst+mask", "VPCMPEQD", []Operand{vreg(t, "Z0"), vreg(t, "Z3"), vreg(t, "K2"), vreg(t, "K1")}, "62f1654a76c8"},
}
for _, c := range cases {
code, err := Encode(c.mnem, c.ops...)
if err != nil {
t.Errorf("%s: Encode: %v", c.name, err)
continue
}
if got := hexCompact(code); got != c.want {
t.Errorf("%s: bytes %s, want %s", c.name, got, c.want)
continue
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Errorf("%s: Decode(%x): %v", c.name, code, err)
continue
}
want := c.mnem
if i := len(want) - 2; i > 0 && want[i:] == ".Z" {
want = want[:i]
}
if inst.Op.String() != want {
t.Errorf("%s: decoded as %s", c.name, inst.Op.String())
}
}
// Error cases.
bad := []struct {
name string
mnem string
ops []Operand
}{
{"zeroing without mask", "VPADDD.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
{"K0 mask", "VPADDD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K0"), vreg(t, "Z3")}},
{"two masks", "VPADDD", []Operand{vreg(t, "Z1"), vreg(t, "K1"), vreg(t, "K2"), vreg(t, "Z3")}},
{".Z on VEX-only", "VPSHUFD.Z", []Operand{Imm(1), vreg(t, "X0"), vreg(t, "X1")}},
{"unsupported suffix", "VPADDD.BCST", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
{"KMOVW.Z", "KMOVW.Z", []Operand{vreg(t, "K1"), vreg(t, "K2")}},
}
for _, c := range bad {
if _, err := Encode(c.mnem, c.ops...); err == nil {
t.Errorf("%s: expected an error, got none", c.name)
}
}
}
// TestEvexErrors checks the EVEX-specific error paths. // TestEvexErrors checks the EVEX-specific error paths.
func TestEvexErrors(t *testing.T) { func TestEvexErrors(t *testing.T) {
cases := []struct { cases := []struct {
@@ -125,7 +217,7 @@ func TestEvexErrors(t *testing.T) {
{"VPMOVDW src", "VPMOVDW", []Operand{AX, vreg(t, "Y0")}}, {"VPMOVDW src", "VPMOVDW", []Operand{AX, vreg(t, "Y0")}},
{"align arity", "VALIGND", []Operand{Imm(1), vreg(t, "Z0"), vreg(t, "Z1")}}, {"align arity", "VALIGND", []Operand{Imm(1), vreg(t, "Z0"), vreg(t, "Z1")}},
// VEX-only mnemonics reject registers only EVEX can encode. // VEX-only mnemonics reject registers only EVEX can encode.
{"VPSHUFD X16", "VPSHUFD", []Operand{Imm(1), vreg(t, "X16"), vreg(t, "X17")}}, {"VMOVMSKPS X16", "VMOVMSKPS", []Operand{vreg(t, "X16"), AX}},
} }
for _, c := range cases { for _, c := range cases {
if _, err := Encode(c.mnem, c.ops...); err == nil { if _, err := Encode(c.mnem, c.ops...); err == nil {
+156 -24
View File
@@ -11,7 +11,9 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath"
"strings" "strings"
"sourcedock.dev/petrbalvin/gasm-devkit/arch" "sourcedock.dev/petrbalvin/gasm-devkit/arch"
@@ -26,7 +28,7 @@ import (
// version is the release version, stamped at build time via // version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release). // -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.6.0" var version = "0.9.0"
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
@@ -47,30 +49,71 @@ func main() {
case "lsp": case "lsp":
os.Exit(cmdLSP(os.Args[2:])) os.Exit(cmdLSP(os.Args[2:]))
case "version", "--version", "-V": case "version", "--version", "-V":
fmt.Printf("gasm %s\n", version) os.Exit(cmdVersion())
case "help", "-h", "--help": case "help", "--help", "-h":
usage(os.Stdout) usage(os.Stdout)
default: default:
fmt.Fprintf(os.Stderr, "gasm: unknown command %q\n\n", os.Args[1]) fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1])
usage(os.Stderr)
os.Exit(2) os.Exit(2)
} }
} }
// cmdVersion prints the release version.
func cmdVersion() int {
fmt.Printf("gasm %s\n", version)
return 0
}
func usage(w io.Writer) { func usage(w io.Writer) {
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler (GAsm)
gasm bundles a lexer, parser, formatter, linter, standalone assembler and
language server for Plan 9 assembly into one self-contained binary.
Usage: Usage:
gasm tokens <file> print the lexical token stream gasm <command> [arguments]
gasm parse <file> parse and report syntax errors gasm [flags]
gasm fmt [-w] <file...> canonicalise formatting (-w writes in place)
gasm lint <file...> run static checks Commands:
gasm asm [-o out.bin] <file> assemble to machine code (amd64, Phase 2) tokens print the lexical token stream
gasm lsp run the language server over stdio parse parse and report syntax errors
gasm version print the version fmt canonicalise formatting (gofmt for assembly)
lint run static checks
asm assemble .s files to machine code (amd64)
lsp run the language server over stdio
version print the version (same as --version)
Flags:
-h, --help show this help
-V, --version print the version
Run "gasm <command> -h" for a command's usage and flags.
Examples:
gasm fmt reformat every .s below the current directory
gasm lint go-flac/*.s run static checks over the kernels
gasm asm -o k.bin kern_amd64.s
`, version) `, version)
} }
// newCommand returns the FlagSet of a subcommand whose -h/--help prints a
// proper usage block: the one-line usage, the long description and the flag
// defaults. The flag package routes -h/--help to fs.Usage and exits 0.
func newCommand(name, usageLine, long string) *flag.FlagSet {
fs := flag.NewFlagSet(name, flag.ExitOnError)
fs.Usage = func() {
w := fs.Output()
fmt.Fprintf(w, "Usage: %s\n\n%s\n", usageLine, strings.TrimSpace(long))
hasFlags := false
fs.VisitAll(func(*flag.Flag) { hasFlags = true })
if hasFlags {
fmt.Fprintln(w, "\nFlags:")
fs.PrintDefaults()
}
}
return fs
}
// readSource returns the contents of path, or stdin when path is "-". // readSource returns the contents of path, or stdin when path is "-".
func readSource(path string) (string, error) { func readSource(path string) (string, error) {
if path == "-" { if path == "-" {
@@ -82,7 +125,10 @@ func readSource(path string) (string, error) {
} }
func cmdTokens(args []string) int { func cmdTokens(args []string) int {
fs := flag.NewFlagSet("tokens", flag.ExitOnError) fs := newCommand("tokens", "gasm tokens <file>", `
Print the lexical token stream of FILE: position, token kind and text, one
token per line. FILE may be "-" to read standard input.
`)
fs.Parse(args) fs.Parse(args)
if fs.NArg() != 1 { if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>") fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>")
@@ -100,7 +146,11 @@ func cmdTokens(args []string) int {
} }
func cmdParse(args []string) int { func cmdParse(args []string) int {
fs := flag.NewFlagSet("parse", flag.ExitOnError) fs := newCommand("parse", "gasm parse <file>", `
Parse FILE and report syntax errors on stderr. On success, print how many
declarations and TEXT functions the file contains. FILE may be "-" to read
standard input.
`)
fs.Parse(args) fs.Parse(args)
if fs.NArg() != 1 { if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm parse <file>") fmt.Fprintln(os.Stderr, "usage: gasm parse <file>")
@@ -130,15 +180,49 @@ func cmdParse(args []string) int {
} }
func cmdFmt(args []string) int { func cmdFmt(args []string) int {
fs := flag.NewFlagSet("fmt", flag.ExitOnError) fs := newCommand("fmt", "gasm fmt [-w] [path...]", `
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
spacing, per-function mnemonic alignment and blank-line layout (exactly one
blank line before each label, TEXT and GLOBL block). Formatting is
idempotent and preserves every line, comments included.
With no paths — or a directory path — every .s file below it is reformatted
in place and the changed files are listed, the way go fmt does; "." and "_"
directories are skipped. Explicit file paths print to stdout unless -w is
given.
`)
write := fs.Bool("w", false, "write result to the source file") write := fs.Bool("w", false, "write result to the source file")
fs.Parse(args) fs.Parse(args)
if fs.NArg() == 0 { // Like go fmt: with no arguments, or with a directory argument, every .s
fmt.Fprintln(os.Stderr, "usage: gasm fmt [-w] <file...>") // file below the directory is formatted in place and the names of the
return 2 // changed files are listed; explicit file arguments keep the -w / stdout
// behaviour.
paths := fs.Args()
dirMode := len(paths) == 0
if dirMode {
paths = []string{"."}
}
var files []string
for _, p := range paths {
info, err := os.Stat(p)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
return 1
}
if info.IsDir() {
dirMode = true
found, err := asmFiles(p)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
return 1
}
files = append(files, found...)
continue
}
files = append(files, p)
} }
rc := 0 rc := 0
for _, path := range fs.Args() { for _, path := range files {
src, err := readSource(path) src, err := readSource(path)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err) fmt.Fprintln(os.Stderr, "gasm:", err)
@@ -146,11 +230,15 @@ func cmdFmt(args []string) int {
continue continue
} }
out := format.Source(path, src) out := format.Source(path, src)
if *write { if dirMode || *write {
if out != src { if out != src {
if err := os.WriteFile(path, []byte(out), 0o644); err != nil { if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err) fmt.Fprintln(os.Stderr, "gasm:", err)
rc = 1 rc = 1
continue
}
if dirMode {
fmt.Println(path)
} }
} }
continue continue
@@ -160,8 +248,40 @@ func cmdFmt(args []string) int {
return rc return rc
} }
// asmFiles collects the .s files below dir, skipping directories whose name
// starts with "." or "_" — as the go tooling does, which keeps .git and
// scratch or reference trees (e.g. _refs) untouched.
func asmFiles(dir string) ([]string, error) {
var out []string
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if path != dir && (strings.HasPrefix(d.Name(), ".") || strings.HasPrefix(d.Name(), "_")) {
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(d.Name(), ".s") {
out = append(out, path)
}
return nil
})
return out, err
}
func cmdLint(args []string) int { func cmdLint(args []string) int {
fs := flag.NewFlagSet("lint", flag.ExitOnError) fs := newCommand("lint", "gasm lint <file...>", `
Run the static checks over the given files and print diagnostics as
"file:line:col: severity: message [code]". The exit status is non-zero when
an error-severity diagnostic is found; warnings (e.g. the register-clobber
audit) do not affect it.
Rules include unknown-instruction, operand-count, undefined-label,
duplicate-label, missing-ret, missing-textflag-include, abi-argsize,
unreachable-code, register-clobber and funcdata-pcdata.
`)
disable := fs.String("disable", "", "comma-separated rule codes to disable") disable := fs.String("disable", "", "comma-separated rule codes to disable")
fs.Parse(args) fs.Parse(args)
if fs.NArg() == 0 { if fs.NArg() == 0 {
@@ -202,7 +322,13 @@ func cmdLint(args []string) int {
} }
func cmdLSP(args []string) int { func cmdLSP(args []string) int {
fs := flag.NewFlagSet("lsp", flag.ExitOnError) fs := newCommand("lsp", "gasm lsp", `
Run the language server over standard input/output: JSON-RPC 2.0 with
Content-Length framing. Point an LSP-capable editor at the binary and
associate it with .s files; the target architecture is inferred from the file
suffix (_amd64.s, _arm64.s, _riscv64.s, _loong64.s). Provides completion,
hover, document symbols, diagnostics and semantic-token highlighting.
`)
fs.Parse(args) fs.Parse(args)
srv := lsp.New(os.Stdin, os.Stdout) srv := lsp.New(os.Stdin, os.Stdout)
if err := srv.Run(); err != nil { if err := srv.Run(); err != nil {
@@ -213,7 +339,13 @@ func cmdLSP(args []string) int {
} }
func cmdAsm(args []string) int { func cmdAsm(args []string) int {
fs := flag.NewFlagSet("asm", flag.ExitOnError) fs := newCommand("asm", "gasm asm [-o out.bin] <file>", `
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
resolved RIP-relative — and printed as a hex dump. With -o the concatenated
image (functions followed by the data section) is written to a file instead.
`)
out := fs.String("o", "", "write the concatenated machine code to this file") out := fs.String("o", "", "write the concatenated machine code to this file")
fs.Parse(args) fs.Parse(args)
if fs.NArg() != 1 { if fs.NArg() != 1 {
+70 -5
View File
@@ -52,6 +52,54 @@ func capture(fn func() int) (stdout, stderr string, code int) {
return string(ob), string(eb), code return string(ob), string(eb), code
} }
// TestCmdFmtRecursive checks the go-fmt-style directory mode: with no
// arguments every .s file below the working directory is formatted in place
// ("." and "_" directories skipped), changed files are listed, and a second
// run is a no-op.
func TestCmdFmtRecursive(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)
unformatted := []byte("TEXT ·f(SB),NOSPLIT,$0\nRET\n")
write := func(path string) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, unformatted, 0o644); err != nil {
t.Fatal(err)
}
}
write("a_amd64.s")
write(filepath.Join("sub", "b_amd64.s"))
write(filepath.Join("_refs", "c_amd64.s"))
write(filepath.Join(".git", "d_amd64.s"))
out, errOut, code := capture(func() int { return cmdFmt(nil) })
if code != 0 {
t.Fatalf("code = %d (%s)", code, errOut)
}
if out != "a_amd64.s\n"+filepath.Join("sub", "b_amd64.s")+"\n" {
t.Errorf("listed files unexpected:\n%s", out)
}
for _, p := range []string{"a_amd64.s", filepath.Join("sub", "b_amd64.s")} {
b, _ := os.ReadFile(p)
if !strings.Contains(string(b), "\tRET") {
t.Errorf("%s not formatted in place:\n%s", p, b)
}
}
for _, p := range []string{filepath.Join("_refs", "c_amd64.s"), filepath.Join(".git", "d_amd64.s")} {
b, _ := os.ReadFile(p)
if string(b) != string(unformatted) {
t.Errorf("%s must not be touched:\n%s", p, b)
}
}
// Second pass: everything is canonical, nothing is listed.
out, _, code = capture(func() int { return cmdFmt(nil) })
if code != 0 || out != "" {
t.Errorf("second pass: code=%d out=%q, want a no-op", code, out)
}
}
func TestCmdTokens(t *testing.T) { func TestCmdTokens(t *testing.T) {
path := writeTemp(t, "f_amd64.s", clean) path := writeTemp(t, "f_amd64.s", clean)
out, _, code := capture(func() int { return cmdTokens([]string{path}) }) out, _, code := capture(func() int { return cmdTokens([]string{path}) })
@@ -152,15 +200,32 @@ func TestCmdFmtWrite(t *testing.T) {
func TestUsage(t *testing.T) { func TestUsage(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
usage(&b) usage(&b)
if !strings.Contains(b.String(), "gasm") { out := b.String()
t.Errorf("usage text unexpected:\n%s", b.String()) for _, want := range []string{
"gasm", "Commands:", "Flags:", "--help", "--version",
"tokens", "parse", "fmt", "lint", "asm", "lsp", "version",
} {
if !strings.Contains(out, want) {
t.Errorf("usage text missing %q:\n%s", want, out)
}
}
}
func TestCmdVersion(t *testing.T) {
out, _, code := capture(func() int { return cmdVersion() })
if code != 0 {
t.Fatalf("code = %d", code)
}
if !strings.Contains(out, version) {
t.Errorf("version output %q does not mention %q", out, version)
} }
} }
func TestCmdArgErrors(t *testing.T) { func TestCmdArgErrors(t *testing.T) {
// Missing file arguments produce a usage error (code 2). // A missing path is an error (code 1); cmdFmt with no arguments is the
if _, _, code := capture(func() int { return cmdFmt(nil) }); code != 2 { // recursive mode now, covered by TestCmdFmtRecursive.
t.Errorf("cmdFmt() code = %d, want 2", code) if _, _, code := capture(func() int { return cmdFmt([]string{"no/such/path"}) }); code != 1 {
t.Errorf("cmdFmt(missing path) code = %d, want 1", code)
} }
if _, _, code := capture(func() int { return cmdLint(nil) }); code != 2 { if _, _, code := capture(func() int { return cmdLint(nil) }); code != 2 {
t.Errorf("cmdLint() code = %d, want 2", code) t.Errorf("cmdLint() code = %d, want 2", code)
+17 -8
View File
@@ -142,7 +142,7 @@ Two deeper analyses sit on top of the AST:
has no System V style callee-saved registers (amd64 `BX`, `R12`–`R15` and has no System V style callee-saved registers (amd64 `BX`, `R12`–`R15` and
the like are caller-saved or permanent scratch, and hand-written kernels may the like are caller-saved or permanent scratch, and hand-written kernels may
clobber them freely). The audited set is the frame pointer and the clobber them freely). The audited set is the frame pointer and the
goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/ the frame pointer, the goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/
`R29`, riscv64 `X27`, loong64 `R22`); the goroutine pointer is reported only `R29`, riscv64 `X27`, loong64 `R22`); the goroutine pointer is reported only
when the function can reach the runtime — it is not `NOSPLIT` or makes a when the function can reach the runtime — it is not `NOSPLIT` or makes a
call — since the ABI0 transition machinery restores it on those paths, and call — since the ABI0 transition machinery restores it on those paths, and
@@ -157,9 +157,15 @@ Two deeper analyses sit on top of the AST:
### `format` ### `format`
The formatter works on the **token stream, not the AST**, so it preserves The formatter works on the **token stream, not the AST**, so it preserves
every line — comments and blanks included. It only normalises indentation, every line — comments and blanks included. It normalises indentation, operand
operand spacing and per-function mnemonic alignment. It is idempotent and its spacing, per-function mnemonic alignment and blank-line layout: a new block
output always round-trips through the parser. (a label, `TEXT` or `GLOBL`) is preceded by exactly one blank line (comments
leading a block stay with it), runs of blanks collapse to one, and a `RET`
terminates the body so the next function's doc comment stays at column 0. It
is idempotent and its output always round-trips through the parser. With a
directory argument — or none — it reformats every `.s` file below it in
place and lists the files changed, the way `go fmt` does (`.` and `_`
directories are skipped).
### `lsp` ### `lsp`
@@ -212,10 +218,13 @@ memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
`VFMADD231PD`) and the no-operand `VZEROUPPER` — together with `VPERMD` and `VFMADD231PD`) and the no-operand `VZEROUPPER` — together with `VPERMD` and
the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves, the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves,
`CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix — the four-byte prefix with `CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix — the four-byte prefix with
5-bit register fields (Z0–Z31, X/Y 16–31), opmask registers as operands and 5-bit register fields (Z0–Z31, X/Y 16–31, with the mod=11 quirk that carries
mask destinations, and the compressed disp8×N displacement, whose multiplier rm[4] in X̄), opmask registers (K0–K7 as operands, mask destinations and
follows the memory operand's size — covering every instruction the go-flac explicit merging/zeroing masks — written the way Go writes them, as a K
AVX2 and AVX-512 kernels use. Every encoding is validated two ways: by operand among the operands plus a `.Z` mnemonic suffix), and the compressed
disp8×N displacement, whose multiplier follows the memory operand's size —
covering every instruction the go-flac and go-lz4 AVX2/AVX-512 kernels use,
plus the common AVX-512 F/BW integer set. Every encoding is validated two ways: by
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against round-trip decoding through `golang.org/x/arch`, and byte-for-byte against
the machine code the real Go assembler emits — a comparison that holds for the machine code the real Go assembler emits — a comparison that holds for
whole functions: all 27 functions of both kernels assemble to exactly the Go whole functions: all 27 functions of both kernels assemble to exactly the Go
+81 -12
View File
@@ -27,14 +27,6 @@ func Source(path, src string) string {
mnemLen int mnemLen int
funcID int funcID int
} }
const (
kBlank = iota
kComment
kPreproc
kDirective
kLabel
kInstr
)
infos := make([]info, len(lines)) infos := make([]info, len(lines))
funcID := -1 funcID := -1
@@ -70,8 +62,8 @@ func Source(path, src string) string {
infos[i] = inf infos[i] = inf
} }
// Second pass: render. // Second pass: render each line.
var b strings.Builder outs := make([]outLine, 0, len(lines))
inBody := false inBody := false
for i, line := range lines { for i, line := range lines {
inf := infos[i] inf := infos[i]
@@ -106,10 +98,87 @@ func Source(path, src string) string {
inBody = false inBody = false
} }
} }
b.WriteString(strings.TrimRight(out, " \t")) outs = append(outs, outLine{kind: inf.kind, text: strings.TrimRight(out, " \t")})
}
return normalizeSpacing(outs)
}
// Line classification, shared by the formatting passes.
const (
kBlank = iota
kComment
kPreproc
kDirective
kLabel
kInstr
)
// outLine is one rendered line together with its classification.
type outLine struct {
kind int
text string
}
// normalizeSpacing enforces the canonical blank-line layout: runs of blank
// lines collapse to one, and a new block — a label, or a TEXT or GLOBL
// directive — is preceded by exactly one blank line. Comments immediately
// above a block belong to it, so the blank line is inserted before them. No
// blank line is forced at the top of the file, right after a TEXT (the
// function's first label), or between stacked labels that share an address.
func normalizeSpacing(outs []outLine) string {
blockStart := func(ol outLine) bool {
switch ol.kind {
case kLabel:
return true
case kDirective:
// TEXT and GLOBL open a block; DATA continues a GLOBL block.
return strings.HasPrefix(ol.text, "TEXT") || strings.HasPrefix(ol.text, "GLOBL")
}
return false
}
insert := make([]bool, len(outs))
for i, ol := range outs {
if !blockStart(ol) {
continue
}
j := i
for j > 0 && outs[j-1].kind == kComment {
j--
}
if j == 0 {
continue // top of file
}
switch prev := outs[j-1]; {
case prev.kind == kBlank, prev.kind == kLabel:
continue // already separated, or stacked labels
case prev.kind == kDirective && strings.HasPrefix(prev.text, "TEXT"):
continue // the function's first label
}
insert[j] = true
}
var b strings.Builder
prevBlank := true // also suppresses leading blanks
for i, ol := range outs {
if insert[i] && !prevBlank {
b.WriteByte('\n') b.WriteByte('\n')
} }
return b.String() if ol.kind == kBlank {
if !prevBlank {
b.WriteByte('\n')
}
prevBlank = true
continue
}
b.WriteString(ol.text)
b.WriteByte('\n')
prevBlank = false
}
out := strings.TrimRight(b.String(), "\n")
if out == "" {
return ""
}
return out + "\n"
} }
// renderInstr renders an instruction line: a tab, the mnemonic padded to the // renderInstr renders an instruction line: a tab, the mnemonic padded to the
+58
View File
@@ -77,6 +77,64 @@ func TestDocCommentIndent(t *testing.T) {
} }
} }
// TestBlankLines checks the blank-line canonicalisation: exactly one blank
// line before a new block (a label, or TEXT/GLOBL), runs of blanks collapsed
// to one, and no blank forced after TEXT, between stacked labels, or at the
// top of the file. Leading comments belong to the block they precede.
func TestBlankLines(t *testing.T) {
in := "#include \"textflag.h\"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"first:\n" + // first label: no blank after TEXT
"XORQ AX, AX\n" +
"JMP next\n" + // unlabeled glue: fmt inserts a blank before next:
"next:\n" +
"stacked:\n" + // stacked labels share an address: no blank between
"INCQ AX\n" +
"\n" +
"\n" + // two blanks collapse to one
"// separated block\n" + // comment belongs to the label below
"later:\n" +
"RET\n" +
"// func g()\n" + // doc comment: blank goes before it
"TEXT ·g(SB), NOSPLIT, $0\n" +
"RET\n" +
"GLOBL ·mask(SB), RODATA, $8\n" + // blank before GLOBL…
"DATA ·mask+0(SB)/4, $1\n" + // …but not before DATA
"\n" +
"\n" +
"\n" // trailing blanks dropped
want := "#include \"textflag.h\"\n" +
"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"first:\n" +
"\tXORQ AX, AX\n" +
"\tJMP next\n" +
"\n" +
"next:\n" +
"stacked:\n" +
"\tINCQ AX\n" +
"\n" +
"\t// separated block\n" + // body comment before a label stays indented
"later:\n" +
"\tRET\n" +
"\n" +
"// func g()\n" +
"TEXT ·g(SB), NOSPLIT, $0\n" +
"\tRET\n" +
"\n" +
"GLOBL ·mask(SB), RODATA, $8\n" +
"DATA ·mask+0(SB)/4, $1\n"
got := Source("b_amd64.s", in)
if got != want {
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
}
if again := Source("b_amd64.s", got); again != got {
t.Fatalf("not idempotent:\n%q", again)
}
}
func TestOperandSpacing(t *testing.T) { func TestOperandSpacing(t *testing.T) {
cases := map[string]string{ cases := map[string]string{
"4(SI)": "4(SI)", "4(SI)": "4(SI)",
+1 -1
View File
@@ -3,7 +3,7 @@
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
version := "0.6.0" version := "0.9.0"
default: default:
@just --list @just --list
+23 -1
View File
@@ -242,7 +242,7 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
} }
} }
if archKnown && !cfg.Disable[CodeOperandCount] && !isMacroInvocation(mnem, macros) { if archKnown && !cfg.Disable[CodeOperandCount] && !isMacroInvocation(mnem, macros) && !maskedEvex(mnem, st.Operands) {
if in, ok := tab.Lookup(mnem); ok && in.MinOps >= 0 { if in, ok := tab.Lookup(mnem); ok && in.MinOps >= 0 {
n := len(st.Operands) n := len(st.Operands)
if n < in.MinOps || n > in.MaxOps { if n < in.MinOps || n > in.MaxOps {
@@ -438,6 +438,28 @@ func isMacroInvocation(mnem string, macros map[string]bool) bool {
return strings.Contains(mnem, "_") || macros[mnem] return strings.Contains(mnem, "_") || macros[mnem]
} }
// maskedEvex reports whether the instruction is a masked EVEX form: the
// mnemonic carries a .Z suffix, or the operand list contains an opmask
// register (K1–K7). Either way the operand count differs from the unmasked
// form, so count checks are skipped.
func maskedEvex(mnem string, ops []*ast.Operand) bool {
if strings.Contains(mnem, ".") {
return true
}
for _, op := range ops {
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Base == "" &&
op.Addr.Index == "" && op.Addr.Sym.Pseudo == "" && isMaskReg(op.Addr.Sym.Name) {
return true
}
}
return false
}
// isMaskReg reports whether name is an opmask register K0–K7.
func isMaskReg(name string) bool {
return len(name) == 2 && name[0] == 'K' && name[1] >= '0' && name[1] <= '7'
}
// isConditionalDirective reports whether a preprocessor directive (the text // isConditionalDirective reports whether a preprocessor directive (the text
// after '#') is a conditional-compilation directive whose branches the parser // after '#') is a conditional-compilation directive whose branches the parser
// cannot resolve. // cannot resolve.
+20
View File
@@ -187,6 +187,26 @@ done:
} }
} }
// TestEvexMaskingRecognised checks that masked EVEX forms — the .Z suffix and
// an explicit K operand — are recognised and exempt from operand-count
// checks.
func TestEvexMaskingRecognised(t *testing.T) {
diags := lintSrc(t, `
#include "textflag.h"
TEXT ·f(SB), NOSPLIT, $0
VPADDD.Z Z1, Z2, K2, Z3
VPMINSD Z1, Z2, K5, Z3
VMOVDQU8 Z1, K3, (SI)
RET
`)
if codes(diags)[CodeUnknownInstr] != 0 {
t.Fatalf("masked EVEX must be recognised: %+v", diags)
}
if codes(diags)[CodeOperandCount] != 0 {
t.Fatalf("masked operand counts must not be flagged: %+v", diags)
}
}
func TestArm64AddressingSuffix(t *testing.T) { func TestArm64AddressingSuffix(t *testing.T) {
// .W (pre-index) and .P (post-index) suffixes must resolve to the base // .W (pre-index) and .P (post-index) suffixes must resolve to the base
// instruction. // instruction.