feat(asm): encode the arm64 FP immediate moves

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 02:36:24 +02:00
1 parent a9b54b6868
commit fbdad8424f
4 files changed
+181

No files matched your search

+96
View File
@@ -5,6 +5,7 @@ package asm
import (
"fmt"
"math"
"math/bits"
"slices"
"strconv"
@@ -1603,6 +1604,39 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
return encodeARM64SBAddr(src.Imm.Sym, rd, relocs), nil
}
rd := arm64RegNum(operandRegName(dst))
// The FP immediates: FMOVS/FMOVD $f, Fd ride the FMOV (immediate)
// instruction when the 8-bit field carries the value and the
// FMOV-from-ZR move for zero; the toolchain pools anything else
// (obj7.go's preprocess), which needs pool symbols gasm does not
// carry. An integer immediate to an FP register is rejected, the
// zero spelling alone excepted, and FMOVQ has no immediate row.
if mnem == "FMOVS" || mnem == "FMOVD" {
if arm64RegClassOf(operandRegName(dst)) == arm64ClsFP {
f, ok := arm64FloatOperandValue(src)
if !ok {
return nil, fmt.Errorf("%s: illegal combination: an integer immediate needs a real register", mnem)
}
if enc := arm64ChipFloat(f); enc > 0 {
base := uint32(0x1e201000) // FMOV Sd, #imm
if mnem == "FMOVD" {
base = 0x1e601000 // FMOV Dd, #imm
}
return a64wordLE(base | uint32(enc)<<13 | uint32(rd)), nil
}
zero := math.Float64bits(f) == 0
if mnem == "FMOVS" {
zero = math.Float32bits(float32(f)) == 0
}
if zero {
base := uint32(0x1e270000) // FMOV Sd, (W)ZR
if mnem == "FMOVD" {
base = 0x9e670000 // FMOV Dd, XZR
}
return a64wordLE(base | 31<<5 | uint32(rd)), nil
}
return nil, fmt.Errorf("%s $%v: floating-point immediate needs the constant pool", mnem, f)
}
}
// The con(register) form: MOVD $con(Rn), Rd adds the displacement to
// the base register (asm7.go case 4). The toolchain rejects every
// other width and the ZR destination outright (RSP is a real register
@@ -2248,6 +2282,68 @@ func arm64AddSubImmWord(op uint32, v int64, rn, rd uint32) uint32 {
return a64AddSub(1, op, 0, sh, uint32(v), rn, rd)
}
// arm64FloatOperandValue recovers a floating-point immediate: the bare
// spelling fills Imm.Float and the parenthesised one stays in the raw text
// ($ ( 4.0 )), so the token shape comes off the raw string. An integer zero
// rides Imm.HasVal and counts (the toolchain's AFMOVD zero row), any other
// integer immediate does not. ok is false for every other shape.
func arm64FloatOperandValue(op *ast.Operand) (float64, bool) {
if op.Imm.Float != "" {
f, err := strconv.ParseFloat(op.Imm.Float, 64)
if err != nil {
return 0, false
}
if op.Imm.Neg {
f = -f
}
return f, true
}
s := strings.Join(strings.Fields(op.Raw), "")
s = strings.TrimPrefix(s, "$")
if strings.HasPrefix(s, "(") && strings.HasSuffix(s, ")") {
inner := s[1 : len(s)-1]
if strings.ContainsAny(inner, ".eE") {
f, err := strconv.ParseFloat(inner, 64)
if err != nil {
return 0, false
}
return f, true
}
}
if op.Imm.HasVal && op.Imm.Val == 0 && !op.Imm.Neg {
return 0, true
}
return 0, false
}
// arm64ChipFloat ports the toolchain's chipfloat7: the 8-bit FMOV immediate
// field for a double whose low 48 mantissa bits are zero and whose exponent
// sits in the -3..4 band, with bit 7 the sign, bit 6 the negative-exponent
// flag and bits 4:0 the exponent tail plus mantissa top. The toolchain
// takes the encoded value only when this reports above zero; zero itself
// pools (or moves from ZR), so 0 is not a valid encoding here either.
func arm64ChipFloat(e float64) int {
ei := math.Float64bits(e)
l := uint32(ei)
h := uint32(ei >> 32)
if l != 0 || h&0xffff != 0 {
return -1
}
h1 := h & 0x7fc00000
if h1 != 0x40000000 && h1 != 0x3fc00000 {
return -1
}
n := 0
if h&0x80000000 != 0 {
n |= 1 << 7
}
if h1 == 0x3fc00000 {
n |= 1 << 6
}
n |= int((h >> 16) & 0x3f)
return n
}
// arm64ImmWithBase reports whether an immediate operand spells the
// con(register) form, $con(REG): the parser leaves it unstructured (an
// immediate whose raw spelling carries the parenthesised register), so the
+50
View File
@@ -2069,3 +2069,53 @@ func TestArm64ConRnRejections(t *testing.T) {
}
}
}
// TestArm64FPImmediate pins the FP immediate moves against `go tool asm`
// words: the FMOV (immediate) instruction for the 8-bit encodable values and
// the FMOV-from-ZR move for zero, plus the rejections the toolchain raises
// (integer immediates to FP registers, FMOVQ immediates) and the values that
// only the toolchain's $f64 pool reaches.
func TestArm64FPImmediate(t *testing.T) {
got := arm64Words(t,
"\tFMOVS $(4.0), F0\n"+
"\tFMOVD $(4.0), F0\n"+
"\tFMOVS $(0.265625), F1\n"+
"\tFMOVD $(0.1796875), F2\n"+
"\tFMOVD $(28.0), F4\n"+
"\tFMOVD $(-4.0), F6\n"+
"\tFMOVD $0, F0\n"+
"\tFMOVD $(0.0), F5\n")
want := []uint32{
0x1e221000, // FMOV S0, #4.0
0x1e621000, // FMOV D0, #4.0
0x1e2a3001, // FMOV S1, #0.265625
0x1e68f002, // FMOV D2, #0.1796875
0x1e679004, // FMOV D4, #28.0
0x1e721006, // FMOV D6, #-4.0
0x9e6703e0, // FMOV D0, XZR
0x9e6703e5, // FMOV D5, XZR
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])
}
}
for _, src := range []string{
"\tFMOVD\t$5, F0\n",
"\tFMOVS\t$4, F0\n",
"\tFMOVQ\t$(4.0), F0\n",
"\tFMOVD\t$(2.0), F0\n",
} {
f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n")
if len(errs) > 0 {
continue // a parse rejection is a rejection
}
if _, err := AssembleFileARM64(f); err == nil {
t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src))
}
}
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// Differential kernel for the arm64 FP immediate moves: the FMOV (immediate)
// instruction for the 8-bit encodable values and the FMOV-from-ZR move for
// zero. The values outside the 8-bit field pool through the toolchain's
// $f64 symbols with a PC-relative relocation; gasm carries no such pool and
// rejects the spelling instead (its previous encoding was wrong code).
// Every function is byte-compared against go tool asm.
#include "textflag.h"
// func encodable()
TEXT ·encodable(SB), NOSPLIT, $0-0
FMOVS $ (4.0), F0
FMOVD $ (4.0), F0
FMOVS $ (0.265625), F1
FMOVD $ (0.1796875), F2
FMOVS $ (0.96875), F3
FMOVD $ (28.0), F4
FMOVD $ (-4.0), F6
FMOVD $ (1.0), F7
FMOVD $ (0.5), F8
RET
// func zeroes()
TEXT ·zeroes(SB), NOSPLIT, $0-0
FMOVD $0, F0
FMOVS $0, F1
FMOVD $ (0.0), F5
FMOVS $ (0.0), F6
FMOVD ZR, F2
FMOVS ZR, F3
RET
+1
View File
@@ -39,6 +39,7 @@ func TestGroundTruthARM64(t *testing.T) {
"../testdata/verify/splits_arm64.s",
"../testdata/verify/regoffset_arm64.s",
"../testdata/verify/simdarr_arm64.s",
"../testdata/verify/fpimm_arm64.s",
"../testdata/verify/crypto_arm64.s",
"../testdata/verify/integer_arm64.s",
"../testdata/verify/simd_arm64.s",