From a382e663e8427820140e7d58ff4e2d03cb0a4bf4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 12 Aug 2026 10:16:56 +0800 Subject: [PATCH 1/7] chore/litgen: update failing checks in place --- chore/litgen/litgen.go | 6 +- chore/litgen/rewrite.go | 252 +++++++++++++++++++++++++++++++++-- chore/litgen/rewrite_test.go | 205 +++++++++++++++++++++++++++- 3 files changed, 443 insertions(+), 20 deletions(-) diff --git a/chore/litgen/litgen.go b/chore/litgen/litgen.go index 34d51fe437..803d0ed1f9 100644 --- a/chore/litgen/litgen.go +++ b/chore/litgen/litgen.go @@ -26,6 +26,8 @@ import ( "github.com/goplus/llgo/xtool/env/llvm" ) +var update = flag.Bool("update", false, "update only failing existing CHECK groups in place") + func main() { llvm.SetupPath() flag.Usage = func() { @@ -69,7 +71,7 @@ func processPath(path string) error { return err } fmt.Fprintln(os.Stderr, "litgen", target.sourceFile) - return generateFile(target) + return generateFile(target, *update) } func processTree(root string) error { @@ -106,7 +108,7 @@ func processTree(root string) error { } for _, target := range targets { fmt.Fprintln(os.Stderr, "litgen", target.sourceFile) - if err := generateFile(target); err != nil { + if err := generateFile(target, *update); err != nil { return err } } diff --git a/chore/litgen/rewrite.go b/chore/litgen/rewrite.go index 6a3eeff002..0f84995467 100644 --- a/chore/litgen/rewrite.go +++ b/chore/litgen/rewrite.go @@ -31,6 +31,7 @@ import ( "sort" "strings" + "github.com/goplus/llgo/internal/filecheck" "github.com/goplus/llgo/internal/llgen" "github.com/goplus/mod" "golang.org/x/mod/modfile" @@ -60,27 +61,61 @@ type irFunction struct { } var ( - defineQuotedRE = regexp.MustCompile(`^define\b.* @"([^"]+)"\(`) - definePlainRE = regexp.MustCompile(`^define\b.* @([^\s(]+)\(`) - globalQuotedRE = regexp.MustCompile(`^@"([^"]+)"\s*=`) - globalPlainRE = regexp.MustCompile(`^@([A-Za-z0-9$._-]+)\s*=`) - globalRefRE = regexp.MustCompile(`@"([^"]+)"|@([A-Za-z0-9$._-]+)`) - checkLineRE = regexp.MustCompile(`^\s*//\s*CHECK(?:-[A-Z]+)?:`) - debugMetaRE = regexp.MustCompile(`, ![A-Za-z0-9_.-]+ ![0-9]+`) - closureEnvRE = regexp.MustCompile(`(\s)(?:nest|swiftself)(\s)`) - numericNameRE = regexp.MustCompile(`^\d+$`) + defineQuotedRE = regexp.MustCompile(`^define\b.* @"([^"]+)"\(`) + definePlainRE = regexp.MustCompile(`^define\b.* @([^\s(]+)\(`) + globalQuotedRE = regexp.MustCompile(`^@"([^"]+)"\s*=`) + globalPlainRE = regexp.MustCompile(`^@([A-Za-z0-9$._-]+)\s*=`) + globalRefRE = regexp.MustCompile(`@"([^"]+)"|@([A-Za-z0-9$._-]+)`) + checkLineRE = regexp.MustCompile(`^\s*//\s*CHECK(?:-[A-Z]+)?:`) + debugMetaRE = regexp.MustCompile(`, ![A-Za-z0-9_.-]+ ![0-9]+`) + closureEnvRE = regexp.MustCompile(`(\s)(?:nest|swiftself)(\s)`) + testCasePathRE = regexp.MustCompile(`"[^"]*/cl/_test[^/"]*/[^/".]+`) + symbolHashRE = regexp.MustCompile(`\$[-A-Za-z0-9_]{43}`) + cgoHashRE = regexp.MustCompile(`(_cgo_)[0-9a-f]+(_Cfunc_)`) + numericGlobalRE = regexp.MustCompile(`@\d+\b`) + metadataIDRE = regexp.MustCompile(`!\d+\b`) + sigJumpRE = regexp.MustCompile(`@(?:__)?(sig(?:set|long)jmp)\b`) + plainJumpRE = regexp.MustCompile(`@_*((?:set|long)jmp)\b`) + jmpBufAllocaRE = regexp.MustCompile(`alloca i8, i64 (?:196|200), align 1`) + numericNameRE = regexp.MustCompile(`^\d+$`) ) -func generateFile(target resolvedTarget) error { +type pthreadOpaqueSize struct { + typeName string + sizes *regexp.Regexp + want string +} + +var pthreadOpaqueSizes = []pthreadOpaqueSize{ + {"MutexAttr", regexp.MustCompile(`\[(?:4|8|16) x i8\]`), `[{{(4|8|16)}} x i8]`}, + {"RWLockAttr", regexp.MustCompile(`\[(?:8|16|24) x i8\]`), `[{{(8|16|24)}} x i8]`}, + {"CondAttr", regexp.MustCompile(`\[(?:4|8|16) x i8\]`), `[{{(4|8|16)}} x i8]`}, + {"Once", regexp.MustCompile(`\[(?:4|16) x i8\]`), `[{{(4|16)}} x i8]`}, + {"Mutex", regexp.MustCompile(`\[(?:40|48|64) x i8\]`), `[{{(40|48|64)}} x i8]`}, + {"RWLock", regexp.MustCompile(`\[(?:56|192|200) x i8\]`), `[{{(56|192|200)}} x i8]`}, + {"Cond", regexp.MustCompile(`\[(?:40|48) x i8\]`), `[{{(40|48)}} x i8]`}, +} + +func generateFile(target resolvedTarget, update bool) error { data, err := os.ReadFile(target.sourceFile) if err != nil { return err } - cleaned := stripCheckDirectives(string(data)) ir, err := genIR(target.genTarget) if err != nil { return err } + if update { + updated, changed, err := updateSourceChecks(string(data), target.sourceFile, target.pkgPath, target.modulePath, ir) + if err != nil { + return err + } + if !changed { + return nil + } + return writeFileAtomically(target.sourceFile, []byte(updated), 0644) + } + cleaned := stripCheckDirectives(string(data)) updated, err := rewriteSource(cleaned, target.sourceFile, target.pkgPath, target.modulePath, ir) if err != nil { return err @@ -92,6 +127,174 @@ func generateFile(target resolvedTarget) error { return writeFileAtomically(target.sourceFile, formatted, 0644) } +type sourceEdit struct { + start int + end int + text string +} + +type checkGroup struct { + start int + end int + text string +} + +// updateSourceChecks preserves every CHECK group that still matches. A +// failing function group is regenerated at the group's existing byte range, +// so updating a golden does not move unrelated checks or add new functions. +func updateSourceChecks(src, srcPath, _, modulePath, ir string) (string, bool, error) { + if err := matchCheckText(src, ir); err == nil { + return src, false, nil + } + prog := parseIR(ir) + + var edits []sourceEdit + var currentFn *irFunction + for _, group := range sourceCheckGroups(src) { + fn, hasDefinition, err := findFunctionForCheckGroup(group.text, prog.funcs) + if err != nil { + return "", false, fmt.Errorf("%s: %w", srcPath, err) + } + if hasDefinition { + currentFn = &fn + } + if err := matchCheckText(group.text, ir); err == nil { + continue + } + if !hasDefinition { + if currentFn == nil { + return "", false, fmt.Errorf("%s: failing CHECK group has no preceding function definition", srcPath) + } + fn = *currentFn + } + lines := buildFunctionChecks(fn, modulePath) + if len(lines) == 0 { + return "", false, fmt.Errorf("%s: no checks generated for %q", srcPath, fn.symbol) + } + if hasDefinition { + // Keep the existing definition directive byte-for-byte. It may be + // intentionally looser than newly generated checks. + lines[0] = firstDefinitionCheck(group.text) + } else { + lines = lines[1:] + } + indent := indentAt(src, group.start) + text := formatDirectiveBlock(indent, lines) + text = preserveTrailingNewlines(text, group.text) + edits = append(edits, sourceEdit{start: group.start, end: group.end, text: text}) + } + if len(edits) == 0 { + return "", false, fmt.Errorf("%s: existing CHECKs fail, but no function CHECK group can be updated", srcPath) + } + sort.Slice(edits, func(i, j int) bool { return edits[i].start > edits[j].start }) + updated := src + for _, edit := range edits { + updated = updated[:edit.start] + edit.text + updated[edit.end:] + } + if err := matchCheckText(updated, ir); err != nil { + return "", false, fmt.Errorf("%s: updated CHECKs still fail: %w", srcPath, err) + } + return updated, updated != src, nil +} + +func firstDefinitionCheck(group string) string { + for _, line := range strings.Split(group, "\n") { + if strings.Contains(line, "define ") { + return strings.TrimLeft(line, " \t") + } + } + return "" +} + +func preserveTrailingNewlines(generated, original string) string { + want := len(original) - len(strings.TrimRight(original, "\n")) + return strings.TrimRight(generated, "\n") + strings.Repeat("\n", want) +} + +func sourceCheckGroups(src string) []checkGroup { + lineStarts := []int{0} + for i := 0; i < len(src); i++ { + if src[i] == '\n' { + lineStarts = append(lineStarts, i+1) + } + } + var groups []checkGroup + for line := 0; line < len(lineStarts); { + lineEnd := len(src) + if line+1 < len(lineStarts) { + lineEnd = lineStarts[line+1] + } + if !checkLineRE.MatchString(strings.TrimRight(src[lineStarts[line]:lineEnd], "\r\n")) { + line++ + continue + } + startLine := line + for line < len(lineStarts) { + lineEnd = len(src) + if line+1 < len(lineStarts) { + lineEnd = lineStarts[line+1] + } + if !checkLineRE.MatchString(strings.TrimRight(src[lineStarts[line]:lineEnd], "\r\n")) { + break + } + line++ + } + start := lineStarts[startLine] + end := len(src) + if line < len(lineStarts) { + end = lineStarts[line] + } + groups = append(groups, checkGroup{start: start, end: end, text: src[start:end]}) + } + return groups +} + +func findFunctionForCheckGroup(group string, funcs []irFunction) (irFunction, bool, error) { + var definitionCheck string + for _, line := range strings.Split(group, "\n") { + idx := strings.Index(line, "define ") + if idx < 0 { + continue + } + definitionCheck = "// CHECK: " + line[idx:] + "\n" + break + } + if definitionCheck == "" { + return irFunction{}, false, nil + } + var matched *irFunction + for i := range funcs { + if len(funcs[i].lines) == 0 || matchCheckText(definitionCheck, strings.Join(funcs[i].lines, "\n")) != nil { + continue + } + if matched != nil { + return irFunction{}, false, fmt.Errorf("function CHECK matches both %q and %q", matched.symbol, funcs[i].symbol) + } + matched = &funcs[i] + } + if matched == nil { + return irFunction{}, false, fmt.Errorf("function CHECK does not match current IR: %s", strings.TrimSpace(definitionCheck)) + } + return *matched, true, nil +} + +func matchCheckText(checks, ir string) error { + tmp, err := os.CreateTemp("", "llgo-litgen-check-*.go") + if err != nil { + return err + } + path := tmp.Name() + defer os.Remove(path) + if _, err := tmp.WriteString(checks); err != nil { + _ = tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return filecheck.Match(path, ir) +} + func resolveTarget(sourceFile, genTarget string) (resolvedTarget, error) { pkgDir := filepath.Dir(sourceFile) root, goMod, err := mod.FindGoMod(pkgDir) @@ -467,19 +670,42 @@ func generalizeDefineLine(line, modulePath string) string { line = head[:sigEnd+1] + "{{.*}}" + line[idx:] } } - return generalizeModulePath(line, modulePath) + return generalizeSymbolPaths(line, modulePath) } func generalizeIRLine(line, modulePath string) string { - return generalizeModulePath(scrubIRLine(line), modulePath) + return generalizeSymbolPaths(scrubIRLine(line), modulePath) } func scrubIRLine(line string) string { line = debugMetaRE.ReplaceAllString(line, "") line = generalizeClosureEnvAttrs(line) + line = symbolHashRE.ReplaceAllString(line, `$${{[-A-Za-z0-9_]+}}`) + line = cgoHashRE.ReplaceAllString(line, `${1}{{[0-9a-f]+}}${2}`) + line = numericGlobalRE.ReplaceAllString(line, `@{{[0-9]+}}`) + line = metadataIDRE.ReplaceAllString(line, `!{{[0-9]+}}`) + line = generalizePlatformIR(line) + line = strings.ReplaceAll(line, "[[", `{{\[\[}}`) return strings.TrimRight(line, " \t") } +func generalizeSymbolPaths(line, modulePath string) string { + line = testCasePathRE.ReplaceAllString(line, `"{{.*}}`) + return generalizeModulePath(line, modulePath) +} + +func generalizePlatformIR(line string) string { + line = sigJumpRE.ReplaceAllString(line, `@{{(__)?}}${1}`) + line = plainJumpRE.ReplaceAllString(line, `@{{_*}}${1}`) + line = jmpBufAllocaRE.ReplaceAllString(line, `alloca i8, i64 {{(196|200)}}, align 1`) + for _, opaque := range pthreadOpaqueSizes { + if strings.Contains(line, "/runtime/internal/clite/pthread/sync."+opaque.typeName+`"`) { + return opaque.sizes.ReplaceAllString(line, opaque.want) + } + } + return line +} + func generalizeClosureEnvAttrs(line string) string { var b strings.Builder start := 0 diff --git a/chore/litgen/rewrite_test.go b/chore/litgen/rewrite_test.go index c81afe563a..a96bb664f0 100644 --- a/chore/litgen/rewrite_test.go +++ b/chore/litgen/rewrite_test.go @@ -159,16 +159,16 @@ _llgo_0: if err != nil { t.Fatal(err) } - if !strings.Contains(got, `// CHECK: {{^}}@0 = private unnamed_addr constant [4 x i8] c"Hi\0A\00", align 1{{$}}`) { + if !strings.Contains(got, `// CHECK: {{^}}@{{[0-9]+}} = private unnamed_addr constant [4 x i8] c"Hi\0A\00", align 1{{$}}`) { t.Fatalf("missing numeric global @0:\n%s", got) } - if !strings.Contains(got, `// CHECK: {{^}}@1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1{{$}}`) { + if !strings.Contains(got, `// CHECK: {{^}}@{{[0-9]+}} = private unnamed_addr constant [3 x i8] c"%s\00", align 1{{$}}`) { t.Fatalf("missing numeric global @1:\n%s", got) } if strings.Contains(got, `// CHECK: {{^}}@"{{.*}}/p.named" = global i64 1{{$}}`) { t.Fatalf("named globals should not be emitted by default:\n%s", got) } - if strings.Index(got, `// CHECK: {{^}}@0 = private unnamed_addr constant [4 x i8] c"Hi\0A\00", align 1{{$}}`) > strings.Index(got, "func main()") { + if strings.Index(got, `// CHECK: {{^}}@{{[0-9]+}} = private unnamed_addr constant [4 x i8] c"Hi\0A\00", align 1{{$}}`) > strings.Index(got, "func main()") { t.Fatalf("global checks should be placed before first declaration:\n%s", got) } } @@ -207,14 +207,14 @@ package main import _ "unsafe" -// CHECK: {{^}}@0 = private unnamed_addr constant [4 x i8] c"sqrt"{{$}} +// CHECK: {{^}}@{{[0-9]+}} = private unnamed_addr constant [4 x i8] c"sqrt"{{$}} //go:linkname cSqrt C.sqrt func cSqrt(float64) float64 // CHECK-LABEL: define double @"{{.*}}/p.callSqrt"(double %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(ptr @0) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(ptr @{{[0-9]+}}) // CHECK-NEXT: %1 = call double @sqrt(double %0) // CHECK-NEXT: ret double %1 // CHECK-NEXT: } @@ -406,3 +406,198 @@ func TestGeneralizeModulePath_IgnoresEscapedQuotes(t *testing.T) { t.Fatalf("generalizeModulePath = %q, want %q", got, want) } } + +func TestGeneralizeSymbolPaths_WildcardsTestCasePrefix(t *testing.T) { + tests := []struct { + line string + want string + }{ + { + `define void @"github.com/goplus/llgo/cl/_testgo/deferfn.A"() {`, + `define void @"{{.*}}.A"() {`, + }, + { + `call void @"github.com/goplus/llgo/cl/_testgo/deferfn/foo.B"()`, + `call void @"{{.*}}/foo.B"()`, + }, + { + `call void @"github.com/goplus/llgo/runtime.Start"()`, + `call void @"{{.*}}/runtime.Start"()`, + }, + } + for _, test := range tests { + if got := generalizeSymbolPaths(test.line, "github.com/goplus/llgo"); got != test.want { + t.Errorf("generalizeSymbolPaths(%q) = %q, want %q", test.line, got, test.want) + } + } +} + +func TestGeneralizePlatformIR(t *testing.T) { + tests := []struct { + line string + want string + }{ + {`%0 = call i32 @__sigsetjmp(ptr %buf, i32 0)`, `%0 = call i32 @{{(__)?}}sigsetjmp(ptr %buf, i32 0)`}, + {`call void @siglongjmp(ptr %buf, i32 1)`, `call void @{{(__)?}}siglongjmp(ptr %buf, i32 1)`}, + {`%0 = call i32 @_setjmp(ptr %buf)`, `%0 = call i32 @{{_*}}setjmp(ptr %buf)`}, + {`call void @longjmp(ptr %buf, i32 1)`, `call void @{{_*}}longjmp(ptr %buf, i32 1)`}, + {`%0 = alloca i8, i64 196, align 1`, `%0 = alloca i8, i64 {{(196|200)}}, align 1`}, + { + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.Mutex" { [64 x i8] zeroinitializer }, ptr %0`, + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.Mutex" { [{{(40|48|64)}} x i8] zeroinitializer }, ptr %0`, + }, + } + for _, test := range tests { + if got := generalizePlatformIR(test.line); got != test.want { + t.Errorf("generalizePlatformIR(%q) = %q, want %q", test.line, got, test.want) + } + } +} + +func TestGeneralizeIRLine_WildcardsUnstableIDs(t *testing.T) { + line := ` call void @0(ptr @19, ptr @"_llgo_closure$QIHBTaw1IFobr8yvWpq-2AJFm3xBNhdW_aNBicqUBGk"), !dbg !42` + got := generalizeIRLine(line, "") + want := ` call void @{{[0-9]+}}(ptr @{{[0-9]+}}, ptr @"_llgo_closure${{[-A-Za-z0-9_]+}}")` + if got != want { + t.Fatalf("generalizeIRLine() = %q, want %q", got, want) + } +} + +func TestGeneralizeIRLine_EscapesFileCheckSyntaxAndCgoHash(t *testing.T) { + line := ` %0 = load ptr, ptr @main._cgo_52352d07b8a3_Cfunc_free, align 8 ; map[[2]int]` + got := generalizeIRLine(line, "") + want := ` %0 = load ptr, ptr @main._cgo_{{[0-9a-f]+}}_Cfunc_free, align 8 ; map{{\[\[}}2]int]` + if got != want { + t.Fatalf("generalizeIRLine() = %q, want %q", got, want) + } +} + +func TestUpdateSourceChecks_UpdatesOnlyFailingGroupInPlace(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK-LABEL: define void @"example.com/p.good"(){{.*}} { +// CHECK-NEXT: entry: +// CHECK-NEXT: ret void +// CHECK-NEXT: } +func good() {} + +// CHECK-LABEL: define i64 @"example.com/p.changed"(ptr %0){{.*}} { +// CHECK-NEXT: entry: +// CHECK-NEXT: %1 = load i64, ptr %0 +// CHECK-NEXT: ret i64 %1 +func changed(*int) int { return 0 } +` + const ir = `define void @"example.com/p.good"() { +entry: + ret void +} + +define i64 @"example.com/p.changed"(ptr %0) { +entry: + %nilcheck = icmp eq ptr %0, null + br i1 %nilcheck, label %panic, label %cont + +panic: + unreachable + +cont: + %1 = load i64, ptr %0 + ret i64 %1 +} +` + got, changed, err := updateSourceChecks(src, "in.go", "example.com/p", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if !changed { + t.Fatal("updateSourceChecks reported no change") + } + good := src[strings.Index(src, `// CHECK-LABEL: define void`):strings.Index(src, `func good()`)] + if !strings.Contains(got, good) { + t.Fatalf("passing CHECK group changed:\n%s", got) + } + if strings.Index(got, `// CHECK-LABEL: define i64`) > strings.Index(got, `func changed(`) { + t.Fatalf("updated CHECK group moved after its declaration:\n%s", got) + } + if !strings.Contains(got, `%nilcheck = icmp eq ptr %0, null`) { + t.Fatalf("updated CHECK group missing changed IR:\n%s", got) + } +} + +func TestUpdateSourceChecks_UpdatesBodyGroupInsideFunction(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK-LABEL: define i64 @"example.com/p.changed"(ptr %0){{.*}} { +func changed(*int) int { + // CHECK-NEXT: entry: + // CHECK-NEXT: %1 = load i64, ptr %0 + // CHECK-NEXT: ret i64 %1 + // CHECK-NEXT: } + return 0 +} +` + const ir = `define i64 @"example.com/p.changed"(ptr %0) { +entry: + %nilcheck = icmp eq ptr %0, null + br i1 %nilcheck, label %panic, label %cont + +panic: + unreachable + +cont: + %1 = load i64, ptr %0 + ret i64 %1 +} +` + got, changed, err := updateSourceChecks(src, "in.go", "example.com/p", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if !changed || !strings.Contains(got, "\t// CHECK-NEXT: %nilcheck = icmp eq ptr %0, null") { + t.Fatalf("body CHECK group was not updated in place:\n%s", got) + } + if strings.Count(got, "CHECK-LABEL") != 1 { + t.Fatalf("function label was duplicated:\n%s", got) + } +} + +func TestUpdateSourceChecks_PreservesDefinitionAndEOF(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK: define i64 @"{{.*}}.changed"(ptr %0){{.*}} { +// CHECK-NEXT: entry: +// CHECK-NEXT: %1 = load i64, ptr %0 +// CHECK-NEXT: ret i64 %1 +// CHECK-NEXT: } +func changed(*int) int { return 0 } +` + const ir = `define i64 @"example.com/p.changed"(ptr %0) { +entry: + %nilcheck = icmp eq ptr %0, null + br i1 %nilcheck, label %panic, label %cont + +panic: + unreachable + +cont: + %1 = load i64, ptr %0 + ret i64 %1 +} +` + got, changed, err := updateSourceChecks(src, "in.go", "example.com/p", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if !changed { + t.Fatal("updateSourceChecks reported no change") + } + if !strings.Contains(got, `// CHECK: define i64 @"{{.*}}.changed"`) || strings.Contains(got, "CHECK-LABEL") { + t.Fatalf("definition directive changed:\n%s", got) + } + if strings.HasSuffix(got, "\n\n") { + t.Fatalf("update added a blank line at EOF:\n%q", got) + } +} From 4869f59f6878b9010c123c67abd3f6407fa7910f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 12 Aug 2026 15:10:32 +0800 Subject: [PATCH 2/7] chore/litgen: ignore non-IR check files in update mode --- chore/litgen/rewrite.go | 6 +++++- chore/litgen/rewrite_test.go | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/chore/litgen/rewrite.go b/chore/litgen/rewrite.go index 0f84995467..b125f06ea5 100644 --- a/chore/litgen/rewrite.go +++ b/chore/litgen/rewrite.go @@ -143,6 +143,10 @@ type checkGroup struct { // failing function group is regenerated at the group's existing byte range, // so updating a golden does not move unrelated checks or add new functions. func updateSourceChecks(src, srcPath, _, modulePath, ir string) (string, bool, error) { + groups := sourceCheckGroups(src) + if len(groups) == 0 { + return src, false, nil + } if err := matchCheckText(src, ir); err == nil { return src, false, nil } @@ -150,7 +154,7 @@ func updateSourceChecks(src, srcPath, _, modulePath, ir string) (string, bool, e var edits []sourceEdit var currentFn *irFunction - for _, group := range sourceCheckGroups(src) { + for _, group := range groups { fn, hasDefinition, err := findFunctionForCheckGroup(group.text, prog.funcs) if err != nil { return "", false, fmt.Errorf("%s: %w", srcPath, err) diff --git a/chore/litgen/rewrite_test.go b/chore/litgen/rewrite_test.go index a96bb664f0..ec2396eace 100644 --- a/chore/litgen/rewrite_test.go +++ b/chore/litgen/rewrite_test.go @@ -601,3 +601,24 @@ cont: t.Fatalf("update added a blank line at EOF:\n%q", got) } } + +func TestUpdateSourceChecks_IgnoresOtherCheckPrefixes(t *testing.T) { + const src = `// LITTEST +package main + +// SYMBOL-DAG: main +func main() {} +` + const ir = `define void @"example.com/p.main"() { +entry: + ret void +} +` + got, changed, err := updateSourceChecks(src, "in.go", "example.com/p", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if changed || got != src { + t.Fatalf("non-CHECK directives should remain unchanged:\n%s", got) + } +} From 2032f29847077280244a178d4defbe2d870c77a9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 12 Aug 2026 18:13:55 +0800 Subject: [PATCH 3/7] chore/litgen: safely refresh continuous checks by default --- chore/litgen/litgen.go | 14 +- chore/litgen/litgen_test.go | 4 +- chore/litgen/rewrite.go | 471 +++++++++++++++++++++++++++++------ chore/litgen/rewrite_test.go | 157 +++++++++++- dev/README.md | 9 +- 5 files changed, 568 insertions(+), 87 deletions(-) diff --git a/chore/litgen/litgen.go b/chore/litgen/litgen.go index 803d0ed1f9..58e93a44b6 100644 --- a/chore/litgen/litgen.go +++ b/chore/litgen/litgen.go @@ -26,7 +26,7 @@ import ( "github.com/goplus/llgo/xtool/env/llvm" ) -var update = flag.Bool("update", false, "update only failing existing CHECK groups in place") +var force = flag.Bool("force", false, "replace all CHECK directives with fully regenerated IR checks") func main() { llvm.SetupPath() @@ -45,6 +45,10 @@ func main() { } func processPath(path string) error { + return processPathWithForce(path, *force) +} + +func processPathWithForce(path string, force bool) error { abs, err := filepath.Abs(path) if err != nil { return err @@ -54,7 +58,7 @@ func processPath(path string) error { return err } if fi.IsDir() { - return processTree(abs) + return processTree(abs, force) } if filepath.Ext(abs) != ".go" { return fmt.Errorf("%s: expected .go file or directory", abs) @@ -71,10 +75,10 @@ func processPath(path string) error { return err } fmt.Fprintln(os.Stderr, "litgen", target.sourceFile) - return generateFile(target, *update) + return generateFile(target, force) } -func processTree(root string) error { +func processTree(root string, force bool) error { var targets []resolvedTarget err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { @@ -108,7 +112,7 @@ func processTree(root string) error { } for _, target := range targets { fmt.Fprintln(os.Stderr, "litgen", target.sourceFile) - if err := generateFile(target, *update); err != nil { + if err := generateFile(target, force); err != nil { return err } } diff --git a/chore/litgen/litgen_test.go b/chore/litgen/litgen_test.go index 75d9cd41e7..01b091c6f8 100644 --- a/chore/litgen/litgen_test.go +++ b/chore/litgen/litgen_test.go @@ -27,7 +27,7 @@ func TestProcessPath_SingleFileUsesContainingDir(t *testing.T) { t.Fatal(err) } - if err := processPath(sourceFile); err != nil { + if err := processPathWithForce(sourceFile, true); err != nil { t.Fatal(err) } @@ -68,7 +68,7 @@ func TestProcessPath_UsesFlagsFileTarget(t *testing.T) { t.Fatal(err) } - if err := processPath(sourceFile); err != nil { + if err := processPathWithForce(sourceFile, true); err != nil { t.Fatal(err) } diff --git a/chore/litgen/rewrite.go b/chore/litgen/rewrite.go index b125f06ea5..3c158e39a6 100644 --- a/chore/litgen/rewrite.go +++ b/chore/litgen/rewrite.go @@ -17,6 +17,7 @@ package main import ( + "errors" "fmt" "go/ast" "go/format" @@ -61,42 +62,44 @@ type irFunction struct { } var ( - defineQuotedRE = regexp.MustCompile(`^define\b.* @"([^"]+)"\(`) - definePlainRE = regexp.MustCompile(`^define\b.* @([^\s(]+)\(`) - globalQuotedRE = regexp.MustCompile(`^@"([^"]+)"\s*=`) - globalPlainRE = regexp.MustCompile(`^@([A-Za-z0-9$._-]+)\s*=`) - globalRefRE = regexp.MustCompile(`@"([^"]+)"|@([A-Za-z0-9$._-]+)`) - checkLineRE = regexp.MustCompile(`^\s*//\s*CHECK(?:-[A-Z]+)?:`) - debugMetaRE = regexp.MustCompile(`, ![A-Za-z0-9_.-]+ ![0-9]+`) - closureEnvRE = regexp.MustCompile(`(\s)(?:nest|swiftself)(\s)`) - testCasePathRE = regexp.MustCompile(`"[^"]*/cl/_test[^/"]*/[^/".]+`) - symbolHashRE = regexp.MustCompile(`\$[-A-Za-z0-9_]{43}`) - cgoHashRE = regexp.MustCompile(`(_cgo_)[0-9a-f]+(_Cfunc_)`) - numericGlobalRE = regexp.MustCompile(`@\d+\b`) - metadataIDRE = regexp.MustCompile(`!\d+\b`) - sigJumpRE = regexp.MustCompile(`@(?:__)?(sig(?:set|long)jmp)\b`) - plainJumpRE = regexp.MustCompile(`@_*((?:set|long)jmp)\b`) - jmpBufAllocaRE = regexp.MustCompile(`alloca i8, i64 (?:196|200), align 1`) - numericNameRE = regexp.MustCompile(`^\d+$`) + defineQuotedRE = regexp.MustCompile(`^define\b.* @"([^"]+)"\(`) + definePlainRE = regexp.MustCompile(`^define\b.* @([^\s(]+)\(`) + globalQuotedRE = regexp.MustCompile(`^@"([^"]+)"\s*=`) + globalPlainRE = regexp.MustCompile(`^@([A-Za-z0-9$._-]+)\s*=`) + globalRefRE = regexp.MustCompile(`@"([^"]+)"|@([A-Za-z0-9$._-]+)`) + checkLineRE = regexp.MustCompile(`^\s*//\s*CHECK(?:-[A-Z]+)?:`) + checkDirectiveRE = regexp.MustCompile(`^\s*//\s*(CHECK(?:-[A-Z]+)?):\s?(.*?)(?:\r?\n)?$`) + symbolLineRE = regexp.MustCompile(`(?m)^\s*//\s*SYMBOL(?:-[A-Z]+)?:`) + debugMetaRE = regexp.MustCompile(`, ![A-Za-z0-9_.-]+ ![0-9]+`) + closureEnvRE = regexp.MustCompile(`(\s)(?:nest|swiftself)(\s)`) + testCasePathRE = regexp.MustCompile(`"[^"]*/cl/_test[^/"]*/[^/".]+`) + symbolHashRE = regexp.MustCompile(`\$[-A-Za-z0-9_]{43}`) + cgoHashRE = regexp.MustCompile(`(_cgo_)[0-9a-f]+(_Cfunc_)`) + numericGlobalRE = regexp.MustCompile(`@\d+\b`) + metadataIDRE = regexp.MustCompile(`!\d+\b`) + sigJumpRE = regexp.MustCompile(`@(?:__)?(sig(?:set|long)jmp)\b`) + plainJumpRE = regexp.MustCompile(`@_*((?:set|long)jmp)\b`) + jmpBufAllocaRE = regexp.MustCompile(`alloca i8, i64 (?:196|200), align 1`) + pthreadTypeRE = regexp.MustCompile(`/runtime/internal/clite/pthread/sync\.([A-Za-z0-9_]+)"`) + numericNameRE = regexp.MustCompile(`^\d+$`) ) type pthreadOpaqueSize struct { - typeName string - sizes *regexp.Regexp - want string + sizes *regexp.Regexp + want string } -var pthreadOpaqueSizes = []pthreadOpaqueSize{ - {"MutexAttr", regexp.MustCompile(`\[(?:4|8|16) x i8\]`), `[{{(4|8|16)}} x i8]`}, - {"RWLockAttr", regexp.MustCompile(`\[(?:8|16|24) x i8\]`), `[{{(8|16|24)}} x i8]`}, - {"CondAttr", regexp.MustCompile(`\[(?:4|8|16) x i8\]`), `[{{(4|8|16)}} x i8]`}, - {"Once", regexp.MustCompile(`\[(?:4|16) x i8\]`), `[{{(4|16)}} x i8]`}, - {"Mutex", regexp.MustCompile(`\[(?:40|48|64) x i8\]`), `[{{(40|48|64)}} x i8]`}, - {"RWLock", regexp.MustCompile(`\[(?:56|192|200) x i8\]`), `[{{(56|192|200)}} x i8]`}, - {"Cond", regexp.MustCompile(`\[(?:40|48) x i8\]`), `[{{(40|48)}} x i8]`}, +var pthreadOpaqueSizes = map[string]pthreadOpaqueSize{ + "MutexAttr": {regexp.MustCompile(`\[(?:4|8|16) x i8\]`), `[{{(4|8|16)}} x i8]`}, + "RWLockAttr": {regexp.MustCompile(`\[(?:8|16|24) x i8\]`), `[{{(8|16|24)}} x i8]`}, + "CondAttr": {regexp.MustCompile(`\[(?:4|8|16) x i8\]`), `[{{(4|8|16)}} x i8]`}, + "Once": {regexp.MustCompile(`\[(?:4|16) x i8\]`), `[{{(4|16)}} x i8]`}, + "Mutex": {regexp.MustCompile(`\[(?:40|48|64) x i8\]`), `[{{(40|48|64)}} x i8]`}, + "RWLock": {regexp.MustCompile(`\[(?:56|192|200) x i8\]`), `[{{(56|192|200)}} x i8]`}, + "Cond": {regexp.MustCompile(`\[(?:40|48) x i8\]`), `[{{(40|48)}} x i8]`}, } -func generateFile(target resolvedTarget, update bool) error { +func generateFile(target resolvedTarget, force bool) error { data, err := os.ReadFile(target.sourceFile) if err != nil { return err @@ -105,7 +108,7 @@ func generateFile(target resolvedTarget, update bool) error { if err != nil { return err } - if update { + if !force { updated, changed, err := updateSourceChecks(string(data), target.sourceFile, target.pkgPath, target.modulePath, ir) if err != nil { return err @@ -139,56 +142,70 @@ type checkGroup struct { text string } -// updateSourceChecks preserves every CHECK group that still matches. A -// failing function group is regenerated at the group's existing byte range, -// so updating a golden does not move unrelated checks or add new functions. +type checkDirective struct { + kind string + pattern string +} + +type updateContext struct { + fn *irFunction + nextLine int + canNext bool +} + +// updateSourceChecks regenerates continuous anchor + NEXT/EMPTY snapshots in +// place. Other CHECK forms express hand-written test intent: they are kept +// verbatim and must still pass the final whole-file FileCheck validation. func updateSourceChecks(src, srcPath, _, modulePath, ir string) (string, bool, error) { groups := sourceCheckGroups(src) if len(groups) == 0 { - return src, false, nil - } - if err := matchCheckText(src, ir); err == nil { - return src, false, nil + if symbolLineRE.MatchString(src) { + return src, false, nil + } + return "", false, fmt.Errorf("%s: no CHECK directives; use -force to initialize IR checks", srcPath) } prog := parseIR(ir) + functionChecks := indexFunctionChecks(prog.funcs, modulePath) var edits []sourceEdit - var currentFn *irFunction + var context updateContext for _, group := range groups { - fn, hasDefinition, err := findFunctionForCheckGroup(group.text, prog.funcs) + directives, err := parseCheckDirectives(group.text) + if err != nil { + return "", false, fmt.Errorf("%s: %w", srcPath, err) + } + fn, hasDefinition, err := findFunctionForCheckGroup(group.text, prog.funcs, functionChecks) if err != nil { return "", false, fmt.Errorf("%s: %w", srcPath, err) } if hasDefinition { - currentFn = &fn + context = updateContext{fn: &fn, nextLine: 1, canNext: len(directives) == 1} } - if err := matchCheckText(group.text, ir); err == nil { + if !isContinuousSnapshot(directives) { + context = advanceManualContext(context, directives, hasDefinition) continue } - if !hasDefinition { - if currentFn == nil { - return "", false, fmt.Errorf("%s: failing CHECK group has no preceding function definition", srcPath) - } - fn = *currentFn - } - lines := buildFunctionChecks(fn, modulePath) - if len(lines) == 0 { - return "", false, fmt.Errorf("%s: no checks generated for %q", srcPath, fn.symbol) + + lines, start, end, implicit, err := resolveSnapshotRange(group.text, directives, hasDefinition, context, ir, fn) + if err != nil { + return "", false, fmt.Errorf("%s: %w; use -force to regenerate all IR checks", srcPath, err) } - if hasDefinition { - // Keep the existing definition directive byte-for-byte. It may be - // intentionally looser than newly generated checks. - lines[0] = firstDefinitionCheck(group.text) - } else { - lines = lines[1:] + generated := buildRangeChecks(lines[start:end+1], modulePath, directives[0], implicit) + if len(generated) == 0 { + return "", false, fmt.Errorf("%s: no checks generated for continuous snapshot", srcPath) } indent := indentAt(src, group.start) - text := formatDirectiveBlock(indent, lines) + text := formatDirectiveBlock(indent, generated) text = preserveTrailingNewlines(text, group.text) - edits = append(edits, sourceEdit{start: group.start, end: group.end, text: text}) - } - if len(edits) == 0 { - return "", false, fmt.Errorf("%s: existing CHECKs fail, but no function CHECK group can be updated", srcPath) + if text != group.text { + edits = append(edits, sourceEdit{start: group.start, end: group.end, text: text}) + } + if context.fn != nil && sameLineSlice(lines, context.fn.lines) { + context.nextLine = end + 1 + context.canNext = true + } else { + context.canNext = false + } } sort.Slice(edits, func(i, j int) bool { return edits[i].start > edits[j].start }) updated := src @@ -196,18 +213,249 @@ func updateSourceChecks(src, srcPath, _, modulePath, ir string) (string, bool, e updated = updated[:edit.start] + edit.text + updated[edit.end:] } if err := matchCheckText(updated, ir); err != nil { - return "", false, fmt.Errorf("%s: updated CHECKs still fail: %w", srcPath, err) + return "", false, fmt.Errorf("%s: CHECK validation failed and cannot be safely updated: %w; use -force to regenerate all IR checks", srcPath, err) } return updated, updated != src, nil } -func firstDefinitionCheck(group string) string { - for _, line := range strings.Split(group, "\n") { - if strings.Contains(line, "define ") { - return strings.TrimLeft(line, " \t") +func advanceManualContext(context updateContext, directives []checkDirective, hasDefinition bool) updateContext { + if context.fn == nil { + context.canNext = false + return context + } + start := 0 + if context.canNext { + start = context.nextLine + } + first := 0 + if hasDefinition { + first = 1 + start = 1 + } + matched := false + for _, directive := range directives[first:] { + if directive.kind != "CHECK" && directive.kind != "CHECK-LABEL" { + context.canNext = false + return context + } + lines := matchingDirectiveLines(directive, context.fn.lines, start) + if len(lines) == 0 { + context.canNext = false + return context } + start = lines[0] + 1 + matched = true } - return "" + if matched || (hasDefinition && len(directives) == 1) { + context.nextLine = start + context.canNext = true + return context + } + context.canNext = false + return context +} + +func sameLineSlice(a, b []string) bool { + return len(a) == len(b) && (len(a) == 0 || &a[0] == &b[0]) +} + +func parseCheckDirectives(group string) ([]checkDirective, error) { + var directives []checkDirective + for _, line := range strings.SplitAfter(group, "\n") { + if strings.TrimSpace(line) == "" { + continue + } + match := checkDirectiveRE.FindStringSubmatch(line) + if match == nil { + return nil, fmt.Errorf("invalid CHECK directive %q", strings.TrimSpace(line)) + } + directives = append(directives, checkDirective{kind: match[1], pattern: match[2]}) + } + return directives, nil +} + +func isContinuousSnapshot(directives []checkDirective) bool { + if len(directives) < 2 || strings.Contains(directives[0].pattern, "[[") { + return false + } + first := directives[0].kind + if first != "CHECK" && first != "CHECK-LABEL" && first != "CHECK-NEXT" { + return false + } + for _, directive := range directives[1:] { + if (directive.kind != "CHECK-NEXT" && directive.kind != "CHECK-EMPTY") || strings.Contains(directive.pattern, "[[") { + return false + } + } + return true +} + +func resolveSnapshotRange(group string, directives []checkDirective, hasDefinition bool, context updateContext, ir string, fn irFunction) ([]string, int, int, bool, error) { + implicit := directives[0].kind == "CHECK-NEXT" + if implicit { + if context.fn == nil || !context.canNext || context.nextLine >= len(context.fn.lines) { + return nil, 0, 0, false, errors.New("CHECK-NEXT snapshot has no recoverable preceding anchor") + } + lines := context.fn.lines + start := context.nextLine + if end, ok := matchSnapshotAt(group, directives, lines, start, true); ok { + return lines, start, end, true, nil + } + end, err := recoverSnapshotEnd(directives, lines, start) + if err != nil { + return nil, 0, 0, false, err + } + return lines, start, end, true, nil + } + + lines := splitIRLines(ir) + if hasDefinition { + lines = fn.lines + if snapshotEndsFunction(directives) { + return lines, 0, len(lines) - 1, false, nil + } + if end, ok := matchSnapshotAt(group, directives, lines, 0, false); ok { + return lines, 0, end, false, nil + } + end, err := recoverSnapshotEnd(directives, lines, 0) + if err != nil { + return nil, 0, 0, false, err + } + return lines, 0, end, false, nil + } + if context.fn != nil { + lines = context.fn.lines + } + minStart := 0 + if context.canNext { + minStart = context.nextLine + } + starts := matchingSnapshotWindows(group, directives, lines, minStart) + if len(starts) == 1 || (context.canNext && len(starts) > 1) { + return lines, starts[0], starts[0] + len(directives) - 1, false, nil + } + if len(starts) > 1 { + return nil, 0, 0, false, fmt.Errorf("continuous snapshot matches %d IR ranges", len(starts)) + } + start, end, err := recoverSnapshotBounds(directives, lines, minStart, context.canNext) + if err != nil { + return nil, 0, 0, false, err + } + return lines, start, end, false, nil +} + +func snapshotEndsFunction(directives []checkDirective) bool { + for i := len(directives) - 1; i >= 0; i-- { + if directives[i].kind == "CHECK-EMPTY" { + continue + } + return strings.TrimSpace(directives[i].pattern) == "}" + } + return false +} + +func matchSnapshotAt(group string, directives []checkDirective, lines []string, start int, implicit bool) (int, bool) { + count := len(directives) + if start < 0 || start+count > len(lines) { + return 0, false + } + checks := group + if implicit { + checks = replaceFirstDirectiveKind(group, "CHECK") + } + input := strings.Join(lines[start:start+count], "\n") + "\n" + return start + count - 1, matchCheckText(checks, input) == nil +} + +func matchingSnapshotWindows(group string, directives []checkDirective, lines []string, minStart int) []int { + var matches []int + for start := minStart; start+len(directives) <= len(lines); start++ { + if _, ok := matchSnapshotAt(group, directives, lines, start, false); ok { + matches = append(matches, start) + } + } + return matches +} + +func recoverSnapshotBounds(directives []checkDirective, lines []string, minStart int, ordered bool) (int, int, error) { + if directives[len(directives)-1].kind == "CHECK-EMPTY" { + return 0, 0, errors.New("failed snapshot ends in CHECK-EMPTY and has no stable end anchor") + } + starts := matchingDirectiveLines(directives[0], lines, minStart) + if len(starts) == 0 || (!ordered && len(starts) != 1) { + return 0, 0, fmt.Errorf("snapshot start anchor matches %d IR lines", len(starts)) + } + end, err := recoverSnapshotEndWithOrder(directives, lines, starts[0], ordered) + if err != nil { + return 0, 0, err + } + return starts[0], end, nil +} + +func recoverSnapshotEnd(directives []checkDirective, lines []string, start int) (int, error) { + return recoverSnapshotEndWithOrder(directives, lines, start, false) +} + +func recoverSnapshotEndWithOrder(directives []checkDirective, lines []string, start int, ordered bool) (int, error) { + last := directives[len(directives)-1] + if last.kind == "CHECK-EMPTY" { + return 0, errors.New("failed snapshot ends in CHECK-EMPTY and has no stable end anchor") + } + ends := matchingDirectiveLines(last, lines, start+1) + var after []int + for _, end := range ends { + after = append(after, end) + } + if len(after) == 0 || (!ordered && len(after) != 1) { + return 0, fmt.Errorf("snapshot end anchor matches %d IR lines after its start", len(after)) + } + return after[0], nil +} + +func matchingDirectiveLines(directive checkDirective, lines []string, minStart int) []int { + if directive.kind == "CHECK-EMPTY" { + return nil + } + check := "// CHECK: " + directive.pattern + "\n" + var matches []int + for i := minStart; i < len(lines); i++ { + line := lines[i] + if matchCheckText(check, line+"\n") == nil { + matches = append(matches, i) + } + } + return matches +} + +func replaceFirstDirectiveKind(group, kind string) string { + loc := checkDirectiveRE.FindStringSubmatchIndex(group) + if loc == nil { + return group + } + return group[:loc[2]] + kind + group[loc[3]:] +} + +func buildRangeChecks(lines []string, modulePath string, first checkDirective, implicit bool) []string { + checks := make([]string, 0, len(lines)) + for i, line := range lines { + if strings.TrimSpace(line) == "" { + checks = append(checks, "// CHECK-EMPTY:") + continue + } + if i == 0 && !implicit { + pattern := first.pattern + if len(matchingDirectiveLines(first, lines[:1], 0)) == 0 { + pattern = generalizeIRLine(line, modulePath) + if strings.HasPrefix(line, "define ") { + pattern = generalizeDefineLine(line, modulePath) + } + } + checks = append(checks, "// "+first.kind+": "+pattern) + continue + } + checks = append(checks, "// CHECK-NEXT: "+generalizeIRLine(line, modulePath)) + } + return checks } func preserveTrailingNewlines(generated, original string) string { @@ -253,19 +501,41 @@ func sourceCheckGroups(src string) []checkGroup { return groups } -func findFunctionForCheckGroup(group string, funcs []irFunction) (irFunction, bool, error) { - var definitionCheck string +func indexFunctionChecks(funcs []irFunction, modulePath string) map[string][]*irFunction { + checks := make(map[string][]*irFunction, len(funcs)) + for i := range funcs { + if len(funcs[i].lines) == 0 { + continue + } + line := generalizeDefineLine(funcs[i].lines[0], modulePath) + checks[line] = append(checks[line], &funcs[i]) + } + return checks +} + +func findFunctionForCheckGroup(group string, funcs []irFunction, functionChecks map[string][]*irFunction) (irFunction, bool, error) { + var definition string for _, line := range strings.Split(group, "\n") { idx := strings.Index(line, "define ") if idx < 0 { continue } - definitionCheck = "// CHECK: " + line[idx:] + "\n" + definition = line[idx:] break } - if definitionCheck == "" { + if definition == "" { return irFunction{}, false, nil } + if matched := functionChecks[definition]; len(matched) != 0 { + if len(matched) != 1 { + return irFunction{}, false, fmt.Errorf("function CHECK matches both %q and %q", matched[0].symbol, matched[1].symbol) + } + return *matched[0], true, nil + } + + // Hand-written definition checks can intentionally be looser than litgen's + // output. Retain FileCheck matching as a compatibility fallback for them. + definitionCheck := "// CHECK: " + definition + "\n" var matched *irFunction for i := range funcs { if len(funcs[i].lines) == 0 || matchCheckText(definitionCheck, strings.Join(funcs[i].lines, "\n")) != nil { @@ -276,10 +546,56 @@ func findFunctionForCheckGroup(group string, funcs []irFunction) (irFunction, bo } matched = &funcs[i] } - if matched == nil { - return irFunction{}, false, fmt.Errorf("function CHECK does not match current IR: %s", strings.TrimSpace(definitionCheck)) + if matched != nil { + return *matched, true, nil } - return *matched, true, nil + + // A source or ABI change can alter the signature while the function symbol + // remains stable. Match just that symbol before giving up on the snapshot. + symbolPattern, ok := definitionSymbolToken(definition) + if ok { + for i := range funcs { + if len(funcs[i].lines) == 0 { + continue + } + actual, found := definitionSymbolToken(funcs[i].lines[0]) + if !found || matchCheckText("// CHECK: "+symbolPattern+"\n", actual+"\n") != nil { + continue + } + if matched != nil { + return irFunction{}, false, fmt.Errorf("function symbol CHECK matches both %q and %q", matched.symbol, funcs[i].symbol) + } + matched = &funcs[i] + } + if matched != nil { + return *matched, true, nil + } + } + return irFunction{}, false, fmt.Errorf("function CHECK does not match current IR: %s", strings.TrimSpace(definitionCheck)) +} + +func definitionSymbolToken(definition string) (string, bool) { + start := strings.IndexByte(definition, '@') + if start < 0 { + return "", false + } + inQuote := false + regexDepth := 0 + for i := start + 1; i < len(definition); i++ { + switch { + case i+1 < len(definition) && definition[i:i+2] == "{{": + regexDepth++ + i++ + case regexDepth > 0 && i+1 < len(definition) && definition[i:i+2] == "}}": + regexDepth-- + i++ + case regexDepth == 0 && definition[i] == '"' && !isEscapedQuote(definition, i): + inQuote = !inQuote + case regexDepth == 0 && !inQuote && definition[i] == '(': + return definition[start:i], true + } + } + return "", false } func matchCheckText(checks, ir string) error { @@ -682,6 +998,8 @@ func generalizeIRLine(line, modulePath string) string { } func scrubIRLine(line string) string { + // Escape source IR syntax before adding FileCheck regexes below. + line = strings.ReplaceAll(line, "[[", `{{\[\[}}`) line = debugMetaRE.ReplaceAllString(line, "") line = generalizeClosureEnvAttrs(line) line = symbolHashRE.ReplaceAllString(line, `$${{[-A-Za-z0-9_]+}}`) @@ -689,7 +1007,6 @@ func scrubIRLine(line string) string { line = numericGlobalRE.ReplaceAllString(line, `@{{[0-9]+}}`) line = metadataIDRE.ReplaceAllString(line, `!{{[0-9]+}}`) line = generalizePlatformIR(line) - line = strings.ReplaceAll(line, "[[", `{{\[\[}}`) return strings.TrimRight(line, " \t") } @@ -702,8 +1019,8 @@ func generalizePlatformIR(line string) string { line = sigJumpRE.ReplaceAllString(line, `@{{(__)?}}${1}`) line = plainJumpRE.ReplaceAllString(line, `@{{_*}}${1}`) line = jmpBufAllocaRE.ReplaceAllString(line, `alloca i8, i64 {{(196|200)}}, align 1`) - for _, opaque := range pthreadOpaqueSizes { - if strings.Contains(line, "/runtime/internal/clite/pthread/sync."+opaque.typeName+`"`) { + if match := pthreadTypeRE.FindStringSubmatch(line); match != nil { + if opaque, ok := pthreadOpaqueSizes[match[1]]; ok { return opaque.sizes.ReplaceAllString(line, opaque.want) } } diff --git a/chore/litgen/rewrite_test.go b/chore/litgen/rewrite_test.go index ec2396eace..203c93b067 100644 --- a/chore/litgen/rewrite_test.go +++ b/chore/litgen/rewrite_test.go @@ -446,6 +446,18 @@ func TestGeneralizePlatformIR(t *testing.T) { `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.Mutex" { [64 x i8] zeroinitializer }, ptr %0`, `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.Mutex" { [{{(40|48|64)}} x i8] zeroinitializer }, ptr %0`, }, + { + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.MutexAttr" { [16 x i8] zeroinitializer }, ptr %0`, + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.MutexAttr" { [{{(4|8|16)}} x i8] zeroinitializer }, ptr %0`, + }, + { + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.RWLockAttr" { [24 x i8] zeroinitializer }, ptr %0`, + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.RWLockAttr" { [{{(8|16|24)}} x i8] zeroinitializer }, ptr %0`, + }, + { + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.CondAttr" { [8 x i8] zeroinitializer }, ptr %0`, + `store %"github.com/goplus/llgo/runtime/internal/clite/pthread/sync.CondAttr" { [{{(4|8|16)}} x i8] zeroinitializer }, ptr %0`, + }, } for _, test := range tests { if got := generalizePlatformIR(test.line); got != test.want { @@ -464,14 +476,30 @@ func TestGeneralizeIRLine_WildcardsUnstableIDs(t *testing.T) { } func TestGeneralizeIRLine_EscapesFileCheckSyntaxAndCgoHash(t *testing.T) { - line := ` %0 = load ptr, ptr @main._cgo_52352d07b8a3_Cfunc_free, align 8 ; map[[2]int]` + line := ` %0 = load ptr, ptr @0[[, ptr @main._cgo_52352d07b8a3_Cfunc_free` got := generalizeIRLine(line, "") - want := ` %0 = load ptr, ptr @main._cgo_{{[0-9a-f]+}}_Cfunc_free, align 8 ; map{{\[\[}}2]int]` + want := ` %0 = load ptr, ptr @{{[0-9]+}}{{\[\[}}, ptr @main._cgo_{{[0-9a-f]+}}_Cfunc_free` if got != want { t.Fatalf("generalizeIRLine() = %q, want %q", got, want) } } +func TestIndexFunctionChecks(t *testing.T) { + funcs := parseIR(`define void @main.main() { +entry: + ret void +} +`).funcs + checks := indexFunctionChecks(funcs, "example.com") + got, found, err := findFunctionForCheckGroup("// CHECK-LABEL: define void @main.main(){{.*}} {\n", funcs, checks) + if err != nil { + t.Fatal(err) + } + if !found || got.symbol != "main.main" { + t.Fatalf("findFunctionForCheckGroup() = (%q, %v), want main.main", got.symbol, found) + } +} + func TestUpdateSourceChecks_UpdatesOnlyFailingGroupInPlace(t *testing.T) { const src = `// LITTEST package p @@ -622,3 +650,128 @@ entry: t.Fatalf("non-CHECK directives should remain unchanged:\n%s", got) } } + +func TestUpdateSourceChecks_RegeneratesPassingContinuousSnapshot(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK-LABEL: define void @main.main(){{.*}} { +// CHECK-NEXT: entry: +// CHECK-NEXT: ret {{.*}} +// CHECK-NEXT: } +func main() {} +` + const ir = `define void @main.main() { +entry: + ret void +} +` + got, changed, err := updateSourceChecks(src, "in.go", "main", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if !changed || !strings.Contains(got, "// CHECK-NEXT: ret void") { + t.Fatalf("passing continuous snapshot was not regenerated:\n%s", got) + } +} + +func TestUpdateSourceChecks_RecoversChangedFunctionSignature(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK-LABEL: define i64 @main.changed(i64 %0){{.*}} { +// CHECK-NEXT: entry: +// CHECK-NEXT: ret i64 %0 +// CHECK-NEXT: } +func changed(int) int { return 0 } +` + const ir = `define void @main.changed(ptr %0) { +entry: + ret void +} +` + got, changed, err := updateSourceChecks(src, "in.go", "main", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if !changed || !strings.Contains(got, "// CHECK-LABEL: define void @main.changed(ptr %0){{.*}} {") { + t.Fatalf("changed function signature was not recovered:\n%s", got) + } +} + +func TestUpdateSourceChecks_RecoversChangedLocalSnapshotBounds(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK-LABEL: define void @main.main(){{.*}} { +func main() { + // CHECK: call void @main.work() + // CHECK-NEXT: ret void + work() +} +` + const ir = `define void @main.main() { +entry: + call void @main.work() + call void @main.added() + ret void +} +` + got, changed, err := updateSourceChecks(src, "in.go", "main", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if !changed || !strings.Contains(got, "// CHECK-NEXT: call void @main.added()") { + t.Fatalf("changed local snapshot was not recovered:\n%s", got) + } +} + +func TestUpdateSourceChecks_RejectsAmbiguousSnapshotBounds(t *testing.T) { + const src = `// LITTEST +package p + +func main() { + // CHECK: load i64, ptr %0 + // CHECK-NEXT: ret void +} +` + const ir = `define void @main.main() { +entry: + load i64, ptr %0 + load i64, ptr %0 + call void @main.added() + ret void +} +` + _, _, err := updateSourceChecks(src, "in.go", "main", "example.com", ir) + if err == nil || !strings.Contains(err.Error(), "start anchor matches 2 IR lines") { + t.Fatalf("ambiguous snapshot error = %v", err) + } +} + +func TestUpdateSourceChecks_RejectsFailingManualCheck(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK-LABEL: define void @main.main() +// CHECK: call void @main.missing() +func main() {} +` + const ir = `define void @main.main() { +entry: + ret void +} +` + _, _, err := updateSourceChecks(src, "in.go", "main", "example.com", ir) + if err == nil || !strings.Contains(err.Error(), "cannot be safely updated") { + t.Fatalf("manual CHECK error = %v", err) + } +} + +func TestUpdateSourceChecks_RequiresForceToInitialize(t *testing.T) { + const src = "// LITTEST\npackage p\n" + _, _, err := updateSourceChecks(src, "in.go", "main", "example.com", "") + if err == nil || !strings.Contains(err.Error(), "use -force") { + t.Fatalf("missing CHECK error = %v", err) + } +} diff --git a/dev/README.md b/dev/README.md index f831633577..c39b197145 100644 --- a/dev/README.md +++ b/dev/README.md @@ -111,12 +111,19 @@ go run ./chore/litgen cl/_testrt/litdemo go run ./chore/litgen cl/_testdata ``` +Replace all CHECK directives and regenerate complete IR checks: + +```bash +go run ./chore/litgen -force cl/_testdata +``` + Behavior: - Accepts one or more paths. - If the path is a `.go` file, it refreshes only that file. The file must start with `// LITTEST`. - If the path is a directory, it walks that directory recursively, finds marked source files, and refreshes each marked test in place. -- Rewrites embedded `CHECK-LABEL`, `CHECK-NEXT`, `CHECK-EMPTY`, and referenced constant `CHECK-LINE` directives from the current generated IR. +- By default, regenerates recognized `CHECK`/`CHECK-LABEL` plus `CHECK-NEXT`/`CHECK-EMPTY` snapshots in place, even when they still pass. Hand-written checks are preserved and must pass FileCheck; ambiguous or unrecoverable snapshots fail without writing. +- With `-force`, replaces all IR CHECK directives and generates referenced globals and all supported functions. `SYMBOL-*` checks are preserved. - Does not update `expect.txt` and does not write `out.ll`. Use `litgen` when the test case stores its IR expectations directly in the Go source instead of `out.ll`. From bd6c9575bb2686ab24daf8fe8bed3b8fb33f9291 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 12 Aug 2026 18:19:05 +0800 Subject: [PATCH 4/7] chore/litgen: recognize extended FileCheck directives --- chore/litgen/rewrite.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chore/litgen/rewrite.go b/chore/litgen/rewrite.go index 3c158e39a6..6e5789834f 100644 --- a/chore/litgen/rewrite.go +++ b/chore/litgen/rewrite.go @@ -67,8 +67,8 @@ var ( globalQuotedRE = regexp.MustCompile(`^@"([^"]+)"\s*=`) globalPlainRE = regexp.MustCompile(`^@([A-Za-z0-9$._-]+)\s*=`) globalRefRE = regexp.MustCompile(`@"([^"]+)"|@([A-Za-z0-9$._-]+)`) - checkLineRE = regexp.MustCompile(`^\s*//\s*CHECK(?:-[A-Z]+)?:`) - checkDirectiveRE = regexp.MustCompile(`^\s*//\s*(CHECK(?:-[A-Z]+)?):\s?(.*?)(?:\r?\n)?$`) + checkLineRE = regexp.MustCompile(`^\s*//\s*CHECK(?:-[A-Z0-9]+)*:`) + checkDirectiveRE = regexp.MustCompile(`^\s*//\s*(CHECK(?:-[A-Z0-9]+)*):\s?(.*?)(?:\r?\n)?$`) symbolLineRE = regexp.MustCompile(`(?m)^\s*//\s*SYMBOL(?:-[A-Z]+)?:`) debugMetaRE = regexp.MustCompile(`, ![A-Za-z0-9_.-]+ ![0-9]+`) closureEnvRE = regexp.MustCompile(`(\s)(?:nest|swiftself)(\s)`) From 7ee9bfd3e8ad27eeef0da9662d28a6ede77ad9a7 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 12 Aug 2026 18:21:59 +0800 Subject: [PATCH 5/7] chore/litgen: split adjacent check anchors --- chore/litgen/rewrite.go | 28 ++++++++++++++++++++++---- chore/litgen/rewrite_test.go | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/chore/litgen/rewrite.go b/chore/litgen/rewrite.go index 6e5789834f..de6673856e 100644 --- a/chore/litgen/rewrite.go +++ b/chore/litgen/rewrite.go @@ -491,12 +491,32 @@ func sourceCheckGroups(src string) []checkGroup { } line++ } - start := lineStarts[startLine] + blockEnd := line + groupStart := startLine + for current := startLine + 1; current < blockEnd; current++ { + currentEnd := len(src) + if current+1 < len(lineStarts) { + currentEnd = lineStarts[current+1] + } + match := checkDirectiveRE.FindStringSubmatch(strings.TrimRight(src[lineStarts[current]:currentEnd], "\r\n")) + if match != nil && match[1] != "CHECK-NEXT" && match[1] != "CHECK-EMPTY" { + groups = append(groups, checkGroup{ + start: lineStarts[groupStart], + end: lineStarts[current], + text: src[lineStarts[groupStart]:lineStarts[current]], + }) + groupStart = current + } + } end := len(src) - if line < len(lineStarts) { - end = lineStarts[line] + if blockEnd < len(lineStarts) { + end = lineStarts[blockEnd] } - groups = append(groups, checkGroup{start: start, end: end, text: src[start:end]}) + groups = append(groups, checkGroup{ + start: lineStarts[groupStart], + end: end, + text: src[lineStarts[groupStart]:end], + }) } return groups } diff --git a/chore/litgen/rewrite_test.go b/chore/litgen/rewrite_test.go index 203c93b067..800828ca68 100644 --- a/chore/litgen/rewrite_test.go +++ b/chore/litgen/rewrite_test.go @@ -675,6 +675,45 @@ entry: } } +func TestUpdateSourceChecks_SplitsAdjacentAnchorsFromContinuousSnapshot(t *testing.T) { + const src = `// LITTEST +package p + +// CHECK: @0 = private constant i8 1 +// CHECK: define i64 @main.changed(ptr %0){{.*}} { +// CHECK-NEXT: entry: +// CHECK-NEXT: %1 = load i64, ptr %0 +// CHECK-NEXT: ret i64 %1 +// CHECK-NEXT: } +func changed(*int) int { return 0 } +` + const ir = `@0 = private constant i8 1 + +define i64 @main.changed(ptr %0) { +entry: + %nilcheck = icmp eq ptr %0, null + br i1 %nilcheck, label %panic, label %cont + +panic: + unreachable + +cont: + %1 = load i64, ptr %0 + ret i64 %1 +} +` + got, changed, err := updateSourceChecks(src, "in.go", "main", "example.com", ir) + if err != nil { + t.Fatal(err) + } + if !changed || !strings.Contains(got, "// CHECK-NEXT: %nilcheck = icmp eq ptr %0, null") { + t.Fatalf("adjacent function snapshot was not regenerated:\n%s", got) + } + if strings.Count(got, "// CHECK: @0 = private constant i8 1") != 1 { + t.Fatalf("adjacent manual anchor changed:\n%s", got) + } +} + func TestUpdateSourceChecks_RecoversChangedFunctionSignature(t *testing.T) { const src = `// LITTEST package p From d8707c0584e92f27493c6cf75771d11c4f397cf2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 12 Aug 2026 18:25:11 +0800 Subject: [PATCH 6/7] chore/litgen: recover split snapshots through function end --- chore/litgen/rewrite.go | 3 +++ chore/litgen/rewrite_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/chore/litgen/rewrite.go b/chore/litgen/rewrite.go index de6673856e..8cc3dfd8be 100644 --- a/chore/litgen/rewrite.go +++ b/chore/litgen/rewrite.go @@ -298,6 +298,9 @@ func resolveSnapshotRange(group string, directives []checkDirective, hasDefiniti } lines := context.fn.lines start := context.nextLine + if snapshotEndsFunction(directives) { + return lines, start, len(lines) - 1, true, nil + } if end, ok := matchSnapshotAt(group, directives, lines, start, true); ok { return lines, start, end, true, nil } diff --git a/chore/litgen/rewrite_test.go b/chore/litgen/rewrite_test.go index 800828ca68..080c933326 100644 --- a/chore/litgen/rewrite_test.go +++ b/chore/litgen/rewrite_test.go @@ -568,6 +568,7 @@ func changed(*int) int { ` const ir = `define i64 @"example.com/p.changed"(ptr %0) { entry: + %aggregate = insertvalue { ptr, ptr } undef, ptr %0, 0 %nilcheck = icmp eq ptr %0, null br i1 %nilcheck, label %panic, label %cont From afffadb63c750e2d3a30c4003379406768d8b3f8 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 12 Aug 2026 18:33:08 +0800 Subject: [PATCH 7/7] chore/litgen: reuse the detected development root --- chore/litgen/litgen.go | 15 +++++++++++++++ chore/litgen/rewrite.go | 1 + 2 files changed, 16 insertions(+) diff --git a/chore/litgen/litgen.go b/chore/litgen/litgen.go index 58e93a44b6..fc51aca087 100644 --- a/chore/litgen/litgen.go +++ b/chore/litgen/litgen.go @@ -21,14 +21,18 @@ import ( "fmt" "os" "path/filepath" + "sync" + llgoenv "github.com/goplus/llgo/internal/env" "github.com/goplus/llgo/internal/littest" "github.com/goplus/llgo/xtool/env/llvm" ) var force = flag.Bool("force", false, "replace all CHECK directives with fully regenerated IR checks") +var setupLLGoRootOnce sync.Once func main() { + setupLLGoRoot() llvm.SetupPath() flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [...]\n", filepath.Base(os.Args[0])) @@ -44,6 +48,17 @@ func main() { } } +func setupLLGoRoot() { + setupLLGoRootOnce.Do(func() { + if os.Getenv("LLGO_ROOT") != "" { + return + } + if root := llgoenv.LLGoROOT(); root != "" { + _ = os.Setenv("LLGO_ROOT", root) + } + }) +} + func processPath(path string) error { return processPathWithForce(path, *force) } diff --git a/chore/litgen/rewrite.go b/chore/litgen/rewrite.go index 8cc3dfd8be..2c0722711b 100644 --- a/chore/litgen/rewrite.go +++ b/chore/litgen/rewrite.go @@ -662,6 +662,7 @@ func resolveTarget(sourceFile, genTarget string) (resolvedTarget, error) { } func genIR(target string) (ret string, err error) { + setupLLGoRoot() defer func() { if r := recover(); r != nil { switch v := r.(type) {