diff --git a/CHANGELOG.md b/CHANGELOG.md index 933883a..1e449e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 resolved to nothing: only the forward forms counted. A negative count now walks the same instruction statements backwards, labels excluded, byte-identical with the toolchain. +- **The arm64 move-wide family reads its immediate as an unsigned + pattern.** `MOVK $(40000<<48)` folds to a negative int64 and was + rejected; the toolchain picks the 16-bit lane from the 64-bit bit + pattern, so the encoder now does the same, and a zero immediate is + rejected where the toolchain rejects it. ## [0.35.0] - 2026-09-22 diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index a5e57ff..9d53d8a 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -3069,29 +3069,29 @@ func encodeARM64MoveWide(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte // base, so MOVZ and MOVN come along for free. opc := baseOp >> 29 & 3 sf := baseOp >> 31 & 1 - v := arm64Imm64(ops[0]) - if v < 0 { - return nil, fmt.Errorf("%s: negative immediate %d", mnem, v) + // The toolchain's optab case 33, shared by the whole family in both + // widths: the immediate is one unsigned 64-bit pattern (a high-lane + // constant such as $(40000<<48) arrives negative through int64 + // folding), it must occupy exactly one 16-bit lane, zero is rejected, + // and the W forms cannot reach the top half. + u := uint64(arm64Imm64(ops[0])) + if u == 0 { + return nil, fmt.Errorf("%s: zero immediate cannot be handled", mnem) } hw := -1 - for i := range 4 { - if v>>(uint(i)*16)&0xFFFF != 0 { - hw = i + for lane := range 4 { + if u&^(uint64(0xFFFF)<<(lane*16)) == 0 { + hw = lane break } } if hw < 0 { - hw = 0 // zero: every chunk is zero, hw = 0 carries it - } - for i := hw + 1; i < 4; i++ { - if v>>(uint(i)*16)&0xFFFF != 0 { - return nil, fmt.Errorf("%s: immediate %d does not fit one 16-bit chunk", mnem, v) - } + return nil, fmt.Errorf("%s: immediate %#x does not fit one 16-bit chunk", mnem, u) } if sf == 0 && hw > 1 { - return nil, fmt.Errorf("%s: immediate %d out of range for the 32-bit form", mnem, v) + return nil, fmt.Errorf("%s: immediate %#x out of range for the 32-bit form", mnem, u) } - return a64wordLE(a64MoveWide(sf, opc, uint32(hw), uint32(v>>uint(hw*16)&0xFFFF), uint32(rd))), nil + return a64wordLE(a64MoveWide(sf, opc, uint32(hw), uint32(u>>uint(hw*16)&0xFFFF), uint32(rd))), nil } // ---- Bitfield/EXTR encoding ---- diff --git a/asm/arm64_encode_test.go b/asm/arm64_encode_test.go index 772eb8c..7bdafc7 100644 --- a/asm/arm64_encode_test.go +++ b/asm/arm64_encode_test.go @@ -1051,6 +1051,41 @@ func TestArm64MOVK(t *testing.T) { } } +// TestArm64MOVKHighLane pins the shifted high-lane immediate the arm64 test +// kernels write: $(40000<<48) folds to a negative int64, and the toolchain +// reads the value as an unsigned 64-bit pattern when it picks the lane. +func TestArm64MOVKHighLane(t *testing.T) { + got := arm64Words(t, "\tMOVK $(40000<<48), R0\n\tMOVK $0x9c40000000000000, R1\n") + want := []uint32{ + 0xf2f38800, // MOVK $(40000<<48), R0 (go tool asm: f2f38800) + 0xf2f38801, // MOVK hw=3 + 0xd65f03c0, + } + 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]) + } + } +} + +// TestArm64MoveWideZeroImmediate pins the toolchain's rejection of a zero +// immediate in the move-wide family (optab case 33: "zero shifts cannot be +// handled"): every lane is zero, so no hw field can carry it. +func TestArm64MoveWideZeroImmediate(t *testing.T) { + for _, mnem := range []string{"MOVK", "MOVZ", "MOVN"} { + f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t"+mnem+" $0, R0\n\tRET\n") + if len(errs) > 0 { + t.Fatalf("%s: parse: %v", mnem, errs) + } + if _, err := AssembleFileARM64(f); err == nil { + t.Errorf("%s $0: expected error, got nil", mnem) + } + } +} + // TestArm64LoadImm64 tests 64-bit immediate loading. func TestArm64LoadImm64(t *testing.T) { src := `#include "textflag.h"