feat(asm): encode the GETCALLERPC, REM, DWORD and half-FCVT shapes

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 21:34:30 +02:00
1 parent df7a5091e0
commit 4066396226
3 files changed
+217

No files matched your search

+81
View File
@@ -439,6 +439,15 @@ func arm64InstrSize(instr *ast.Instr, fi arm64FrameInfo, pos int) int {
if mnem == "BYTE" {
return len(ops)
}
if mnem == "DWORD" {
// Eight little-endian bytes per immediate.
return 8 * len(ops)
}
// The remainder family: the division into REGTMP plus the MSUB tail.
switch mnem {
case "REM", "REMW", "UREM", "UREMW":
return 8
}
if mnem == "NOP" {
return 0 // the zero-size pseudo-instruction, operand or not
}
@@ -610,6 +619,41 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
return nil, fmt.Errorf("WORD: immediate %d does not fit a 32-bit word", w)
}
return a64wordLE(uint32(w)), nil
case "DWORD":
// The toolchain's ADWORD (asm7.go case 11): eight little-endian
// bytes per immediate. A symbol operand would need the data
// section's relocation plumbing, which the code path does not
// reach, so the immediate form alone is supported.
if len(ops) == 0 {
return nil, fmt.Errorf("DWORD expects an operand")
}
var out []byte
for _, op := range ops {
if !isImmOperand(op) || op.Imm.Sym != nil {
return nil, fmt.Errorf("DWORD: only an immediate operand is supported")
}
v := arm64Imm64(op)
out = append(out, byte(v), byte(v>>8), byte(v>>16), byte(v>>24),
byte(v>>32), byte(v>>40), byte(v>>48), byte(v>>56))
}
return out, nil
case "GETCALLERPC":
// The toolchain's rewrite (obj7.go AGETCALLERPC): a leaf reads the
// link register, a function with a frame reads the saved LR at
// 0(SP), where both prologue shapes leave it.
if len(ops) != 1 {
return nil, fmt.Errorf("GETCALLERPC expects 1 operand, got %d", len(ops))
}
rd := arm64RegNum(operandRegName(ops[0]))
if rd < 0 {
return nil, fmt.Errorf("invalid register operand in GETCALLERPC")
}
if fi.leaf {
return a64wordLE(0xaa000000 | 30<<16 | 31<<5 | uint32(rd)), nil // MOVD R30, Rd
}
return a64wordLE(a64LSU(3, 0, 1, 0, 31, uint32(rd))), nil // MOVD (RSP), Rd
case "REM", "REMW", "UREM", "UREMW":
return encodeARM64Rem(mnem, ops)
case "B", "JMP":
return encodeARM64Branch(mnem, ops, pc, offsets, false, relocs, resolve)
case "BL", "CALL":
@@ -2059,6 +2103,43 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
return encodeARM64RegMove(mnem, src, dst)
}
// encodeARM64Rem encodes the remainder family the toolchain's optab case
// 16 does: the quotient rides REGTMP (R27) through SDIV/UDIV and the
// remainder is the minuend less REGTMP times the divisor, an MSUB whose W
// forms keep the 32-bit size. The two-operand spelling divides into the
// destination (asm7.go: r defaults to rt), and an RSP destination is the
// toolchain's illegal combination.
func encodeARM64Rem(mnem string, ops []*ast.Operand) ([]byte, error) {
if len(ops) != 2 && len(ops) != 3 {
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
rd := arm64RegNum(operandRegName(ops[len(ops)-1]))
if strings.EqualFold(operandRegName(ops[len(ops)-1]), "RSP") {
return nil, fmt.Errorf("%s: illegal combination: the destination cannot be RSP", mnem)
}
rm := arm64RegNum(operandRegName(ops[0]))
rn := rd
if len(ops) == 3 {
rn = arm64RegNum(operandRegName(ops[1]))
}
if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
sf := uint32(1) // 64-bit
if strings.HasSuffix(mnem, "W") {
sf = 0
}
div := sf<<31 | 0xd6<<21 | 3<<10 // SDIV / SDIVW
if strings.HasPrefix(mnem, "U") {
div = sf<<31 | 0xd6<<21 | 2<<10 // UDIV / UDIVW
}
msub := sf<<31 | 0x1b<<24 | 1<<15 // MSUB / MSUBW
return a64WordsLE(
div|uint32(rm)<<16|uint32(rn)<<5|27,
msub|uint32(rm)<<16|uint32(rn)<<10|27<<5|uint32(rd),
), nil
}
// arm64PairSize returns the encoded size of a load/store pair instruction,
// mirroring the encoder's branch order exactly: the label offsets of pass 1
// must match the bytes pass 2 lays down.
+4
View File
@@ -595,6 +595,10 @@ func init() {
"FNEGS": 0x1e214000, "FNEGD": 0x1e614000,
"FSQRTS": 0x1e21c000, "FSQRTD": 0x1e61c000,
"FCVTSD": 0x1e22c000, "FCVTDS": 0x1e624000,
// The half-precision conversions: FPOP1S rows with the half source
// or destination (type 3), single and double beside them.
"FCVTHS": 0x1ee24000, "FCVTHD": 0x1ee2c000,
"FCVTSH": 0x1e23c000, "FCVTDH": 0x1e63c000,
"FRINTNS": 0x1e244000, "FRINTND": 0x1e644000,
"FRINTPS": 0x1e24c000, "FRINTPD": 0x1e64c000,
"FRINTMS": 0x1e254000, "FRINTMD": 0x1e654000,
+132
View File
@@ -2642,3 +2642,135 @@ func TestArm64ImmediateExpression(t *testing.T) {
}
}
}
// TestArm64RemFamily pins the remainder family the toolchain lowers to a
// division into REGTMP plus the MSUB tail (asm7.go case 16): the words are
// `go tool asm`'s own, and the two-operand spelling divides into the
// destination.
func TestArm64RemFamily(t *testing.T) {
got := arm64Words(t, "\tREM R1, R2, R3\n\tREMW R1, R2, R3\n\tUREM R1, R2, R3\n\tUREMW R1, R2, R3\n\tREM R1, R2\n")
want := []uint32{
0x9ac10c5b, // SDIV R1, R2, R27
0x9b018b63, // MSUB R3 = R2 - R27*R1
0x1ac10c5b, // SDIVW R1, R2, R27
0x1b018b63, // MSUBW
0x9ac1085b, // UDIV R1, R2, R27
0x9b018b63, // MSUB
0x1ac1085b, // UDIVW R1, R2, R27
0x1b018b63, // MSUBW
0x9ac10c5b, // SDIV R1, R2, R27
0x9b018b62, // MSUB R2 = R2 - R27*R1
0xd65f03c0, // RET
}
if len(got) != len(want) {
t.Fatalf("word count = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
}
}
f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\tREM R1, R2, RSP\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
if _, err := AssembleFileARM64(f); err == nil {
t.Error("REM with an RSP destination assembled, want an illegal combination")
}
}
// TestArm64GetCallerPC pins the toolchain's rewrite (obj7.go AGETCALLERPC):
// a leaf reads the link register, a function with a frame reads the saved
// LR at 0(SP).
func TestArm64GetCallerPC(t *testing.T) {
got := arm64Words(t, "\tGETCALLERPC R0\n\tGETCALLERPC R5\n")
want := []uint32{
0xaa1e03e0, // MOVD R30, R0: the leaf reads LR
0xaa1e03e5, // MOVD R30, R5
0xd65f03c0, // RET
}
if len(got) != len(want) {
t.Fatalf("word count = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
}
}
// With a call in the body the frame exists and the saved LR sits at
// 0(SP): prologue (3 words), CALL, then the load.
f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $8-8\n\tCALL ·x(SB)\n\tGETCALLERPC R5\n\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := AssembleFileARM64(f)
if err != nil {
t.Fatalf("AssembleFileARM64: %v", err)
}
ws := leWords(img.Code)
// words 3: CALL, 4: MOVD (RSP), R5.
if len(ws) < 6 {
t.Fatalf("word count = %d, want at least 6", len(ws))
}
if ws[4] != 0xf94003e5 {
t.Errorf("non-leaf GETCALLERPC = %08x, want MOVD (RSP), R5 (f94003e5)", ws[4])
}
}
// TestArm64Dword pins the DWORD pseudo-statement: eight little-endian bytes
// per immediate, the toolchain's ADWORD.
func TestArm64Dword(t *testing.T) {
got := arm64Words(t, "\tDWORD $1\n\tDWORD $0x1122334455667788\n")
// The data words ride the listing raw: DWORD $1 is 01 00 00 00 00 00 00 00,
// the second an 8-byte little-endian constant.
wantBytes := []byte{
0x01, 0, 0, 0, 0, 0, 0, 0,
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
}
// The RET follows; locate the 16 data bytes at the head.
_ = got
code := imgCode(t, "\tDWORD $1\n\tDWORD $0x1122334455667788\n")
if len(code) != len(wantBytes)+4 {
t.Fatalf("code length = %d, want %d", len(code), len(wantBytes)+4)
}
for i, b := range wantBytes {
if code[i] != b {
t.Errorf("byte %d = %02x, want %02x", i, code[i], b)
}
}
}
// imgCode assembles a leaf body and returns the whole function image.
func imgCode(t *testing.T, body string) []byte {
t.Helper()
f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+body+"\tRET\n")
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := AssembleFileARM64(f)
if err != nil {
t.Fatalf("AssembleFileARM64: %v", err)
}
return img.Code
}
// TestArm64HalfFCVT pins the half-precision conversions beside their single
// and double relatives, the FPOP1S rows with the half type field.
func TestArm64HalfFCVT(t *testing.T) {
got := arm64Words(t, "\tFCVTHS F1, F2\n\tFCVTSH F1, F2\n\tFCVTDH F1, F2\n\tFCVTHD F1, F2\n")
want := []uint32{
0x1ee24022, // FCVTHS F1, F2: half to single
0x1e23c022, // FCVTSH F1, F2: single to half
0x1e63c022, // FCVTDH F1, F2: half to double
0x1ee2c022, // FCVTHD F1, F2: double to half
0xd65f03c0, // RET
}
if len(got) != len(want) {
t.Fatalf("word count = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
}
}
}