fix(format): preserve macro adjacency and continuation bodies

The canonical spelling of a #define line glued a '(' to the macro name
whatever the input's spacing, turning an object macro whose body opens
with a parenthesis into a parameterised one, and it flattened the
backslash continuations of a multi-line body into one physical line,
fusing the statements the expansion splits at those boundaries.  Both
change what a valid file assembles to, so renderPreproc now keeps the
name's adjacency (the same column check the preprocessor applies) and
restores the continuation boundaries from the token positions.

Assisted-by: GLM 5.3
This commit is contained in:
petrbalvin committed 2026-10-07 13:54:42 +02:00
1 parent acd30088af
commit e600fc00e7
3 files changed
+202 -9

No files matched your search

+59 -7
View File
@@ -10,6 +10,7 @@ package format
import (
"strings"
"unicode/utf8"
"sourcedock.dev/petrbalvin/gasm-sdk/lexer"
"sourcedock.dev/petrbalvin/gasm-sdk/token"
@@ -288,24 +289,75 @@ func renderInstr(line []token.Token, width int) string {
// renderPreproc renders a preprocessor line such as #include "textflag.h".
func renderPreproc(line []token.Token) string {
// "#" directive [args]
// "#" include "header"
if len(line) >= 3 && line[1].Kind == token.Ident && line[1].Text == "include" &&
line[2].Kind == token.String {
// Whatever follows the header name is stray, but it is the file's
// stray text: it renders after the name rather than vanishing, so
// re-lexing the output sees exactly the tokens the input carried.
out := "#include " + line[2].Text
if rest := renderOps(line[3:]); rest != "" {
if rest := renderDirective(line[3:]); rest != "" {
out += " " + rest
}
return out
}
// The body of a directive, a macro definition included, is an ordinary
// token run: rendering it through renderOps applies the same punctuation
// rules as everywhere else, so a macro body keeps its canonical spelling
// ($v, (a, b), the ';' separators between statements) instead of being
// spread with a space between every token.
return "#" + renderOps(line[1:])
// token run: rendering it through renderDirective applies the same
// punctuation rules as everywhere else, so a macro body keeps its
// canonical spelling ($v, (a, b), the ';' separators between statements)
// instead of being spread with a space between every token.
return "#" + renderDirective(line[1:])
}
// renderDirective renders the token run of a preprocessor directive. Two
// distinctions of the macro language live in the source's layout and must
// survive the canonical spelling, because the expansion reads them back:
//
// - A '(' glued to a #define name makes the macro parameterised, while one
// separated by a space leaves it an object macro whose body happens to
// open with a parenthesis. The separator is decided by the input's
// adjacency, exactly the column check the preprocessor applies.
// - A backslash continuation the lexer spliced into the logical line is a
// statement boundary inside a macro body: the expansion splits a
// multi-line body at those boundaries, so flattening them would fuse two
// instructions into one operand list and change what the file assembles
// to. The boundary is restored from the token positions as " \\\n\t".
func renderDirective(toks []token.Token) string {
name := -1 // index of the macro name in a #define, else -1
if len(toks) >= 2 && toks[0].Kind == token.Ident && toks[0].Text == "define" &&
toks[1].Kind == token.Ident {
name = 1
}
var b strings.Builder
for i, t := range toks {
if i > 0 {
prev := toks[i-1]
spliced := t.Pos.Line > prev.Pos.Line
switch {
case spliced:
b.WriteString(" \\\n\t")
case i-1 == name && t.Kind == token.LParen &&
t.Pos.Column != prev.Pos.Column+utf8.RuneCountInString(prev.Text):
b.WriteByte(' ')
default:
// The operand punctuation rules, plus the guards that keep a
// tight spelling from re-lexing as something else.
sp := spaceBetween(prev, t)
if !sp && (t.Kind == token.Slash || t.Kind == token.Star) &&
strings.HasSuffix(b.String(), "/") {
sp = true
}
if !sp && wouldMerge(prev, t) {
sp = true
}
if sp {
b.WriteByte(' ')
}
}
}
b.WriteString(t.Text)
}
return b.String()
}
// renderOps re-spaces a run of operand tokens into canonical form. It never