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

965 lines
38 KiB
Go

package asmhelp
import (
"regexp"
"slices"
"strings"
"loov.dev/lensm/internal/asmref"
)
type Help struct {
Mnemonic string
Description string
Explanation string
// Ports lists execution-port usage (uops.info notation) for x86 mnemonics,
// sourced from the generated reference. Empty for other architectures.
Ports []string
// Note flags that the tooltip has no real reference data for this mnemonic;
// the Description is a generic placeholder. Empty when reference data exists.
Note string
}
type asmInstructionRule struct {
Prefixes []string
Description string
Explain func([]string) string
// ExplainArch is used instead of Explain when the operand meaning
// depends on the architecture.
ExplainArch func(arch string, operands []string) string
}
// asmInstructionRules is intentionally data-driven: add a prefix and, when
// useful, a small operand rewrite to extend the built-in reference.
var asmInstructionRules = []asmInstructionRule{
{Prefixes: []string{"MOVZX", "MOVSX", "MOVSXD"}, Description: "Move a value while extending it to a wider size.", Explain: explainMove},
{Prefixes: []string{"MOV"}, Description: "Move or copy data between registers and memory.", Explain: explainMove},
{Prefixes: []string{"LEA"}, Description: "Load an effective address without reading memory.", Explain: explainLEA},
{Prefixes: []string{"ADD", "ADC"}, Description: "Add values; ADC also includes the carry flag.", Explain: explainBinary("+")},
{Prefixes: []string{"SUB", "SBB"}, Description: "Subtract values; SBB also includes the borrow flag.", Explain: explainBinary("-")},
{Prefixes: []string{"MUL", "IMUL"}, Description: "Multiply values (IMUL is signed multiplication).", Explain: explainBinary("*")},
{Prefixes: []string{"MADD"}, Description: "Multiply two values and add a third.", Explain: explainMADD},
{Prefixes: []string{"MSUB"}, Description: "Multiply two values and subtract the product from a third.", Explain: explainMSUB},
{Prefixes: []string{"DIV", "IDIV", "SDIV", "UDIV"}, Description: "Divide values (signed or unsigned according to the mnemonic).", Explain: explainBinary("/")},
{Prefixes: []string{"CQO", "CDQ", "CWD", "CBW"}, Description: "Sign-extend the accumulator for a wider signed operation."},
{Prefixes: []string{"AND"}, Description: "Compute a bitwise AND.", Explain: explainBinary("&")},
{Prefixes: []string{"OR", "ORR"}, Description: "Compute a bitwise OR.", Explain: explainBinary("|")},
{Prefixes: []string{"BIC"}, Description: "Clear selected bits of a value.", Explain: explainBinary("&^")},
{Prefixes: []string{"XOR", "EOR"}, Description: "Compute a bitwise exclusive OR.", Explain: explainBinary("^")},
{Prefixes: []string{"SHL", "SAL", "LSL"}, Description: "Shift bits left.", Explain: explainBinary("<<")},
{Prefixes: []string{"SHR", "LSR"}, Description: "Shift bits right, filling with zeroes.", Explain: explainBinary(">>")},
{Prefixes: []string{"SAR", "ASR"}, Description: "Arithmetic right shift, preserving the sign.", Explain: explainBinary(">>")},
{Prefixes: []string{"INC"}, Description: "Increment a value by one.", Explain: explainUnaryDelta("+", "1")},
{Prefixes: []string{"DEC"}, Description: "Decrement a value by one.", Explain: explainUnaryDelta("-", "1")},
{Prefixes: []string{"NEG"}, Description: "Negate a signed value.", Explain: explainUnaryPrefix("-")},
{Prefixes: []string{"NOT", "MVN"}, Description: "Invert every bit in a value.", Explain: explainUnaryPrefix("^")},
{Prefixes: []string{"CMP"}, Description: "Compare values and update condition flags.", ExplainArch: explainCompare},
{Prefixes: []string{"CMOV"}, Description: "Conditionally move a value when the selected flags match.", Explain: explainMove},
{Prefixes: []string{"SET"}, Description: "Set a byte to 0 or 1 according to condition flags."},
{Prefixes: []string{"CSEL", "CSET"}, Description: "Select or set a value according to condition flags."},
{Prefixes: []string{"TEST", "TST"}, Description: "Test bits and update condition flags without storing a result.", Explain: explainTest},
{Prefixes: []string{"FMADD"}, Description: "Fused floating-point multiply-add with one rounding step.", Explain: explainFMADD},
{Prefixes: []string{"FADD"}, Description: "Add floating-point values.", Explain: explainBinary("+")},
{Prefixes: []string{"FSUB"}, Description: "Subtract floating-point values.", Explain: explainBinary("-")},
{Prefixes: []string{"FMUL"}, Description: "Multiply floating-point values.", Explain: explainBinary("*")},
{Prefixes: []string{"FMOV"}, Description: "Move a floating-point value.", Explain: explainMove},
{Prefixes: []string{"LDR", "LDUR", "LOAD"}, Description: "Load a value from memory into a register.", Explain: explainLoad},
{Prefixes: []string{"STR", "STUR", "STORE"}, Description: "Store a register value in memory.", Explain: explainStore},
{Prefixes: []string{"ADR", "ADRP"}, Description: "Form a PC-relative address."},
{Prefixes: []string{"LDP"}, Description: "Load a pair of registers from memory."},
{Prefixes: []string{"STP"}, Description: "Store a pair of registers to memory."},
{Prefixes: []string{"XCHG", "SWAP"}, Description: "Exchange two values."},
{Prefixes: []string{"BSWAP", "REV"}, Description: "Reverse the byte order of a value."},
{Prefixes: []string{"BSF", "BSR"}, Description: "Scan for the position of the first or last set bit."},
{Prefixes: []string{"CLZ", "CTZ"}, Description: "Count leading or trailing zero bits."},
{Prefixes: []string{"POPCNT"}, Description: "Count the one bits in a value."},
{Prefixes: []string{"PUSH"}, Description: "Push a value onto the stack."},
{Prefixes: []string{"POP"}, Description: "Pop the top stack value."},
{Prefixes: []string{"CALL", "BL", "BLR", "JAL", "JALR"}, Description: "Call a function and save a return address."},
{Prefixes: []string{"LOCK"}, Description: "Make the following read-modify-write instruction atomic."},
{Prefixes: []string{"FS", "GS"}, Description: "Segment override prefix for the following memory access."},
{Prefixes: []string{"SYSCALL", "SVC"}, Description: "Enter the operating system or supervisor."},
{Prefixes: []string{"INT"}, Description: "Raise a software interrupt."},
{Prefixes: []string{"ICEBP"}, Description: "Raise a debug exception (undocumented INT1)."},
{Prefixes: []string{"RET", "LRET"}, Description: "Return to the caller."},
{Prefixes: []string{"JMP", "B", "BR"}, Description: "Jump to another instruction."},
{Prefixes: []string{"JE", "JZ", "BEQ"}, Description: "Jump when values are equal (zero flag set)."},
{Prefixes: []string{"JNE", "JNZ", "BNE"}, Description: "Jump when values are not equal."},
{Prefixes: []string{"JG", "JGE", "JL", "JLE", "BGT", "BGE", "BLT", "BLE"}, Description: "Conditional jump after a signed comparison."},
{Prefixes: []string{"JA", "JAE", "JB", "JBE", "BHI", "BHS", "BLO", "BLS", "BCS", "BCC"}, Description: "Conditional jump after an unsigned comparison."},
{Prefixes: []string{"BMI", "BPL", "BVS", "BVC"}, Description: "Conditional jump testing the sign or overflow flag."},
{Prefixes: []string{"CBZ"}, Description: "Jump when a register is zero."},
{Prefixes: []string{"CBNZ"}, Description: "Jump when a register is not zero."},
{Prefixes: []string{"TBZ"}, Description: "Jump when a selected bit is zero."},
{Prefixes: []string{"TBNZ"}, Description: "Jump when a selected bit is not zero."},
{Prefixes: []string{"NOP", "NOOP"}, Description: "Do nothing for one instruction slot."},
{Prefixes: []string{"HLT", "WFI"}, Description: "Stop or wait for an external event."},
{Prefixes: []string{"PAUSE", "YIELD"}, Description: "Hint that the processor is in a spin-wait loop."},
{Prefixes: []string{"MFENCE", "LFENCE", "SFENCE", "DMB", "DSB", "ISB"}, Description: "Order memory or instruction accesses across this barrier."},
}
// NativeAssemblyInstructionHelp returns reference text and a syntax-correct
// effect for the native (GNU) spelling of an instruction. GNU x86/AT&T uses
// source-first operands while ARM-family GNU syntax uses destination-first.
func ForNative(arch, canonical, text string) (Help, bool) {
mnemonic, operands := splitAssemblyInstruction(text)
lookup := canonicalNativeMnemonic(mnemonic)
// Arch is irrelevant here: only the description is kept, the
// explanation is replaced with a native-syntax rewrite below.
help, ok := knownAssemblyInstructionHelp("", lookup, nil)
if !ok {
return Help{}, false
}
help.Mnemonic = mnemonic
help.Explanation = explainNativeInstruction(lookup, operands)
if help.Explanation == "" {
help.Explanation = explainNativeEffect(lookup, operands)
}
if ref, ok := referenceEntry(arch, canonical, mnemonic); ok && isX86(arch) {
help.Ports = referencePorts(ref)
}
return help, true
}
func canonicalNativeMnemonic(mnemonic string) string {
switch {
case mnemonic == "ADDS":
return "ADD"
case mnemonic == "SUBS":
return "SUB"
case mnemonic == "ANDS":
return "AND"
case strings.HasPrefix(mnemonic, "LDR"):
return "LDR"
case strings.HasPrefix(mnemonic, "LDUR"):
return "LDUR"
case strings.HasPrefix(mnemonic, "STR"):
return "STR"
case strings.HasPrefix(mnemonic, "STUR"):
return "STUR"
case strings.HasPrefix(mnemonic, "CMOV"):
return "CMOV"
case strings.HasPrefix(mnemonic, "SET"):
return "SET"
case strings.HasPrefix(mnemonic, "MOVZ"), strings.HasPrefix(mnemonic, "MOVSX"), strings.HasPrefix(mnemonic, "MOVSXD"):
return "MOVZX"
case mnemonic == "CQTO":
return "CQO"
case strings.HasPrefix(mnemonic, "B."):
condition := strings.TrimPrefix(mnemonic, "B.")
switch condition {
case "EQ":
return "BEQ"
case "NE":
return "BNE"
case "GT":
return "BGT"
case "GE":
return "BGE"
case "LT":
return "BLT"
case "LE":
return "BLE"
case "HI":
return "BHI"
case "HS", "CS":
return "BHS"
case "LO", "CC":
return "BLO"
case "LS":
return "BLS"
case "MI":
return "BMI"
case "PL":
return "BPL"
case "VS":
return "BVS"
case "VC":
return "BVC"
case "AL", "NV":
// Both encode "always" on modern cores.
return "B"
}
}
for _, base := range []string{"PUSH", "POP", "CALL", "JMP", "RET", "XCHG", "POPCNT", "BSWAP", "INC", "DEC", "NEG", "NOT"} {
if mnemonic == base+"B" || mnemonic == base+"W" || mnemonic == base+"L" || mnemonic == base+"Q" {
return base
}
}
return mnemonic
}
func explainNativeEffect(mnemonic string, operands []string) string {
operand := func(index int) string {
if index >= len(operands) {
return "target"
}
return strings.TrimSpace(strings.TrimPrefix(operands[index], "#"))
}
switch {
case mnemonic == "PUSH":
return "stack := push(stack, " + operand(0) + ")"
case mnemonic == "POP":
return operand(0) + " := pop(stack)"
case mnemonic == "CALL" || mnemonic == "BL" || mnemonic == "BLR" || mnemonic == "JAL" || mnemonic == "JALR":
return "call " + operand(0) + " and save the return address"
case mnemonic == "RET":
return "PC := saved return address"
case mnemonic == "JMP" || mnemonic == "B" || mnemonic == "BR":
return "PC := " + operand(0)
case mnemonic == "JE" || mnemonic == "JZ" || mnemonic == "BEQ":
return "if the compared values are equal (Z == 1), PC := " + operand(0)
case mnemonic == "JNE" || mnemonic == "JNZ" || mnemonic == "BNE":
return "if the compared values are not equal (Z == 0), PC := " + operand(0)
case mnemonic == "BGT":
return "if signed greater than (Z == 0 and N == V), PC := " + operand(0)
case mnemonic == "BGE":
return "if signed greater than or equal (N == V), PC := " + operand(0)
case mnemonic == "BLT":
return "if signed less than (N != V), PC := " + operand(0)
case mnemonic == "BLE":
return "if signed less than or equal (Z == 1 or N != V), PC := " + operand(0)
case mnemonic == "JG":
return "if signed greater than (ZF == 0 and SF == OF), PC := " + operand(0)
case mnemonic == "JGE":
return "if signed greater than or equal (SF == OF), PC := " + operand(0)
case mnemonic == "JL":
return "if signed less than (SF != OF), PC := " + operand(0)
case mnemonic == "JLE":
return "if signed less than or equal (ZF == 1 or SF != OF), PC := " + operand(0)
case mnemonic == "BHI":
return "if unsigned higher than (C == 1 and Z == 0), PC := " + operand(0)
case mnemonic == "BHS":
return "if unsigned higher than or equal (C == 1), PC := " + operand(0)
case mnemonic == "BLO":
return "if unsigned lower than (C == 0), PC := " + operand(0)
case mnemonic == "BLS":
return "if unsigned lower than or equal (C == 0 or Z == 1), PC := " + operand(0)
case mnemonic == "BMI":
return "if negative (N == 1), PC := " + operand(0)
case mnemonic == "BPL":
return "if positive or zero (N == 0), PC := " + operand(0)
case mnemonic == "BVS":
return "if overflow (V == 1), PC := " + operand(0)
case mnemonic == "BVC":
return "if no overflow (V == 0), PC := " + operand(0)
case mnemonic == "JA":
return "if unsigned above (CF == 0 and ZF == 0), PC := " + operand(0)
case mnemonic == "JAE":
return "if unsigned above or equal (CF == 0), PC := " + operand(0)
case mnemonic == "JB":
return "if unsigned below (CF == 1), PC := " + operand(0)
case mnemonic == "JBE":
return "if unsigned below or equal (CF == 1 or ZF == 1), PC := " + operand(0)
case mnemonic == "CBZ":
return "if " + operand(0) + " == 0, PC := " + operand(1)
case mnemonic == "CBNZ":
return "if " + operand(0) + " != 0, PC := " + operand(1)
case mnemonic == "TBZ":
return "if bit " + operand(1) + " of " + operand(0) + " == 0, PC := " + operand(2)
case mnemonic == "TBNZ":
return "if bit " + operand(1) + " of " + operand(0) + " != 0, PC := " + operand(2)
case mnemonic == "INC":
return operand(0) + " := " + operand(0) + " + 1"
case mnemonic == "DEC":
return operand(0) + " := " + operand(0) + " - 1"
case mnemonic == "NEG":
return operand(0) + " := -" + operand(0)
case mnemonic == "NOT":
return operand(0) + " := ^" + operand(0)
case mnemonic == "SET":
return operand(0) + " := condition ? 1 : 0"
case mnemonic == "NOP":
return "state is unchanged"
case mnemonic == "SYSCALL" || mnemonic == "SVC" || mnemonic == "INT":
return "transfer control to the operating system"
case mnemonic == "MFENCE" || mnemonic == "LFENCE" || mnemonic == "SFENCE" || mnemonic == "DMB" || mnemonic == "DSB" || mnemonic == "ISB":
return "wait for the ordered accesses to complete"
case mnemonic == "PAUSE" || mnemonic == "YIELD":
return "temporarily yield processor execution resources"
case mnemonic == "HLT" || mnemonic == "WFI":
return "wait until an interrupt or event occurs"
case mnemonic == "CQO" || mnemonic == "CDQ" || mnemonic == "CWD" || mnemonic == "CBW":
return "extend the accumulator's sign into the high half"
case mnemonic == "XCHG" || mnemonic == "SWAP":
return operand(0) + ", " + operand(1) + " := " + operand(1) + ", " + operand(0)
}
return ""
}
func explainNativeInstruction(mnemonic string, operands []string) string {
if len(operands) == 0 {
return ""
}
// Registers prefixed with % and immediates prefixed with $ identify GNU
// x86/AT&T syntax, whose source-first operand order agrees with the
// source-first Plan 9 rewrites (hence the empty arch: the "amd64" CMP
// rewrite is natural-order, which would be wrong for AT&T).
if strings.Contains(strings.Join(operands, " "), "%") || strings.HasPrefix(strings.TrimSpace(operands[0]), "$") {
if help, ok := ForInstruction("", "", mnemonic+" "+strings.Join(operands, ", ")); ok {
if help.Explanation != "" {
return help.Explanation
}
}
return explainNativeX86Effect(mnemonic, operands)
}
value := formatNativeValue
if len(operands) >= 2 {
destination := value(operands[0])
switch {
case mnemonic == "MOVZX", strings.HasPrefix(mnemonic, "MOV"):
return destination + " := " + value(operands[1])
case mnemonic == "LDR" || mnemonic == "LDUR" || mnemonic == "LOAD":
return explainNativeLoad(destination, operands[1:])
case mnemonic == "STR" || mnemonic == "STUR" || mnemonic == "STORE":
return explainNativeStore(value(operands[0]), operands[1:])
case mnemonic == "LDP" && len(operands) >= 3:
return explainNativePairLoad(value(operands[0]), value(operands[1]), operands[2:])
case mnemonic == "STP" && len(operands) >= 3:
return explainNativePairStore(value(operands[0]), value(operands[1]), operands[2:])
case mnemonic == "LEA", mnemonic == "ADR", mnemonic == "ADRP":
return destination + " := address(" + value(operands[1]) + ")"
case mnemonic == "ADD" || mnemonic == "ADC":
return explainNativeDestinationFirst(destination, operands[1:], "+", value)
case mnemonic == "SUB" || mnemonic == "SBB":
return explainNativeDestinationFirst(destination, operands[1:], "-", value)
case mnemonic == "MUL" || mnemonic == "IMUL" || mnemonic == "FMUL":
return explainNativeDestinationFirst(destination, operands[1:], "*", value)
case mnemonic == "MADD" && len(operands) >= 4:
return destination + " := " + value(operands[1]) + " * " + value(operands[2]) + " + " + value(operands[3])
case mnemonic == "MSUB" && len(operands) >= 4:
return destination + " := " + value(operands[3]) + " - " + value(operands[1]) + " * " + value(operands[2])
case mnemonic == "NEG":
// arm64 two-operand form: neg x0, x1 is x0 := -x1.
return destination + " := -" + value(operands[1])
case mnemonic == "NOT" || mnemonic == "MVN":
return destination + " := ^" + value(operands[1])
case mnemonic == "DIV" || mnemonic == "IDIV" || mnemonic == "SDIV" || mnemonic == "UDIV":
return explainNativeDestinationFirst(destination, operands[1:], "/", value)
case mnemonic == "AND":
return explainNativeDestinationFirst(destination, operands[1:], "&", value)
case mnemonic == "BIC":
return explainNativeDestinationFirst(destination, operands[1:], "&^", value)
case mnemonic == "OR" || mnemonic == "ORR":
return explainNativeDestinationFirst(destination, operands[1:], "|", value)
case mnemonic == "XOR" || mnemonic == "EOR":
return explainNativeDestinationFirst(destination, operands[1:], "^", value)
case mnemonic == "SHL" || mnemonic == "SAL" || mnemonic == "LSL":
return explainNativeDestinationFirst(destination, operands[1:], "<<", value)
case mnemonic == "SHR" || mnemonic == "LSR" || mnemonic == "SAR" || mnemonic == "ASR":
return explainNativeDestinationFirst(destination, operands[1:], ">>", value)
case mnemonic == "FADD":
return explainNativeDestinationFirst(destination, operands[1:], "+", value)
case mnemonic == "FSUB":
return explainNativeDestinationFirst(destination, operands[1:], "-", value)
case mnemonic == "FMADD" && len(operands) >= 4:
return destination + " := " + value(operands[1]) + " * " + value(operands[2]) + " + " + value(operands[3])
case mnemonic == "CMP":
return "flags := compare(" + value(operands[0]) + ", " + value(operands[1]) + ")"
case mnemonic == "TEST" || mnemonic == "TST":
return "flags := " + value(operands[0]) + " & " + value(operands[1])
case mnemonic == "CSEL" && len(operands) >= 4:
return destination + " := condition(" + value(operands[3]) + ") ? " + value(operands[1]) + " : " + value(operands[2])
case mnemonic == "CSET":
return destination + " := condition(" + value(operands[1]) + ") ? 1 : 0"
case mnemonic == "POPCNT":
return destination + " := countOneBits(" + value(operands[1]) + ")"
case mnemonic == "BSWAP" || mnemonic == "REV":
return destination + " := reverseBytes(" + value(operands[1]) + ")"
case mnemonic == "BSF" || mnemonic == "BSR" || mnemonic == "CLZ" || mnemonic == "CTZ":
return destination + " := significantBitPosition(" + value(operands[1]) + ")"
}
}
return ""
}
func explainNativeX86Effect(mnemonic string, operands []string) string {
operand := func(index int) string {
if index >= len(operands) {
return "target"
}
return formatNativeValue(operands[index])
}
destination := operand(len(operands) - 1)
switch mnemonic {
case "POPCNT":
return destination + " := countOneBits(" + operand(0) + ")"
case "BSWAP":
return destination + " := reverseBytes(" + destination + ")"
case "XCHG":
return operand(0) + ", " + operand(1) + " := " + operand(1) + ", " + operand(0)
case "SET":
return destination + " := condition ? 1 : 0"
case "CMOV":
return "if condition, " + destination + " := " + operand(0)
}
return explainNativeEffect(mnemonic, operands)
}
func formatNativeValue(operand string) string {
operand = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(operand), "#"))
if strings.HasPrefix(operand, "[") {
return formatNativeMemory(operand)
}
return operand
}
func formatNativeMemory(operand string) string {
_, address, _ := parseNativeMemory(operand)
return "memory[" + address + "]"
}
func explainNativeLoad(destination string, addressOperands []string) string {
base, address, preIndex := parseNativeMemory(addressOperands[0])
if preIndex {
return base + " := " + address + "; " + destination + " := memory[" + base + "]"
}
effect := destination + " := memory[" + address + "]"
if len(addressOperands) > 1 {
effect += "; " + base + " := " + addNativeOffset(base, addressOperands[1])
}
return effect
}
func explainNativeStore(source string, addressOperands []string) string {
base, address, preIndex := parseNativeMemory(addressOperands[0])
if preIndex {
return base + " := " + address + "; memory[" + base + "] := " + source
}
effect := "memory[" + address + "] := " + source
if len(addressOperands) > 1 {
effect += "; " + base + " := " + addNativeOffset(base, addressOperands[1])
}
return effect
}
func explainNativePairLoad(first, second string, addressOperands []string) string {
base, address, preIndex := parseNativeMemory(addressOperands[0])
load := first + ", " + second + " := pair(memory[" + address + "])"
if preIndex {
return base + " := " + address + "; " + first + ", " + second + " := pair(memory[" + base + "])"
}
if len(addressOperands) > 1 {
load += "; " + base + " := " + addNativeOffset(base, addressOperands[1])
}
return load
}
func explainNativePairStore(first, second string, addressOperands []string) string {
base, address, preIndex := parseNativeMemory(addressOperands[0])
store := "memory[" + address + "] := pair(" + first + ", " + second + ")"
if preIndex {
return base + " := " + address + "; memory[" + base + "] := pair(" + first + ", " + second + ")"
}
if len(addressOperands) > 1 {
store += "; " + base + " := " + addNativeOffset(base, addressOperands[1])
}
return store
}
func parseNativeMemory(operand string) (base, address string, preIndex bool) {
operand = strings.TrimSpace(operand)
preIndex = strings.HasSuffix(operand, "!")
inside := strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(strings.TrimPrefix(operand, "["), "!"), "]"))
parts := splitAssemblyOperands(inside)
base = strings.TrimSpace(parts[0])
address = base
if len(parts) > 1 {
address = addNativeOffset(base, strings.Join(parts[1:], ", "))
}
return base, address, preIndex
}
func addNativeOffset(base, offset string) string {
offset = strings.TrimSpace(strings.ReplaceAll(offset, "#", ""))
if after, ok := strings.CutPrefix(offset, "-"); ok {
return base + " - " + after
}
return base + " + " + offset
}
func explainNativeDestinationFirst(destination string, sources []string, operator string, value func(string) string) string {
if len(sources) == 1 {
return destination + " := " + destination + " " + operator + " " + value(sources[0])
}
return destination + " := " + value(sources[0]) + " " + operator + " " + value(sources[1])
}
// ForInstruction returns help for a Go-assembly instruction. canonical is the
// decoder's mnemonic (e.g. "LD1" for the Go spelling "VLD1"); pass "" when it is
// unavailable and the mnemonic is recovered from text instead.
func ForInstruction(arch, canonical, text string) (Help, bool) {
display, operands := splitAssemblyInstruction(text)
if display == "" {
return Help{}, false
}
// Strip the Go assembler's .P/.W index marker before matching, so post- and
// pre-index forms (MOVBU.P, FLDPQ.P) resolve like their base mnemonic.
mnemonic := display
if i := strings.IndexByte(mnemonic, '.'); i > 0 {
mnemonic = mnemonic[:i]
}
ref, hasRef := referenceEntry(arch, canonical, mnemonic)
help, ok := knownAssemblyInstructionHelp(arch, mnemonic, operands)
switch {
case ok:
// Curated rules own the bespoke Explanation semantics; keep them.
case hasRef:
// For mnemonics the rules don't cover, fall back to the generated
// reference so the tooltip shows real ARM/x86 text.
help, ok = Help{Description: referenceBrief(ref)}, true
case plausibleMnemonic(mnemonic):
help = Help{
Description: "Execute the " + mnemonic + " instruction.",
Note: noReferenceNote,
}
default:
return Help{}, false
}
// Ports are x86-only in the reference; the table merges ARM and x86 under
// one mnemonic key, so gate by arch to avoid showing x86 ports for arm64.
if hasRef && isX86(arch) {
help.Ports = referencePorts(ref)
}
help.Mnemonic = display
return help, true
}
func isX86(arch string) bool {
return arch == "386" || arch == "amd64"
}
// referenceArch is the microarchitecture whose port usage is summarised in the
// tooltip. The table stores every measured microarchitecture; the hover shows
// one recent, well-covered core.
const referenceArch = "ADL-P"
// referencePorts returns the distinct port usages measured for the reference
// microarchitecture across the mnemonic's operand forms.
func referencePorts(ref asmref.Entry) []string {
var out []string
for _, p := range ref.PerfFor(referenceArch) {
if p.Ports != "" && !slices.Contains(out, p.Ports) {
out = append(out, p.Ports)
}
}
return out
}
// noReferenceNote is shown on tooltips that have no real reference data — the
// Description is only a generic placeholder.
const noReferenceNote = "No reference information available for this instruction."
// referenceEntry looks up the generated reference for an instruction. It tries
// the canonical decoder mnemonic first (authoritative), then falls back to
// spellings recovered from the displayed text: the table is keyed by base
// mnemonic (ADD, LD1, CRC32) while the disassembly shows ADDQ, CRC32Q (x86 size
// suffixes) or VLD1.P, VMOV (arm64 SIMD V-prefix and .P post-index marker).
func referenceEntry(arch, canonical, textMnemonic string) (asmref.Entry, bool) {
for _, m := range []string{canonical, textMnemonic} {
if m == "" {
continue
}
for _, candidate := range mnemonicCandidates(arch, m) {
if e, ok := asmref.Lookup(candidate); ok {
return e, true
}
}
}
return asmref.Entry{}, false
}
// mnemonicCandidates returns the base mnemonics to try, most specific first.
func mnemonicCandidates(arch, mnemonic string) []string {
candidates := []string{mnemonic}
base := mnemonic
// Go assembler dot-suffixes mark writeback/post-index (VLD1.P -> VLD1).
if i := strings.IndexByte(base, '.'); i > 0 {
base = base[:i]
candidates = append(candidates, base)
}
if isX86(arch) {
// Go disambiguates some SSE ops with a _XMM tag (MOVSD_XMM -> MOVSD).
if i := strings.IndexByte(base, '_'); i > 0 {
base = base[:i]
candidates = append(candidates, base)
}
if s := trimGoAsmSuffix(base); s != base {
candidates = append(candidates, s)
}
return candidates
}
// arm64: expand the Go spelling into candidate ARM base mnemonics. Only
// table hits are used, so over-eager candidates that match nothing are
// harmless; rule-covered mnemonics (e.g. branches) never reach here.
forms := []string{base}
// Go spells SIMD ops with a leading V (VLD1 -> LD1, VMOV -> MOV).
if strings.HasPrefix(base, "V") && len(base) > 1 {
forms = append(forms, base[1:])
}
for _, f := range append([]string(nil), forms...) {
// Trailing type/width suffixes: W (32-bit), B/H/S/D/Q (element size),
// and two-letter combos on converts (FCVTZSDW -> FCVTZS).
for n := 1; n <= 2; n++ {
if len(f) > n && isArmSizeSuffix(f[len(f)-n:]) {
forms = append(forms, f[:len(f)-n])
}
}
// "2" upper-half variants (PMULL2, UMLAL2) are documented under the base.
if strings.HasSuffix(f, "2") && len(f) > 1 {
forms = append(forms, f[:len(f)-1])
}
// SIMD&FP register-pair load/store: FLDP/FSTP -> LDP/STP.
if strings.HasPrefix(f, "FLDP") {
forms = append(forms, "LDP")
} else if strings.HasPrefix(f, "FSTP") {
forms = append(forms, "STP")
}
}
return append(candidates, forms...)
}
func isArmSizeSuffix(s string) bool {
for _, r := range s {
switch r {
case 'B', 'H', 'W', 'S', 'D', 'Q':
default:
return false
}
}
return true
}
// trimGoAsmSuffix removes a single trailing Go-assembler operand-size suffix.
// Longer suffixes (BU/HU/WU) are tried before single letters. Only used after
// an exact lookup misses, so stripping a genuine trailing letter is harmless.
func trimGoAsmSuffix(mnemonic string) string {
for _, suffix := range []string{"BU", "HU", "WU", "B", "W", "L", "Q", "D", "H", "S"} {
if len(mnemonic) > len(suffix) && strings.HasSuffix(mnemonic, suffix) {
return mnemonic[:len(mnemonic)-len(suffix)]
}
}
return mnemonic
}
// referenceBrief returns a short human description for an entry, preferring the
// brief title over the full first paragraph.
func referenceBrief(ref asmref.Entry) string {
if ref.Brief != "" {
return ref.Brief
}
return ref.Description
}
// plausibleMnemonic reports whether a token looks like an instruction
// mnemonic. Undecodable bytes render as "?", which must not get an
// authoritative-sounding "Execute the ? instruction." fallback.
func plausibleMnemonic(mnemonic string) bool {
if r := mnemonic[0]; r < 'A' || r > 'Z' {
return false
}
for _, r := range mnemonic {
switch {
case 'A' <= r && r <= 'Z', '0' <= r && r <= '9', r == '.', r == '_':
default:
return false
}
}
return true
}
func knownAssemblyInstructionHelp(arch, mnemonic string, operands []string) (Help, bool) {
// Resolve exact mnemonics first. Otherwise BLS can be mistaken for BL with
// an S size suffix, and similar prefix collisions produce wrong semantics.
for _, rule := range asmInstructionRules {
if slices.Contains(rule.Prefixes, mnemonic) {
return assemblyHelpFromRule(arch, mnemonic, operands, rule), true
}
}
for _, rule := range asmInstructionRules {
for _, prefix := range rule.Prefixes {
if mnemonicMatches(mnemonic, prefix) {
return assemblyHelpFromRule(arch, mnemonic, operands, rule), true
}
}
}
return Help{}, false
}
func assemblyHelpFromRule(arch, mnemonic string, operands []string, rule asmInstructionRule) Help {
help := Help{Mnemonic: mnemonic, Description: rule.Description}
switch {
case rule.ExplainArch != nil:
help.Explanation = rule.ExplainArch(arch, operands)
case rule.Explain != nil:
help.Explanation = rule.Explain(operands)
}
return help
}
func mnemonicMatches(mnemonic, prefix string) bool {
if mnemonic == prefix {
return true
}
if !strings.HasPrefix(mnemonic, prefix) {
return false
}
suffix := strings.TrimPrefix(mnemonic, prefix)
// CMOV/SET take an x86 condition-code suffix (CMOVNE, SETA) rather than a
// size suffix.
if conditionFamilies[prefix] {
return x86ConditionCodes[suffix]
}
return nativeSizeSuffixes(prefix)[suffix]
}
// conditionFamilies are mnemonics whose suffix is a condition code, not a size.
var conditionFamilies = map[string]bool{"CMOV": true, "SET": true}
var x86ConditionCodes = map[string]bool{
"E": true, "NE": true, "Z": true, "NZ": true, "A": true, "AE": true,
"B": true, "BE": true, "G": true, "GE": true, "L": true, "LE": true,
"O": true, "NO": true, "S": true, "NS": true, "P": true, "PE": true,
"NP": true, "PO": true, "C": true, "NC": true,
}
// Suffix sets are package-level: nativeSizeSuffixes runs in the per-frame
// hover lookup and must not allocate.
var (
x86IntegerSuffixes = map[string]bool{"B": true, "W": true, "L": true, "Q": true}
floatOrVectorSuffixes = map[string]bool{"S": true, "D": true}
// arm64 Go assembly sizes moves with D/H/W/B plus zero-extending
// BU/HU/WU: MOVD, MOVWU, ... The x86 sizes stay valid alongside.
movSuffixes = map[string]bool{
"B": true, "W": true, "L": true, "Q": true,
"D": true, "H": true, "BU": true, "HU": true, "WU": true,
}
// arm64 32-bit register variants: SDIVW, MADDW, LSLW, ...
arm64WordSuffixes = map[string]bool{"W": true}
)
func nativeSizeSuffixes(mnemonic string) map[string]bool {
switch mnemonic {
case "MOV":
return movSuffixes
case "FMOV":
return floatOrVectorSuffixes
case "LEA", "ADD", "ADC", "SUB", "SBB", "MUL", "IMUL", "DIV", "IDIV",
"AND", "OR", "XOR", "SHL", "SAL", "SHR", "SAR", "INC", "DEC", "NEG", "NOT",
"CMP", "TEST", "PUSH", "POP", "CALL", "RET", "XCHG", "BSWAP", "POPCNT":
return x86IntegerSuffixes
case "SDIV", "UDIV", "MADD", "MSUB", "ORR", "EOR", "LSL", "LSR", "ASR", "BIC", "MVN":
return arm64WordSuffixes
case "FADD", "FSUB", "FMUL", "FMADD":
return floatOrVectorSuffixes
default:
return nil
}
}
func splitAssemblyInstruction(text string) (string, []string) {
text = strings.TrimSpace(text)
if text == "" {
return "", nil
}
if index := strings.Index(text, "\t"); index >= 0 {
text = text[:index]
}
fields := strings.Fields(text)
if len(fields) == 0 {
return "", nil
}
mnemonic := strings.ToUpper(strings.TrimSpace(fields[0]))
rest := strings.TrimSpace(strings.TrimPrefix(text, fields[0]))
if rest == "" {
return mnemonic, nil
}
parts := splitAssemblyOperands(rest)
operands := make([]string, 0, len(parts))
for _, part := range parts {
operands = append(operands, strings.TrimSpace(part))
}
return mnemonic, operands
}
func splitAssemblyOperands(text string) []string {
var parts []string
start, depth := 0, 0
for index, char := range text {
switch char {
case '(', '[':
depth++
case ')', ']':
if depth > 0 {
depth--
}
case ',':
if depth == 0 {
parts = append(parts, text[start:index])
start = index + 1
}
}
}
return append(parts, text[start:])
}
func explainMove(operands []string) string {
if len(operands) < 2 {
return ""
}
return formatDestination(operands[len(operands)-1]) + " := " + formatValue(operands[0])
}
func explainLEA(operands []string) string {
if len(operands) < 2 {
return ""
}
return formatDestination(operands[len(operands)-1]) + " := " + formatAddress(operands[0])
}
func explainBinary(operator string) func([]string) string {
return func(operands []string) string {
if len(operands) < 2 {
return ""
}
destination := formatDestination(operands[len(operands)-1])
if len(operands) == 2 {
return destination + " := " + destination + " " + operator + " " + formatValue(operands[0])
}
// Three-operand Go assembly computes dst := second op first:
// SUB R1, R5, R3 is R3 = R5 - R1, LSL $3, R5, R3 is R3 = R5 << 3.
return destination + " := " + formatValue(operands[len(operands)-2]) + " " + operator + " " + formatValue(operands[0])
}
}
func explainUnaryDelta(operator, amount string) func([]string) string {
return func(operands []string) string {
if len(operands) == 0 {
return ""
}
destination := formatDestination(operands[len(operands)-1])
return destination + " := " + destination + " " + operator + " " + amount
}
}
func explainUnaryPrefix(operator string) func([]string) string {
return func(operands []string) string {
if len(operands) == 0 {
return ""
}
destination := formatDestination(operands[len(operands)-1])
return destination + " := " + operator + destination
}
}
func explainCompare(arch string, operands []string) string {
if len(operands) < 2 {
return ""
}
// Go assembly for x86 deliberately keeps CMP operands in natural
// left-to-right order (CMPQ AX, BX sets flags from AX-BX; see
// go.dev/issue/60920), while arm64 and the other ports use
// source-first order (CMP R1, R4 sets flags from R4-R1).
if arch == "386" || arch == "amd64" {
return "flags := compare(" + formatValue(operands[0]) + ", " + formatValue(operands[1]) + ")"
}
return "flags := compare(" + formatValue(operands[1]) + ", " + formatValue(operands[0]) + ")"
}
func explainTest(operands []string) string {
if len(operands) < 2 {
return ""
}
return "flags := " + formatValue(operands[1]) + " & " + formatValue(operands[0])
}
func explainFMADD(operands []string) string {
if len(operands) < 4 {
return ""
}
destination := formatDestination(operands[3])
return destination + " := " + formatValue(operands[0]) + " * " + formatValue(operands[2]) + " + " + formatValue(operands[1])
}
// Go arm64 assembly orders MADD/MSUB as "Rm, Ra, Rn, Rd":
// MADD R2, R3, R1, R0 computes R0 = R1*R2 + R3.
func explainMADD(operands []string) string {
if len(operands) < 4 {
return ""
}
destination := formatDestination(operands[3])
return destination + " := " + formatValue(operands[2]) + " * " + formatValue(operands[0]) + " + " + formatValue(operands[1])
}
func explainMSUB(operands []string) string {
if len(operands) < 4 {
return ""
}
destination := formatDestination(operands[3])
return destination + " := " + formatValue(operands[1]) + " - " + formatValue(operands[2]) + " * " + formatValue(operands[0])
}
func explainLoad(operands []string) string {
if len(operands) < 2 {
return ""
}
return formatDestination(operands[len(operands)-1]) + " := " + formatValue(operands[0])
}
func explainStore(operands []string) string {
if len(operands) < 2 {
return ""
}
return "memory[" + formatAddress(operands[len(operands)-1]) + "] := " + formatValue(operands[0])
}
var plan9Address = regexp.MustCompile(`^([^()]*)\(([^)]+)\)(?:\(([^)]+)\))?$`)
func formatValue(operand string) string {
operand = strings.TrimSpace(operand)
if after, ok := strings.CutPrefix(operand, "$"); ok {
return after
}
if plan9Address.MatchString(operand) {
return "memory[" + formatAddress(operand) + "]"
}
return operand
}
func formatDestination(operand string) string {
operand = strings.TrimSpace(operand)
if plan9Address.MatchString(operand) {
return "memory[" + formatAddress(operand) + "]"
}
return operand
}
func formatAddress(operand string) string {
operand = strings.TrimSpace(strings.TrimPrefix(operand, "$"))
match := plan9Address.FindStringSubmatch(operand)
if len(match) == 0 {
return operand
}
displacement := strings.TrimSpace(match[1])
base := strings.TrimSpace(match[2])
index := strings.TrimSpace(match[3])
var terms []string
if base != "" && base != "SB" {
terms = append(terms, base)
}
if index != "" {
index = strings.ReplaceAll(index, "*", " * ")
terms = append(terms, index)
}
if displacement != "" && displacement != "0" {
terms = append(terms, displacement)
}
if len(terms) == 0 {
return displacement
}
return strings.Join(terms, " + ")
}