fix(parser): bound folding depth and macro expansion work

Assisted-by: GLM 5.3
This commit is contained in:
2026-10-02 00:40:20 +02:00
parent 6570709226
commit ca887d3927
4 changed files with 343 additions and 78 deletions
+59
View File
@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
)
@@ -58,6 +59,64 @@ func wantLines(t *testing.T, got []string, want ...string) {
}
}
// TestManyInvocationsExpandInLinearTime pins the cost contract of macro
// expansion: a long run of invocations costs work proportional to what it
// produces. Splicing each expansion into one flat slice instead made every
// invocation shift the whole remaining tail, so the 200000 invocations here
// took minutes; the pending stream is a stack of frames since, measured at
// roughly 120 ms on this machine, and ten seconds stays a generous ceiling
// even on a runner an order of magnitude slower.
func TestManyInvocationsExpandInLinearTime(t *testing.T) {
const n = 200000
src := "#define A MOVQ AX, BX\nTEXT ·f(SB), NOSPLIT, $0\n\t" +
strings.Repeat("A\n", n) + "\tRET\n"
start := time.Now()
f, errs := ParseWithOptions("t_amd64.s", src, Options{Expand: true})
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
var ts []*ast.Text
for _, d := range f.Decls {
if text, ok := d.(*ast.Text); ok {
ts = append(ts, text)
}
}
if len(ts) != 1 {
t.Fatalf("got %d TEXT declarations, want 1", len(ts))
}
if len(ts[0].Body) != n+1 {
t.Fatalf("got %d body statements, want %d", len(ts[0].Body), n+1)
}
if instr, ok := ts[0].Body[0].(*ast.Instr); !ok || instr.Mnemonic.Text != "MOVQ" {
t.Fatalf("first body statement is not the expansion: %#v", ts[0].Body[0])
}
if elapsed := time.Since(start); elapsed > 10*time.Second {
t.Fatalf("expansion of %d invocations took %v, want the linear-cost contract", n, elapsed)
}
}
// TestAmplifyingMacrosStopAtBudget pins the work budget of expansion: a body
// that repeats its argument multiplies every level of nesting, so a short
// line of nested invocations names exponentially many tokens. The expansion
// must stop at the budget with a diagnostic instead of committing the
// machine to producing them.
func TestAmplifyingMacrosStopAtBudget(t *testing.T) {
src := "#define A(x) x x x x x x x x x x\n" +
"A(A(A(A(A(A(A(A(A(A(1))))))))))\n"
f, errs := ParseWithOptions("t_amd64.s", src, Options{Expand: true})
if len(errs) == 0 {
t.Fatal("amplifying macro expansion reported no error")
}
if !strings.Contains(errs[0].Error(), "work budget") {
t.Fatalf("error is not the budget diagnostic: %v", errs[0])
}
for _, d := range f.Decls {
if _, ok := d.(*ast.Text); ok {
t.Fatal("amplified line produced a TEXT declaration")
}
}
}
func TestObjectMacroExpandsAtUse(t *testing.T) {
_, got := expand(t, `
#define REGTMP CX