Files
wehub-resource-sync c4536f7e05
CI / test (push) Failing after 1s
CI / macOS amd64 (push) Has been cancelled
CI / macOS arm64 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:31 +08:00

389 lines
14 KiB
Go

package asmhelp
import (
"strings"
"testing"
)
func TestAssemblyInstructionExplanations(t *testing.T) {
tests := []struct {
arch string
instruction string
want string
}{
{"arm64", "MOV (R2), R1", "R1 := memory[R2]"},
{"amd64", "ADDQ $1, R1", "R1 := R1 + 1"},
{"amd64", "LEAQ 0(R1)(R2*4), R3", "R3 := R1 + R2 * 4"},
{"arm64", "FMADDS F0, F2, F1, F2", "F2 := F0 * F1 + F2"},
// x86 Go assembly keeps CMP operands in natural order: flags are
// computed from AX - 0x10 (go.dev/issue/60920).
{"amd64", "CMPQ AX, $0x10", "flags := compare(AX, 0x10)"},
// arm64 Go assembly is source-first: CMP R1, R4 is R4 - R1.
{"arm64", "CMP R1, R4", "flags := compare(R4, R1)"},
// Three-operand arm64 forms compute dst := second op first.
{"arm64", "SUB R1, R5, R3", "R3 := R5 - R1"},
{"arm64", "LSL $3, R5, R3", "R3 := R5 << 3"},
// arm64 sized moves and 32-bit variants must keep matching their
// base rules.
{"arm64", "MOVD (R1), R0", "R0 := memory[R1]"},
{"arm64", "MOVWU (R1), R0", "R0 := memory[R1]"},
{"arm64", "SDIVW R1, R2, R0", "R0 := R2 / R1"},
{"arm64", "FMOVD F1, F0", "F0 := F1"},
// MADD R2, R3, R1, R0 computes R0 = R1*R2 + R3.
{"arm64", "MADD R2, R3, R1, R0", "R0 := R1 * R2 + R3"},
{"arm64", "MSUB R2, R3, R1, R0", "R0 := R3 - R1 * R2"},
}
for _, test := range tests {
help, ok := ForInstruction(test.arch, "", test.instruction)
if !ok {
t.Fatalf("no help for %q", test.instruction)
}
if help.Explanation != test.want {
t.Errorf("explanation for %q (%s) = %q, want %q", test.instruction, test.arch, help.Explanation, test.want)
}
}
}
func TestAssemblyInstructionReference(t *testing.T) {
help, ok := ForInstruction("amd64", "", "JNE 12(PC)")
if !ok || help.Description == "" {
t.Fatalf("JNE help = %#v, %v", help, ok)
}
if help.Explanation != "" {
t.Fatalf("unexpected JNE explanation: %q", help.Explanation)
}
}
func TestUnknownGoAssemblyInstructionHasFallbackReference(t *testing.T) {
// A plausible mnemonic that is in neither the curated rules nor the
// generated reference falls back to the generic line, flagged with a note.
help, ok := ForInstruction("amd64", "", "ZZUNKNOWNOP $49, Y1, Y2, Y3")
if !ok {
t.Fatal("no fallback help for Go assembly instruction")
}
if help.Mnemonic != "ZZUNKNOWNOP" || help.Description != "Execute the ZZUNKNOWNOP instruction." {
t.Fatalf("fallback help = %#v", help)
}
if help.Note == "" {
t.Error("generic fallback should carry a missing-reference note")
}
}
// TestGoAssemblerSpellingsResolve covers the mnemonic spellings the Go
// assembler emits that differ from the ARM/x86 reference names, found by
// disassembling the lensm binary. Each must resolve to real help (no
// missing-reference note).
func TestGoAssemblerSpellingsResolve(t *testing.T) {
cases := []struct{ arch, mnemonic string }{
// arm64 width/type suffixes, V-prefix, .P index, F-pair, "2" variant.
{"arm64", "LDPW"}, {"arm64", "STPW"}, {"arm64", "CBZW"}, {"arm64", "TSTW"},
{"arm64", "REV16W"}, {"arm64", "MOVKW"}, {"arm64", "LDARW"},
{"arm64", "FCMPS"}, {"arm64", "FDIVD"}, {"arm64", "FMOVQ"},
{"arm64", "SCVTFWD"}, {"arm64", "FCVTZSDW"}, {"arm64", "UCVTFD"},
{"arm64", "FLDPQ"}, {"arm64", "FSTPS"}, {"arm64", "FLDPQ.P"},
{"arm64", "VLD1.P"}, {"arm64", "VMOV"}, {"arm64", "VPMULL2"},
{"arm64", "MOVBU.P"}, {"arm64", "BCC"}, {"arm64", "NOOP"},
// amd64 condition families, prefixes, SSE tag, obscure ops.
{"amd64", "CMOVNE"}, {"amd64", "CMOVAE"}, {"amd64", "SETA"}, {"amd64", "SETNE"},
{"amd64", "LOCK"}, {"amd64", "MOVSD_XMM"}, {"amd64", "ICEBP"},
{"amd64", "FS"}, {"amd64", "LRET"},
}
for _, tc := range cases {
help, ok := ForInstruction(tc.arch, "", tc.mnemonic)
if !ok || help.Note != "" || help.Description == "" {
t.Errorf("%s %s did not resolve: ok=%v %#v", tc.arch, tc.mnemonic, ok, help)
}
}
}
func TestCanonicalMnemonicIsAuthoritative(t *testing.T) {
// The decoder's canonical op name resolves the reference directly, even when
// the displayed mnemonic is unrecognisable and no heuristic would map it.
help, ok := ForInstruction("arm64", "FMOV", "ZZZ F0, F1")
if !ok || help.Note != "" || help.Description == "" {
t.Fatalf("canonical FMOV did not resolve: ok=%v %#v", ok, help)
}
if help.Mnemonic != "ZZZ" {
t.Errorf("display mnemonic = %q, want ZZZ (from text)", help.Mnemonic)
}
}
func TestReferenceResolvesArm64GoSpellings(t *testing.T) {
// The Go arm64 assembler spells SIMD ops with a V prefix and a .P
// post-index marker; the reference is keyed by the ARM base name (LD1, MOV).
for _, instruction := range []string{
"VLD1 (R0), [V0.B16]",
"VLD1.P 16(R0), [V0.B16]",
"VMOV V1.B16, V2.B16",
} {
help, ok := ForInstruction("arm64", "", instruction)
if !ok {
t.Fatalf("no help for %q", instruction)
}
if help.Description == "" || help.Note != "" {
t.Errorf("%q should resolve to real reference text, got %#v", instruction, help)
}
}
}
func TestGeneratedReferenceReplacesGenericFallback(t *testing.T) {
// ABS and CRC32 are absent from the curated rules, so they used to get the
// generic "Execute the X instruction." line. The generated asmref table now
// supplies real reference text, while the bespoke Explanation stays empty
// (no rule fabricates semantics for them). Asserted structurally so the test
// holds against whatever prose the current ISA release ships.
for _, tc := range []struct{ arch, instruction string }{
{"arm64", "ABS V0.8B, V1.8B"},
{"amd64", "CRC32 AX, BL"},
} {
mnemonic, _ := splitAssemblyInstruction(tc.instruction)
help, ok := ForInstruction(tc.arch, "", tc.instruction)
if !ok {
t.Fatalf("no help for %q", tc.instruction)
}
if help.Description == "" || help.Description == "Execute the "+mnemonic+" instruction." {
t.Errorf("%q got generic/empty description %q", tc.instruction, help.Description)
}
if help.Explanation != "" {
t.Errorf("unexpected explanation for %q: %q", tc.instruction, help.Explanation)
}
}
}
func TestReferenceLookupToleratesPlan9Suffixes(t *testing.T) {
// lensm shows Plan 9 spellings with size suffixes (CRC32Q), but the table
// is keyed by the base mnemonic (CRC32). The fallback must still resolve it.
help, ok := ForInstruction("amd64", "", "CRC32Q AX, BX")
if !ok {
t.Fatal("no help for CRC32Q")
}
if !strings.Contains(strings.ToUpper(help.Description), "CRC32") {
t.Errorf("CRC32Q description = %q, want it to mention CRC32", help.Description)
}
if len(help.Ports) == 0 {
t.Errorf("CRC32Q should carry ports from the reference")
}
}
func TestPortsAttachToRuleCoveredX86ButNotArm(t *testing.T) {
// ADDQ is covered by the curated rules; ports still come from the reference.
x86, ok := ForInstruction("amd64", "", "ADDQ $1, AX")
if !ok {
t.Fatal("no help for ADDQ")
}
if len(x86.Ports) == 0 {
t.Errorf("amd64 ADDQ should carry ports, got %#v", x86)
}
// The same merged ADD entry must not leak its x86 ports onto arm64.
arm, ok := ForInstruction("arm64", "", "ADD R1, R2, R3")
if !ok {
t.Fatal("no help for arm64 ADD")
}
if len(arm.Ports) != 0 {
t.Errorf("arm64 ADD must not carry x86 ports, got %#v", arm.Ports)
}
}
func TestUndecodableInstructionHasNoFallback(t *testing.T) {
// Undecodable bytes render as "?" in the Go column.
if help, ok := ForInstruction("amd64", "", "?"); ok {
t.Fatalf("unexpected fallback for undecodable instruction: %#v", help)
}
if help, ok := ForInstruction("amd64", "", "// pseudo"); ok {
t.Fatalf("unexpected fallback for non-mnemonic token: %#v", help)
}
}
func TestUnknownNativeAssemblyInstructionHasNoGoFallback(t *testing.T) {
if help, ok := ForNative("", "", "unknownop %rax"); ok {
t.Fatalf("unexpected native fallback: %#v", help)
}
}
func TestNativeAssemblyInstructionHelpUsesNativeRewrite(t *testing.T) {
help, ok := ForNative("", "", "addq $1, %rax")
if !ok {
t.Fatal("no native help for ADDQ")
}
if help.Explanation != "%rax := %rax + 1" {
t.Fatalf("native explanation = %q", help.Explanation)
}
}
func TestNativeARMAssemblyInstructionExplanation(t *testing.T) {
help, ok := ForNative("", "", "add x0, x1, #8")
if !ok {
t.Fatal("no native help for ARM ADD")
}
if help.Explanation != "x0 := x1 + 8" {
t.Fatalf("native ARM explanation = %q", help.Explanation)
}
}
func TestNativeARMStoreInstructionExplanation(t *testing.T) {
help, ok := ForNative("", "", "str x0, [sp, #16]")
if !ok {
t.Fatal("no native help for ARM STR")
}
if help.Explanation != "memory[sp + 16] := x0" {
t.Fatalf("native ARM STR explanation = %q", help.Explanation)
}
}
func TestNativeARMIndexedMemoryExplanations(t *testing.T) {
tests := map[string]string{
"str x30, [sp, #-112]!": "sp := sp - 112; memory[sp] := x30",
"ldr x0, [sp], #16": "x0 := memory[sp]; sp := sp + 16",
"stp x29, x30, [sp, #-16]!": "sp := sp - 16; memory[sp] := pair(x29, x30)",
}
for instruction, want := range tests {
help, ok := ForNative("", "", instruction)
if !ok || help.Explanation != want {
t.Errorf("%q explanation = %q, want %q", instruction, help.Explanation, want)
}
}
}
func TestNativeDirectJumpHasHelp(t *testing.T) {
// x86 GNU syntax spells direct jumps jmpq.
help, ok := ForNative("", "", "jmpq .+0x100")
if !ok {
t.Fatal("no native help for jmpq")
}
if help.Explanation != "PC := .+0x100" {
t.Fatalf("jmpq explanation = %q", help.Explanation)
}
}
func TestNativeARMTwoOperandNeg(t *testing.T) {
help, ok := ForNative("", "", "neg x0, x1")
if !ok {
t.Fatal("no native help for neg")
}
if help.Explanation != "x0 := -x1" {
t.Fatalf("neg explanation = %q", help.Explanation)
}
help, ok = ForNative("", "", "mvn x0, x1")
if !ok {
t.Fatal("no native help for mvn")
}
if help.Explanation != "x0 := ^x1" {
t.Fatalf("mvn explanation = %q", help.Explanation)
}
}
func TestNativeARMUnsignedConditionalBranchIsNotCall(t *testing.T) {
help, ok := ForNative("", "", "b.ls .+0x1bc")
if !ok {
t.Fatal("no native help for B.LS")
}
if help.Description != "Conditional jump after an unsigned comparison." {
t.Fatalf("B.LS description = %q", help.Description)
}
if help.Explanation != "if unsigned lower than or equal (C == 0 or Z == 1), PC := .+0x1bc" {
t.Fatalf("B.LS explanation = %q", help.Explanation)
}
}
func TestAssemblyHelpRuleTableHasNoMnemonicCollisions(t *testing.T) {
type owner struct {
description string
prefix string
}
exact := make(map[string]owner)
for _, rule := range asmInstructionRules {
for _, prefix := range rule.Prefixes {
if previous, exists := exact[prefix]; exists {
t.Errorf("duplicate mnemonic %s in %s and %s", prefix, previous.prefix, prefix)
}
exact[prefix] = owner{description: rule.Description, prefix: prefix}
}
}
for _, rule := range asmInstructionRules {
for _, prefix := range rule.Prefixes {
for suffix := range nativeSizeSuffixes(prefix) {
mnemonic := prefix + suffix
if exactOwner, isExactMnemonic := exact[mnemonic]; isExactMnemonic {
if help, ok := ForInstruction("", "", mnemonic); !ok || help.Description != exactOwner.description {
t.Errorf("exact %s was captured as a suffix of %s", mnemonic, prefix)
}
continue
}
help, ok := ForInstruction("", "", mnemonic)
if !ok || help.Description != rule.Description {
t.Errorf("%s resolved to %#v; want rule for %s", mnemonic, help, prefix)
}
}
}
}
}
func TestAllNativeARMConditionBranchesResolveAsBranches(t *testing.T) {
conditions := map[string]string{
"eq": "if the compared values are equal (Z == 1), PC := .+4",
"ne": "if the compared values are not equal (Z == 0), PC := .+4",
"gt": "if signed greater than (Z == 0 and N == V), PC := .+4",
"ge": "if signed greater than or equal (N == V), PC := .+4",
"lt": "if signed less than (N != V), PC := .+4",
"le": "if signed less than or equal (Z == 1 or N != V), PC := .+4",
"hi": "if unsigned higher than (C == 1 and Z == 0), PC := .+4",
"hs": "if unsigned higher than or equal (C == 1), PC := .+4",
"cs": "if unsigned higher than or equal (C == 1), PC := .+4",
"lo": "if unsigned lower than (C == 0), PC := .+4",
"cc": "if unsigned lower than (C == 0), PC := .+4",
"ls": "if unsigned lower than or equal (C == 0 or Z == 1), PC := .+4",
}
for condition, want := range conditions {
instruction := "b." + condition + " .+4"
help, ok := ForNative("", "", instruction)
if !ok {
t.Errorf("no help for %s", instruction)
continue
}
if strings.Contains(help.Description, "Call a function") {
t.Errorf("%s incorrectly resolved as call: %#v", instruction, help)
}
if help.Explanation != want {
t.Errorf("%s effect = %q, want %q", instruction, help.Explanation, want)
}
}
}
func TestAssemblyInstructionReferenceCoverage(t *testing.T) {
for _, instruction := range []string{
"movzbl (%rax), %eax", "cmovne %rax, %rbx", "sete %al",
"cqto", "syscall", "mfence", "adrp x0, 0x1000", "stp x0, x1, [sp]",
} {
if help, ok := ForNative("", "", instruction); !ok || help.Description == "" || help.Explanation == "" {
t.Errorf("no native help for %q", instruction)
}
}
}
func TestNativeInstructionFamiliesHaveConcreteExplanations(t *testing.T) {
instructions := []string{
"movq %rax, %rbx", "movzbl (%rax), %ebx", "leaq 8(%rax), %rbx",
"addq $1, %rax", "sub x0, x1, x2", "madd x0, x1, x2, x3",
"msub x0, x1, x2, x3", "udiv x0, x1, x2", "and x0, x1, x2",
"orr x0, x1, x2", "eor x0, x1, x2", "lsl x0, x1, #3",
"cmp x0, x1", "tst x0, x1", "fadd d0, d1, d2",
"fsub d0, d1, d2", "fmul d0, d1, d2", "fmadd d0, d1, d2, d3",
"ldrb w0, [x1, #4]", "strh w0, [sp, #8]", "ldp x0, x1, [sp]",
"stp x0, x1, [sp, #-16]!", "adrp x0, 0x1000", "csel x0, x1, x2, eq",
"cset x0, ne", "rev x0, x1", "clz x0, x1", "popcnt %rax, %rbx",
"pushq %rax", "popq %rax", "callq 0x1000", "ret", "jmp 0x1000",
"b.eq 0x1000", "cbz x0, 0x1000", "tbnz x0, #2, 0x1000",
"nop", "syscall", "dmb ish", "yield", "xchgq %rax, %rbx",
}
for _, instruction := range instructions {
help, ok := ForNative("", "", instruction)
if !ok || help.Explanation == "" {
t.Errorf("native %q help has no concrete explanation: %#v", instruction, help)
}
if strings.HasPrefix(help.Explanation, "execute ") {
t.Errorf("native %q uses generic explanation %q", instruction, help.Explanation)
}
}
}