From be2ceaafb999fd9936503e2e4e20ae023418e35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 27 Aug 2026 16:36:18 +0200 Subject: [PATCH] feat(amd64): encode legacy SSE packed binaries and imm8 shuffles --- arch/amd64.go | 2 ++ asm/encode.go | 11 ++++++ asm/encode_test.go | 57 +++++++++++++++++++++++++++++++ asm/instrs.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+) diff --git a/arch/amd64.go b/arch/amd64.go index c45f071..5434e70 100644 --- a/arch/amd64.go +++ b/arch/amd64.go @@ -270,6 +270,8 @@ func amd64Curated() []Instr { "VMINPD", "VMINPS", "VMINSD", "VMINSS", "VMAXPD", "VMAXPS", "VMAXSD", "VMAXSS", "VXORPD", "VXORPS", "VANDPD", "VANDPS", "VANDNPD", "VANDNPS", "VORPD", "VORPS", "VUNPCKHPD", "VUNPCKLPD", "VUNPCKHPS", "VUNPCKLPS", + "PSHUFD", "PSHUFHW", "PSHUFLW", "SHUFPS", "SHUFPD", + "UNPCKLPS", "UNPCKHPS", "UNPCKLPD", "UNPCKHPD", "VSQRTPD", "VSQRTPS", "VSQRTSD", "VSQRTSS", "VRSQRTPS", "VRCPPS", "VCMPPD", "VCMPPS", "VCMPSD", "VCMPSS", } { diff --git a/asm/encode.go b/asm/encode.go index a8655a7..82bd9c8 100644 --- a/asm/encode.go +++ b/asm/encode.go @@ -76,6 +76,17 @@ func (e *enc) encode(mnem string, ops []Operand) error { if size == 0 { size = 8 // default operand size in 64-bit mode (e.g. PUSHQ) } + // Legacy SSE imm8 shuffles whose names end in W/H (PSHUFLW, + // PSHUFHW) must dispatch BEFORE the size-suffix split, and the + // others ride along. + if m, ok := sseShufTable[upper]; ok { + return e.encodeSSEShuf(m, ops) + } + // Legacy SSE packed binary ops and imm8 shuffles (ADDPS/MULPS/ + // SHUFPS/PSHUFD/...): no size suffix, dispatch on the full name. + if m, ok := sseBinTable[base]; ok { + return e.encodeSSEBin(m, ops) + } switch base { case "MOV": return e.encodeMov(ops, size) diff --git a/asm/encode_test.go b/asm/encode_test.go index 383776e..cec3aa2 100644 --- a/asm/encode_test.go +++ b/asm/encode_test.go @@ -4,6 +4,7 @@ package asm import ( + "fmt" "strings" "testing" @@ -312,3 +313,59 @@ func TestScalarErrors(t *testing.T) { } } } + +// TestSSEBinGroundTruth checks the legacy packed/scalar binary family +// byte for byte (no prefix / 66 / F2 / F3 variants). +func TestSSEBinGroundTruth(t *testing.T) { + cases := []struct { + name string + mnem string + ops []Operand + want string + }{ + {"MULPS X0,X1", "MULPS", []Operand{vreg(t, "X0"), vreg(t, "X1")}, "0f59c8"}, + {"MULPS (DI),X1", "MULPS", []Operand{Ptr(DI, 0, 16), vreg(t, "X1")}, "0f590f"}, + {"ADDPD X1,X2", "ADDPD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "660f58d1"}, + {"XORPS X0,X0", "XORPS", []Operand{vreg(t, "X0"), vreg(t, "X0")}, "0f57c0"}, + {"UNPCKLPS X0,X0", "UNPCKLPS", []Operand{vreg(t, "X0"), vreg(t, "X0")}, "0f14c0"}, + {"MULSD X1,X2", "MULSD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "f20f59d1"}, + {"ADDSS (DI),X0", "ADDSS", []Operand{Ptr(DI, 0, 4), vreg(t, "X0")}, "f30f5807"}, + } + for _, c := range cases { + code, err := Encode(c.mnem, c.ops...) + if err != nil { + t.Errorf("%s: %v", c.name, err) + continue + } + if got := fmt.Sprintf("%x", code); got != c.want { + t.Errorf("%s = %s, want %s", c.name, got, c.want) + } + } +} + +// TestSSEShuffleGroundTruth checks the imm8 shuffle family: immediate +// first in Plan 9 order, encoded last on the wire. +func TestSSEShuffleGroundTruth(t *testing.T) { + cases := []struct { + name string + mnem string + ops []Operand + want string + }{ + {"SHUFPS $0,X0,X0", "SHUFPS", []Operand{Imm(0), vreg(t, "X0"), vreg(t, "X0")}, "0fc6c000"}, + {"SHUFPS $27,X1,X2", "SHUFPS", []Operand{Imm(27), vreg(t, "X1"), vreg(t, "X2")}, "0fc6d11b"}, + {"PSHUFD $0,X0,X0", "PSHUFD", []Operand{Imm(0), vreg(t, "X0"), vreg(t, "X0")}, "660f70c000"}, + {"PSHUFLW $3,(DI),X1", "PSHUFLW", []Operand{Imm(3), Ptr(DI, 0, 8), vreg(t, "X1")}, "f20f700f03"}, + {"PSHUFHW $2,X1,X2", "PSHUFHW", []Operand{Imm(2), vreg(t, "X1"), vreg(t, "X2")}, "f30f70d102"}, + } + for _, c := range cases { + code, err := Encode(c.mnem, c.ops...) + if err != nil { + t.Errorf("%s: %v", c.name, err) + continue + } + if got := fmt.Sprintf("%x", code); got != c.want { + t.Errorf("%s = %s, want %s", c.name, got, c.want) + } + } +} diff --git a/asm/instrs.go b/asm/instrs.go index d7e82cb..6237f38 100644 --- a/asm/instrs.go +++ b/asm/instrs.go @@ -756,6 +756,90 @@ func (e *enc) encodeSSEMove(m sseMove, ops []Operand) error { return e.emit(i) } +// --- legacy SSE packed binary and shuffles ----------------------------------- + +// sseBin describes a legacy (non-VEX) SSE packed/scalar binary op: an +// optional mandatory prefix plus the 0F-prefixed opcode. Plan 9 asm +// lists the source operand first, so MULPS X0, X1 computes X1 = X1 * X0. +type sseBin struct { + prefix byte // 0, 0x66, 0xF2 or 0xF3 + op byte +} + +var sseBinTable = map[string]sseBin{ + "ADDPS": {0, 0x58}, "ADDPD": {0x66, 0x58}, + "MULPS": {0, 0x59}, "MULPD": {0x66, 0x59}, + "SUBPS": {0, 0x5C}, "SUBPD": {0x66, 0x5C}, + "DIVPS": {0, 0x5E}, "DIVPD": {0x66, 0x5E}, + "ANDPS": {0, 0x54}, "ANDPD": {0x66, 0x54}, + "ORPS": {0, 0x56}, "ORPD": {0x66, 0x56}, + "XORPS": {0, 0x57}, "XORPD": {0x66, 0x57}, + "MINPS": {0, 0x5D}, "MINPD": {0x66, 0x5D}, + "MAXPS": {0, 0x5F}, "MAXPD": {0x66, 0x5F}, + "ADDSS": {0xF3, 0x58}, "ADDSD": {0xF2, 0x58}, + "MULSS": {0xF3, 0x59}, "MULSD": {0xF2, 0x59}, + "SUBSS": {0xF3, 0x5C}, "SUBSD": {0xF2, 0x5C}, + "DIVSS": {0xF3, 0x5E}, "DIVSD": {0xF2, 0x5E}, + "MINSS": {0xF3, 0x5D}, "MINSD": {0xF2, 0x5D}, + "MAXSS": {0xF3, 0x5F}, "MAXSD": {0xF2, 0x5F}, + "UNPCKLPS": {0, 0x14}, "UNPCKHPS": {0, 0x15}, + "UNPCKLPD": {0x66, 0x14}, "UNPCKHPD": {0x66, 0x15}, +} + +// sseShuf describes a legacy SSE shuffle taking a trailing imm8 +// (PSHUFD/PSHUFHW/PSHUFLW also carry the packed-int 0x66/F3/F2 prefixes). +type sseShuf struct { + prefix byte + op byte +} + +var sseShufTable = map[string]sseShuf{ + "SHUFPS": {0, 0xC6}, "SHUFPD": {0x66, 0xC6}, + "PSHUFD": {0x66, 0x70}, "PSHUFHW": {0xF3, 0x70}, "PSHUFLW": {0xF2, 0x70}, +} + +// encodeSSEBin encodes reg = reg op rm (memory allowed for rm). +func (e *enc) encodeSSEBin(m sseBin, ops []Operand) error { + if len(ops) != 2 { + return fmt.Errorf("SSE binary expects 2 operands, got %d", len(ops)) + } + src, dst := ops[0], ops[1] + dstReg, ok := dst.(Reg) + if !ok || !dstReg.isVec() { + return fmt.Errorf("SSE binary destination must be a vector register") + } + i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1} + if err := setRM(i, dstReg, src, 8); err != nil { + return err + } + return e.emit(i) +} + +// encodeSSEShuf encodes an imm8 shuffle: SHUFPS $imm, src, dst. +func (e *enc) encodeSSEShuf(m sseShuf, ops []Operand) error { + if len(ops) != 3 { + return fmt.Errorf("SSE shuffle expects 3 operands, got %d", len(ops)) + } + imm, ok := ops[0].(Imm) + if !ok { + return fmt.Errorf("SSE shuffle needs an imm8 first operand") + } + if imm < -128 || imm > 255 { + return fmt.Errorf("SSE shuffle imm8 %d out of range", imm) + } + src, dst := ops[1], ops[2] + dstReg, ok2 := dst.(Reg) + if !ok2 || !dstReg.isVec() { + return fmt.Errorf("SSE shuffle destination must be a vector register") + } + i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1} + if err := setRM(i, dstReg, src, 8); err != nil { + return err + } + i.imm = []byte{byte(int8(imm))} + return e.emit(i) +} + // --- CVTSL2SD / CVTSQ2SD ----------------------------------------------------- // encodeCvtsi2sd encodes a signed integer to scalar double conversion