From e307bf830fd6ef75888da87c0392b82fb777f143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 14 Sep 2026 18:22:00 +0200 Subject: [PATCH] fix(lint): guard unnamed TEXT and refresh the textflag table --- lint/analysis.go | 6 +++--- lint/lint.go | 43 ++++++++++++++++++++++--------------------- lint/liveness.go | 4 ++-- lint/registers.go | 2 +- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lint/analysis.go b/lint/analysis.go index aca3fbc..9553bda 100644 --- a/lint/analysis.go +++ b/lint/analysis.go @@ -12,8 +12,8 @@ import ( // checkFuncdata validates the structure of FUNCDATA and PCDATA directives, // which carry the GC stack-map information. The checks are deliberately -// shallow — they confirm the operands are well formed and that a literal index -// is within the small range the runtime uses — and never try to interpret a +// shallow, they confirm the operands are well formed and that a literal index +// is within the small range the runtime uses, and never try to interpret a // named index constant such as $PCDATA_StackMapIndex. func checkFuncdata(t *ast.Text, cfg Config) []Diagnostic { var out []Diagnostic @@ -89,7 +89,7 @@ func checkIndex(op *ast.Operand, directive string) []Diagnostic { if op.Imm.HasVal && (op.Imm.Val < 0 || op.Imm.Val > 10) { return []Diagnostic{{ Pos: op.Pos, Severity: Warning, Code: CodeFuncdata, - Message: fmt.Sprintf("%s index %d is outside the valid range 0–10", directive, op.Imm.Val), + Message: fmt.Sprintf("%s index %d is outside the valid range 0-10", directive, op.Imm.Val), }} } return nil diff --git a/lint/lint.go b/lint/lint.go index 8573826..74ffef2 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -10,6 +10,7 @@ package lint import ( "fmt" + "strconv" "strings" "sourcedock.dev/petrbalvin/gasm-devkit/arch" @@ -84,10 +85,12 @@ const ( CodeReservedRegister = "reserved-register-write" ) -// knownTextFlags are the flags recognised by the Go assembler's textflag.h. +// knownTextFlags are the flags recognised by runtime/textflag.h, plus the +// older REFLECTED spelling of REFLECTMETHOD. var knownTextFlags = map[string]bool{ "NOSPLIT": true, "DUPOK": true, "RODATA": true, "NOPROF": true, - "WRIT": true, "TLSBSS": true, "NOFRAME": true, "REFLECTED": true, + "NOPTR": true, "WRAPPER": true, "NEEDCTXT": true, "TLSBSS": true, + "NOFRAME": true, "REFLECTED": true, "REFLECTMETHOD": true, "TOPFRAME": true, "ABIWRAPPER": true, } @@ -172,8 +175,9 @@ func File(f *ast.File, cfg Config) []Diagnostic { } } for _, fl := range flags { - // Numeric flags (1, 8, 9) are legacy Go toolchain constants. - if fl >= "0" && fl <= "9" { + // Numeric flags are legacy textflag.h constants (1, 2, 8, + // 9, 10, …); their meaning is decided at assembly time. + if _, err := strconv.Atoi(fl); err == nil { continue } if !knownTextFlags[fl] { @@ -242,7 +246,7 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m // Unreachable code: a real instruction following a RET/UNDEF and // before any label, in a function whose control flow is fully - // resolvable. Only RET/UNDEF are treated as terminators here — an + // resolvable. Only RET/UNDEF are treated as terminators here, an // unconditional jump may be one entry of a hand-arranged branch // table (e.g. the generated callback tables), so it is not assumed // to make the following code dead. Pseudo-ops and macro invocations @@ -373,8 +377,10 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m } // Missing RET heuristic. Functions that invoke a macro are skipped: the - // macro body (opaque to us) may supply the RET. - if doLabelChecks && !cfg.Disable[CodeMissingRet] && instrCount > 0 && !hasRet && !lastTerminal && !hasMacro { + // macro body (opaque to us) may supply the RET. A TEXT whose symbol is + // missing (already reported by the parser) is skipped too. + if doLabelChecks && !cfg.Disable[CodeMissingRet] && t.Name != nil && + instrCount > 0 && !hasRet && !lastTerminal && !hasMacro { out = append(out, Diagnostic{ Pos: t.Keyword.Pos, Severity: Warning, @@ -506,7 +512,7 @@ func reachesRuntime(t *ast.Text) bool { } // usesFPArgs reports whether a function references its arguments through the FP -// pseudo-register — i.e. it uses the stack-based ABI0 layout, where the +// pseudo-register, i.e. it uses the stack-based ABI0 layout, where the // declared argument size must match the signature. func usesFPArgs(t *ast.Text) bool { for _, s := range t.Body { @@ -572,7 +578,7 @@ func isMacroInvocation(mnem string, macros map[string]bool) bool { // maskedEvex reports whether the instruction is a masked EVEX form: the // mnemonic carries a .Z suffix, or the operand list contains an opmask -// register (K1–K7). Either way the operand count differs from the unmasked +// register (K1-K7). Either way the operand count differs from the unmasked // form, so count checks are skipped. func maskedEvex(mnem string, ops []*ast.Operand) bool { if strings.Contains(mnem, ".") { @@ -587,7 +593,7 @@ func maskedEvex(mnem string, ops []*ast.Operand) bool { return false } -// isMaskReg reports whether name is an opmask register K0–K7. +// isMaskReg reports whether name is an opmask register K0-K7. func isMaskReg(name string) bool { return len(name) == 2 && name[0] == 'K' && name[1] >= '0' && name[1] <= '7' } @@ -702,11 +708,6 @@ func stackDelta(t *ast.Text, a arch.Arch) int64 { } case arch.ARM64: switch upper { - case "STP": - // STP with pre-index: STP Xt1, Xt2, [SP, #imm]! - if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) { - // Could be pre-index decrement; skip for simplicity. - } case "SUB": if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) { if in.Operands[1].Imm.HasVal { @@ -770,10 +771,10 @@ func isSPReg(op *ast.Operand, a arch.Arch) bool { // checkRegisterWidth detects amd64 register-width mismatches. The naming // truth of the Go assembler governs: AX, BX, CX, DX, SI, DI, BP, SP and -// R8–R15 ARE the 64-bit register names (there are no separate EAX/RAX -// spellings in go tool asm), and AL–DH are the byte forms. The width comes +// R8-R15 ARE the 64-bit register names (there are no separate EAX/RAX +// spellings in go tool asm), and AL-DH are the byte forms. The width comes // from the opcode suffix, so an L/W operation over a canonical 64-bit name is -// the normal, correct spelling — flagging it is pure noise on real kernels. +// the normal, correct spelling, flagging it is pure noise on real kernels. // What remains worth flagging: a Q (64-bit) operation over a narrower spelled // register (EAX under the gasm alias extension, or a byte form), and byte // registers in L/W operations. @@ -812,8 +813,8 @@ func checkRegisterWidth(mnem string, ops []*ast.Operand) string { } // amd64RegWidth returns the width in bytes of an amd64 register name under -// the Go assembler's naming model: the canonical word names (AX…SP, R8–R15) -// are 64-bit, AL–DH are the 8-bit forms, and the R/E-prefixed spellings are +// the Go assembler's naming model: the canonical word names (AX…SP, R8-R15) +// are 64-bit, AL-DH are the 8-bit forms, and the R/E-prefixed spellings are // the gasm alias extension with their intuitive widths. func amd64RegWidth(name string) int { switch name { @@ -833,7 +834,7 @@ func countRange(min, max int) string { if min == max { return fmt.Sprintf("%d operand(s)", min) } - return fmt.Sprintf("%d–%d operands", min, max) + return fmt.Sprintf("%d-%d operands", min, max) } // sortDiagnostics orders diagnostics by line, then column, then code. diff --git a/lint/liveness.go b/lint/liveness.go index 5d1c5f9..d94747c 100644 --- a/lint/liveness.go +++ b/lint/liveness.go @@ -370,7 +370,7 @@ func sameSet(a, b map[string]bool) bool { } // goFixedGPRs returns the general-purpose registers the Go ABI designates as -// fixed across calls — the ones hand-written assembly must not permanently +// fixed across calls, the ones hand-written assembly must not permanently // clobber. This follows cmd/compile/abi-internal.md, not the platform ABI: // Go's stack-based ABI0 (which hand-written assembly uses) has no System V // style callee-saved registers, so clobbering the argument and scratch @@ -414,7 +414,7 @@ func gprSet(names ...string) map[string]bool { // without also saving and restoring them. The first result lists registers // whose loss is never safe; the second lists the goroutine-pointer class, // whose loss is reported only when reachesRuntime is true (a non-NOSPLIT -// function, or one that makes calls — the ABI0 transition machinery restores +// function, or one that makes calls, the ABI0 transition machinery restores // the g pointer only on such paths). func clobberedGoFixed(l *liveness, a arch.Arch, reachesRuntime bool) (always, runtime []string) { alwaysSet, runtimeSet := goFixedGPRs(a) diff --git a/lint/registers.go b/lint/registers.go index 7f28357..0c98bdf 100644 --- a/lint/registers.go +++ b/lint/registers.go @@ -22,7 +22,7 @@ var amd64WordNames = map[string]string{ // nonportableRegister maps a gasm-only register alias to the canonical go // tool asm spelling. R or E followed by a canonical word name is the only -// alias family; R8–R15 are already canonical. +// alias family; R8-R15 are already canonical. func nonportableRegister(name string) (string, bool) { up := strings.ToUpper(name) if len(up) != 3 {