From f452b8a995fb8a3a90836061a5f3d033b5d4e6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 13:12:35 +0200 Subject: [PATCH] fix(format): keep a macro-named label line whole A label whose name is a macro expands into whatever the body is, so the line's statement structure exists only after expansion; splitting the label off changed clean input into a different statement sequence. The formatter now records the names #define'd above each line and renders such a label line unsplit, and the crashing input joins the corpus. Assisted-by: GLM 5.3 --- format/expansion_test.go | 20 +++++++++++ format/format.go | 35 ++++++++++++++++--- .../3508c784782f0866 | 2 ++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 format/testdata/fuzz/FuzzFormatExpansionRoundTrip/3508c784782f0866 diff --git a/format/expansion_test.go b/format/expansion_test.go index 32e2600..3a200c1 100644 --- a/format/expansion_test.go +++ b/format/expansion_test.go @@ -95,6 +95,26 @@ func TestFormatPreservesExpansionSemantics(t *testing.T) { "\tM(1, R0)\n" + "\tRET\n", }, + { + // A label whose name is a macro: the expansion decides what the + // line becomes (here an instruction with several operands), so + // the canonical form keeps the line whole instead of splitting + // the label off, which would change the statement structure. + name: "label named as a macro", + src: "#define M B0, C0\n" + + "TEXT ·f(SB), $0\n" + + "\tM: A00\n" + + "\tRET\n", + }, + { + // A use before the define: the name is not a macro yet at that + // point of the file, so the label splits as any other does. + name: "label before the define of its name", + src: "TEXT ·f(SB), $0\n" + + "\tM: A00\n" + + "\tRET\n" + + "#define M B0, C0\n", + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/format/format.go b/format/format.go index 882292c..91f9675 100644 --- a/format/format.go +++ b/format/format.go @@ -24,14 +24,16 @@ func Source(src string) string { // index of the TEXT function it belongs to, so that mnemonic widths can be // aligned per function. type info struct { - kind int - mnemLen int - funcID int + kind int + mnemLen int + funcID int + macroName bool // the line's label names a macro defined above it } infos := make([]info, len(lines)) funcID := -1 - maxWidth := map[int]int{} // funcID -> widest mnemonic + maxWidth := map[int]int{} // funcID -> widest mnemonic + macros := map[string]bool{} // names #define'd so far, in file order for i, line := range lines { inf := info{kind: kBlank, funcID: funcID} if len(line) > 0 { @@ -45,6 +47,10 @@ func Source(src string) string { inf.kind = kComment case line[0].Kind == token.Hash: inf.kind = kPreproc + if len(line) >= 3 && line[1].Kind == token.Ident && + line[1].Text == "define" && line[2].Kind == token.Ident { + macros[line[2].Text] = true + } case line[0].Kind == token.Ident && isDirective(line[0].Text): inf.kind = kDirective if line[0].Text == "TEXT" { @@ -56,6 +62,14 @@ func Source(src string) string { } case len(line) >= 2 && line[1].Kind == token.Colon: inf.kind = kLabel + if macros[line[0].Text] { + // 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. + inf.macroName = true + infos[i] = inf + continue + } // Peel stacked labels exactly as the render pass does; the // instruction after the last one is rendered at the // function's alignment width, so its mnemonic counts here. @@ -115,6 +129,19 @@ func Source(src string) string { } inBody = line[0].Text == "TEXT" case kLabel: + if inf.macroName && len(line) > 2 { + // A label that names a macro expands into whatever the body + // is: after expansion the line may not be a label at all, so + // its statement structure exists only as one line. The label + // keeps the canonical colon and everything behind it renders + // as one unaligned run, unsplit. + out := line[0].Text + ":" + if rest := renderOps(line[2:]); rest != "" { + out += " " + rest + } + outs = append(outs, outLine{kind: kLabel, text: trimLineRight(out, last)}) + continue + } // Every label, and a trailing instruction, becomes its own // output line: separate outLines keep the blank-line pass // honest about what it is looking at. diff --git a/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/3508c784782f0866 b/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/3508c784782f0866 new file mode 100644 index 0000000..155e0da --- /dev/null +++ b/format/testdata/fuzz/FuzzFormatExpansionRoundTrip/3508c784782f0866 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("#define A B0\"\nA:A00")