fix(parser): reject macro parameter lists the toolchain rejects

The C variadic spellings ("..." and the GNU "name..."), an empty or
trailing parameter, a missing comma, a stray token and an unterminated
list all parsed silently before: non-identifier tokens were skipped, so
"#define M(...)" defined a zero-parameter macro and invocations were
diagnosed only by argument count, if at all.  The toolchain rejects the
definition itself ("bad definition for macro"), and so does the
preprocessor now: the parameter list must be identifiers separated by
single commas and closed by the parenthesis, and a rejected definition
leaves the name unbound.  The spellings join the fuzz corpus.

Assisted-by: GLM 5.3
This commit is contained in:
petrbalvin committed 2026-10-07 14:04:15 +02:00
1 parent 373ec51061
commit 63ed141522
3 files changed
+102 -14

No files matched your search

+49
View File
@@ -321,6 +321,55 @@ func TestWrongArgumentCountIsAnError(t *testing.T) {
}
}
// TestBadMacroDefinitionsAreRejected pins the preprocessor's reading of a
// parameter list against the toolchain's: the list is identifiers separated
// by single commas and closed by the parenthesis. The C variadic spellings
// ("...", and the GNU "name...", one token here because the lexer reads dots
// as identifier characters), an empty or trailing parameter, a missing
// comma, a stray token and an unterminated list are all a bad definition,
// rejected at the definition exactly as the toolchain rejects them, so a use
// never silently binds to a parameter list the writer did not write.
func TestBadMacroDefinitionsAreRejected(t *testing.T) {
bad := map[string]string{
"ellipsis": "#define M(...) ADDQ $1, AX\n",
"ellipsis after a name": "#define M(a, ...) ADDQ a, AX\n",
"gnu dots": "#define M(args...) ADDQ args, AX\n",
"trailing comma": "#define M(a,) ADDQ a, AX\n",
"leading comma": "#define M(,a) ADDQ a, AX\n",
"missing comma": "#define M(a b) ADDQ a, AX\n",
"numeric parameter": "#define M(1) ADDQ $1, AX\n",
"unterminated parameter list": "#define M(a\n",
}
for name, def := range bad {
t.Run(name, func(t *testing.T) {
f, errs := ParseWithOptions("t_amd64.s", def+"TEXT ·f(SB), NOSPLIT, $0\n\tM(BX)\n\tRET\n", Options{Expand: true})
if f == nil {
t.Fatal("ParseWithOptions returned a nil file")
}
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "bad definition for macro M") {
t.Fatalf("got %v, want the definition rejected", errs)
}
})
}
// The plain shapes stay accepted, with the invocation checked against
// the parameter count the writer wrote.
for _, def := range []string{
"#define M() ADDQ $1, AX\n",
"#define M(a) ADDQ a, AX\n",
"#define M(a, b) ADDQ a, b\n",
} {
f, errs := ParseWithOptions("t_amd64.s", def+"TEXT ·f(SB), NOSPLIT, $0\n\tM(BX)\n\tRET\n", Options{Expand: true})
if f == nil || len(f.Decls) == 0 {
t.Fatalf("%s: unusable file", def)
}
for _, err := range errs {
if strings.Contains(err.Error(), "bad definition") {
t.Fatalf("%s: definition wrongly rejected: %v", def, errs)
}
}
}
}
func TestConditionalsSelectOneBranch(t *testing.T) {
_, got := expand(t, `
#define MODE2