From 4066396226806b377d02f0952734a246489ea331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 21:31:29 +0200 Subject: [PATCH] feat(asm): encode the GETCALLERPC, REM, DWORD and half-FCVT shapes Assisted-by: GLM 5.3 Flash --- asm/arm64_assemble.go | 81 ++++++++++++++++++++++++ asm/arm64_encode.go | 4 ++ asm/arm64_encode_test.go | 132 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+) diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index 30d7176..3ce002b 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -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. diff --git a/asm/arm64_encode.go b/asm/arm64_encode.go index 3870993..3fcc97c 100644 --- a/asm/arm64_encode.go +++ b/asm/arm64_encode.go @@ -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, diff --git a/asm/arm64_encode_test.go b/asm/arm64_encode_test.go index 90bdef7..cc43690 100644 --- a/asm/arm64_encode_test.go +++ b/asm/arm64_encode_test.go @@ -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]) + } + } +}