From 373ec51061e9417aec1294ee74169f0af732d5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 13:35:47 +0200 Subject: [PATCH] fix(format): treat a selector-folded label as naming its macro The lexer folds NAME.selector into one identifier, and an object macro reached through the selector expands with it travelling along, so a label spelled NAME.selector: restructures exactly like the bare name would. The macro-name checks now resolve the prefix before the first period, and the crashing input joins the corpus. Assisted-by: GLM 5.3 --- format/expansion_test.go | 11 ++++++++ format/format.go | 25 +++++++++++++++---- .../ea7fb86b86361801 | 2 ++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 format/testdata/fuzz/FuzzFormatExpansionRoundTrip/ea7fb86b86361801 diff --git a/format/expansion_test.go b/format/expansion_test.go index 5b4ee12..f92c107 100644 --- a/format/expansion_test.go +++ b/format/expansion_test.go @@ -127,6 +127,17 @@ func TestFormatPreservesExpansionSemantics(t *testing.T) { "\tA: M:\n" + "\tRET\n", }, + { + // A label reached through a selector: the lexer folds NAME and + // .selector into one identifier, and the object macro behind the + // name expands with the selector travelling along, so the folded + // spelling restructures exactly like the bare name. + name: "label naming a macro through a selector", + src: "#define A B\n" + + "TEXT ·f(SB), $0\n" + + "\tA.: A0\n" + + "\tRET\n", + }, { // A use above its define: the name is not a macro at that point // of the file, so splitting would be safe; the whole-file set diff --git a/format/format.go b/format/format.go index c6abfe2..fc84560 100644 --- a/format/format.go +++ b/format/format.go @@ -58,7 +58,7 @@ func Source(src string) string { } case len(line) >= 2 && line[1].Kind == token.Colon: inf.kind = kLabel - if macros[line[0].Text] { + if namesMacro(line[0].Text, macros) { // The label names a macro, so what the line becomes // exists only after expansion; the line must not be // restructured, and no mnemonic of it aligns. @@ -74,11 +74,11 @@ func Source(src string) string { // either: its expansion decides what the line becomes. rest := line[2:] for len(rest) >= 2 && rest[0].Kind == token.Ident && rest[1].Kind == token.Colon && - !isDirective(rest[0].Text) && !macros[rest[0].Text] { + !isDirective(rest[0].Text) && !namesMacro(rest[0].Text, macros) { rest = rest[2:] } if len(rest) > 0 && rest[0].Kind == token.Ident && !isDirective(rest[0].Text) && - !macros[rest[0].Text] { + !namesMacro(rest[0].Text, macros) { inf.mnemLen = len(rest[0].Text) if funcID >= 0 && inf.mnemLen > maxWidth[funcID] { maxWidth[funcID] = inf.mnemLen @@ -150,7 +150,7 @@ func Source(src string) string { // A stacked label naming a macro is never peeled: its expansion // decides what the line becomes, exactly as the first label's. for len(rest) >= 2 && rest[0].Kind == token.Ident && rest[1].Kind == token.Colon && - !isDirective(rest[0].Text) && !macros[rest[0].Text] { + !isDirective(rest[0].Text) && !namesMacro(rest[0].Text, macros) { outs = append(outs, outLine{kind: kLabel, text: rest[0].Text + ":"}) rest = rest[2:] } @@ -165,7 +165,7 @@ func Source(src string) string { // parser wants a symbol per line), so a directive sharing // the label's line stays there. outs[len(outs)-1].text += " " + trimLineRight(renderOps(rest), rest[len(rest)-1]) - } else if len(rest) > 0 && rest[0].Kind == token.Ident && !macros[rest[0].Text] { + } else if len(rest) > 0 && rest[0].Kind == token.Ident && !namesMacro(rest[0].Text, macros) { outs = append(outs, outLine{kind: kInstr, text: trimLineRight(renderInstr(rest, maxWidth[inf.funcID]), rest[len(rest)-1])}) if strings.EqualFold(rest[0].Text, "RET") { inBody = false @@ -208,6 +208,21 @@ func macroNames(lines [][]token.Token) map[string]bool { return names } +// namesMacro reports whether text names a macro from names, directly or +// through the selector suffix the lexer folds into one identifier: an object +// macro reached as "NAME.selector" expands with the selector travelling +// along, so such a label restructures exactly like the bare name. The check +// ignores the object-and-parameterised distinction and stays conservative. +func namesMacro(text string, names map[string]bool) bool { + if names[text] { + return true + } + if j := strings.IndexByte(text, '.'); j > 0 { + return names[text[:j]] + } + return false +} + // trimLineRight removes trailing spaces and tabs from a rendered line, which // are layout the canonical form drops. The trim never eats into the text of // the line's final token: a string or rune literal may legitimately end in diff --git a/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/ea7fb86b86361801 b/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/ea7fb86b86361801 new file mode 100644 index 0000000..946f6bd --- /dev/null +++ b/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/ea7fb86b86361801 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("#define A B\"\nA.:A0")