test: add litgen tool for source IR checks

This commit is contained in:
ZhouGuangyuan
2026-04-17 22:54:09 +08:00
parent 3bfac48717
commit 8cb9e72c1f
8 changed files with 1122 additions and 0 deletions
+4
View File
@@ -424,8 +424,12 @@ cd llgo
* [pysigfetch](https://github.com/goplus/hdq/tree/main/chore/pysigfetch): It generates symbol information by extracting information from Python's documentation site. This tool is not part of the `llgo` project, but we depend on it.
* [llpyg](chore/llpyg): It is used to automatically convert Python libraries into Go packages that `llgo` can import. It depends on `pydump` and `pysigfetch` to accomplish the task.
* [llgen](chore/llgen): It is used to compile Go packages into LLVM IR files (*.ll).
* [gentests](chore/gentests): It refreshes the built-in golden test data under `cl/_test*`, including `out.ll` and `expect.txt`. Directories that use source-embedded `// LITTEST` checks are skipped for `out.ll` regeneration.
* [litgen](chore/litgen): It generates and refreshes source-embedded `// LITTEST` FileCheck directives from the current LLVM IR for marked Go source files.
* [ssadump](chore/ssadump): It is a Go SSA builder and interpreter.
For local workflows and test-golden refresh commands, see [dev/README.md](dev/README.md#6-refresh-test-goldens).
How do I generate these tools?
<!-- embedme doc/_readme/scripts/install_full.sh#L2-L1000 -->
+49
View File
@@ -17,6 +17,7 @@
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
@@ -50,6 +51,12 @@ func llgenDir(dir string) {
continue
}
testDir := dir + "/" + name
skip, err := dirHasLITTESTSource(testDir)
check(err)
if skip {
fmt.Fprintln(os.Stderr, "skip llgen", testDir, "(// LITTEST)")
continue
}
fmt.Fprintln(os.Stderr, "llgen", testDir)
check(os.Chdir(testDir))
llgen.SmartDoFile(testDir)
@@ -98,3 +105,45 @@ func check(err error) {
panic(err)
}
}
func dirHasLITTESTSource(dir string) (bool, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return false, err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if filepath.Ext(name) != ".go" || strings.HasSuffix(name, "_test.go") {
continue
}
ok, err := hasLITTESTMarker(filepath.Join(dir, name))
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
}
func hasLITTESTMarker(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {
return false, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
return false, scanner.Err()
}
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "//") {
return false, nil
}
return strings.TrimSpace(strings.TrimPrefix(line, "//")) == "LITTEST", nil
}
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestDirHasLITTESTSource(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "in.go"), []byte("// LITTEST\npackage main\n"), 0644); err != nil {
t.Fatal(err)
}
ok, err := dirHasLITTESTSource(dir)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("dirHasLITTESTSource = false, want true")
}
}
func TestDirHasLITTESTSource_IgnoresUnmarkedFiles(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "in.go"), []byte("package main\n"), 0644); err != nil {
t.Fatal(err)
}
ok, err := dirHasLITTESTSource(dir)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("dirHasLITTESTSource = true, want false")
}
}
+116
View File
@@ -0,0 +1,116 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
)
func main() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s <file-or-dir> [<file-or-dir>...]\n", filepath.Base(os.Args[0]))
flag.PrintDefaults()
}
flag.Parse()
if flag.NArg() == 0 {
flag.Usage()
os.Exit(2)
}
for _, arg := range flag.Args() {
check(processPath(arg))
}
}
func processPath(path string) error {
abs, err := filepath.Abs(path)
if err != nil {
return err
}
fi, err := os.Stat(abs)
if err != nil {
return err
}
if fi.IsDir() {
return processTree(abs)
}
if filepath.Ext(abs) != ".go" {
return fmt.Errorf("%s: expected .go file or directory", abs)
}
ok, err := hasLITTESTMarker(abs)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("%s: missing // LITTEST marker", abs)
}
target, err := resolveTarget(abs, abs)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "litgen", target.sourceFile)
return generateFile(target)
}
func processTree(root string) error {
var targets []resolvedTarget
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
return nil
}
if path != root && len(d.Name()) > 0 && d.Name()[0] == '_' {
return filepath.SkipDir
}
marked, found, err := findMarkedSourceFile(path)
if err != nil {
return err
}
if !found {
return nil
}
target, err := resolveTarget(marked, path)
if err != nil {
return err
}
targets = append(targets, target)
return nil
})
if err != nil {
return err
}
if len(targets) == 0 {
return fmt.Errorf("%s: no // LITTEST sources found", root)
}
for _, target := range targets {
fmt.Fprintln(os.Stderr, "litgen", target.sourceFile)
if err := generateFile(target); err != nil {
return err
}
}
return nil
}
func check(err error) {
if err != nil {
panic(err)
}
}
+619
View File
@@ -0,0 +1,619 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bufio"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"github.com/goplus/llgo/internal/llgen"
"github.com/goplus/mod"
)
const littestMarker = "LITTEST"
type resolvedTarget struct {
sourceFile string
genTarget string
pkgDir string
modulePath string
pkgPath string
}
type irProgram struct {
globals []irGlobal
funcs []irFunction
}
type irGlobal struct {
symbol string
line string
}
type irFunction struct {
symbol string
lines []string
}
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]+`)
attrGroupTailRE = regexp.MustCompile(`\s+#\d+$`)
numericNameRE = regexp.MustCompile(`^\d+$`)
)
func generateFile(target resolvedTarget) 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
}
updated, err := rewriteSource(cleaned, target.sourceFile, target.pkgPath, target.modulePath, ir)
if err != nil {
return err
}
formatted, err := format.Source([]byte(updated))
if err != nil {
return fmt.Errorf("%s: gofmt failed: %w", target.sourceFile, err)
}
return os.WriteFile(target.sourceFile, formatted, 0644)
}
func resolveTarget(sourceFile, genTarget string) (resolvedTarget, error) {
pkgDir := filepath.Dir(sourceFile)
root, goMod, err := mod.FindGoMod(pkgDir)
if err != nil {
return resolvedTarget{}, err
}
modulePath, err := readModulePath(goMod)
if err != nil {
return resolvedTarget{}, err
}
pkgPath, err := packagePath(modulePath, root, pkgDir)
if err != nil {
return resolvedTarget{}, err
}
return resolvedTarget{
sourceFile: sourceFile,
genTarget: genTarget,
pkgDir: pkgDir,
modulePath: modulePath,
pkgPath: pkgPath,
}, nil
}
func genIR(target string) (ret string, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("llgen failed for %s: %v", target, r)
}
}()
return llgen.GenFrom(target), nil
}
func readModulePath(goMod string) (string, error) {
f, err := os.Open(goMod)
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "//") {
continue
}
if strings.HasPrefix(line, "module ") {
return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
}
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", fmt.Errorf("%s: module directive not found", goMod)
}
func packagePath(modulePath, root, pkgDir string) (string, error) {
rel, err := filepath.Rel(root, pkgDir)
if err != nil {
return "", err
}
if rel == "." {
return modulePath, nil
}
return path.Join(modulePath, filepath.ToSlash(rel)), nil
}
func rewriteSource(src, srcPath, pkgPath, modulePath, ir string) (string, error) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, srcPath, src, parser.ParseComments)
if err != nil {
return "", err
}
prog := parseIR(ir)
anchors, topPos := collectAnchors(src, fset, file)
injections := make(map[int][]string)
if globals := buildGlobalChecks(prog, modulePath); len(globals) != 0 {
injections[topPos] = append(injections[topPos], formatDirectiveBlock(indentAt(src, topPos), globals))
}
eof := len(src)
lastOffset := topPos
for _, fn := range prog.funcs {
if shouldSkipFunctionCheck(fn.symbol) {
continue
}
lines := buildFunctionChecks(fn, modulePath)
if len(lines) == 0 {
continue
}
offset := eof
if name, ok := trimPkgPrefix(fn.symbol, pkgPath); ok {
if pos, found := anchors[name]; found {
offset = pos
}
}
if offset < lastOffset {
offset = lastOffset
}
injections[offset] = append(injections[offset], formatDirectiveBlock(indentAt(src, offset), lines))
lastOffset = offset
}
return applyInjections(src, injections), nil
}
func collectAnchors(src string, fset *token.FileSet, file *ast.File) (map[string]int, int) {
anchors := make(map[string]int)
topPos := topInsertPos(src, fset, file)
if initPos, ok := syntheticInitPos(src, fset, file); ok {
anchors["init"] = initPos
}
for _, decl := range file.Decls {
switch d := decl.(type) {
case *ast.FuncDecl:
name := inPkgFuncName(d)
anchors[name] = declInsertPos(src, fset, d.Pos(), d.Doc)
collectFuncLitAnchors(src, fset, d.Body, name, anchors)
case *ast.GenDecl:
if d.Tok == token.IMPORT {
continue
}
collectFuncLitAnchors(src, fset, d, "init", anchors)
}
}
return anchors, topPos
}
func topInsertPos(src string, fset *token.FileSet, file *ast.File) int {
for _, decl := range file.Decls {
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT {
continue
}
return lineStart(src, offsetOf(fset, decl.Pos()))
}
return len(src)
}
func syntheticInitPos(src string, fset *token.FileSet, file *ast.File) (int, bool) {
for _, decl := range file.Decls {
switch d := decl.(type) {
case *ast.FuncDecl:
if d.Name.Name == "init" {
return declInsertPos(src, fset, d.Pos(), d.Doc), true
}
case *ast.GenDecl:
if d.Tok != token.IMPORT {
return declInsertPos(src, fset, d.Pos(), d.Doc), true
}
}
}
return 0, false
}
func declInsertPos(src string, fset *token.FileSet, pos token.Pos, doc *ast.CommentGroup) int {
if doc != nil {
pos = doc.Pos()
}
return lineStart(src, offsetOf(fset, pos))
}
func collectFuncLitAnchors(src string, fset *token.FileSet, node ast.Node, parent string, anchors map[string]int) {
if isNilNode(node) {
return
}
counts := make(map[string]int)
var walk func(ast.Node, string)
walk = func(root ast.Node, current string) {
if isNilNode(root) {
return
}
ast.Inspect(root, func(n ast.Node) bool {
lit, ok := n.(*ast.FuncLit)
if !ok {
return true
}
counts[current]++
name := fmt.Sprintf("%s$%d", current, counts[current])
anchors[name] = lineStart(src, offsetOf(fset, lit.Pos()))
walk(lit.Body, name)
return false
})
}
walk(node, parent)
}
func isNilNode(node ast.Node) bool {
if node == nil {
return true
}
v := reflect.ValueOf(node)
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
default:
return false
}
}
func inPkgFuncName(fn *ast.FuncDecl) string {
name := fn.Name.Name
if fn.Recv == nil || len(fn.Recv.List) == 0 {
return name
}
recv := fn.Recv.List[0].Type
if star, ok := recv.(*ast.StarExpr); ok {
return "(*" + recvTypeName(star.X) + ")." + name
}
return recvTypeName(recv) + "." + name
}
func recvTypeName(expr ast.Expr) string {
switch v := expr.(type) {
case *ast.Ident:
return v.Name
case *ast.SelectorExpr:
return recvTypeName(v.X) + "." + v.Sel.Name
case *ast.IndexExpr:
return recvTypeName(v.X)
case *ast.IndexListExpr:
return recvTypeName(v.X)
default:
return ""
}
}
func parseIR(ir string) irProgram {
lines := splitIRLines(ir)
var prog irProgram
for i := 0; i < len(lines); i++ {
line := lines[i]
if strings.HasPrefix(line, "define ") {
j := i + 1
for j < len(lines) {
if lines[j] == "}" {
j++
break
}
j++
}
block := append([]string(nil), lines[i:j]...)
prog.funcs = append(prog.funcs, irFunction{
symbol: extractDefineSymbol(line),
lines: block,
})
i = j - 1
continue
}
if symbol, ok := extractGlobalSymbol(line); ok {
prog.globals = append(prog.globals, irGlobal{symbol: symbol, line: line})
}
}
return prog
}
func splitIRLines(ir string) []string {
ir = strings.ReplaceAll(ir, "\r\n", "\n")
ir = strings.TrimSuffix(ir, "\n")
if ir == "" {
return nil
}
return strings.Split(ir, "\n")
}
func extractDefineSymbol(line string) string {
if m := defineQuotedRE.FindStringSubmatch(line); m != nil {
return m[1]
}
if m := definePlainRE.FindStringSubmatch(line); m != nil {
return m[1]
}
return ""
}
func extractGlobalSymbol(line string) (string, bool) {
if m := globalQuotedRE.FindStringSubmatch(line); m != nil {
return m[1], true
}
if m := globalPlainRE.FindStringSubmatch(line); m != nil {
return m[1], true
}
return "", false
}
func buildGlobalChecks(prog irProgram, modulePath string) []string {
defs := make(map[string]string, len(prog.globals))
order := make([]string, 0, len(prog.globals))
for _, g := range prog.globals {
if !shouldCheckGlobal(g.symbol) {
continue
}
defs[g.symbol] = g.line
order = append(order, g.symbol)
}
if len(defs) == 0 {
return nil
}
needed := make(map[string]bool)
for _, fn := range prog.funcs {
if shouldSkipFunctionCheck(fn.symbol) {
continue
}
for _, line := range fn.lines[1:] {
for _, ref := range collectRefs(line) {
if _, ok := defs[ref]; ok {
needed[ref] = true
}
}
}
}
var lines []string
for _, symbol := range order {
if !needed[symbol] {
continue
}
lines = append(lines, "// CHECK-LINE: "+generalizeIRLine(defs[symbol], modulePath))
}
return lines
}
func buildFunctionChecks(fn irFunction, modulePath string) []string {
if len(fn.lines) == 0 {
return nil
}
lines := make([]string, 0, len(fn.lines))
lines = append(lines, "// CHECK-LABEL: "+generalizeDefineLine(fn.lines[0], modulePath))
for _, line := range fn.lines[1:] {
if strings.TrimSpace(line) == "" {
lines = append(lines, "// CHECK-EMPTY:")
continue
}
lines = append(lines, "// CHECK-NEXT: "+generalizeIRLine(line, modulePath))
}
return lines
}
func generalizeDefineLine(line, modulePath string) string {
line = scrubIRLine(line)
if idx := strings.LastIndex(line, " {"); idx >= 0 {
head := attrGroupTailRE.ReplaceAllString(line[:idx], "")
line = head + line[idx:]
}
return generalizeModulePath(line, modulePath)
}
func generalizeIRLine(line, modulePath string) string {
return generalizeModulePath(scrubIRLine(line), modulePath)
}
func scrubIRLine(line string) string {
line = debugMetaRE.ReplaceAllString(line, "")
return strings.TrimRight(line, " \t")
}
func generalizeModulePath(line, modulePath string) string {
if modulePath == "" {
return line
}
return strings.ReplaceAll(line, modulePath, "{{.*}}")
}
func shouldCheckGlobal(symbol string) bool {
return numericNameRE.MatchString(symbol)
}
func collectRefs(line string) []string {
matches := globalRefRE.FindAllStringSubmatch(line, -1)
if len(matches) == 0 {
return nil
}
refs := make([]string, 0, len(matches))
for _, m := range matches {
if m[1] != "" {
refs = append(refs, m[1])
continue
}
if m[2] != "" {
refs = append(refs, m[2])
}
}
return refs
}
func shouldSkipFunctionCheck(symbol string) bool {
base := strings.TrimPrefix(symbol, "__llgo_stub.")
return strings.HasSuffix(base, "/runtime/internal/runtime.memequal32") ||
strings.HasSuffix(base, "/runtime/internal/runtime.memequalptr") ||
strings.HasSuffix(base, "/runtime/internal/runtime.strequal")
}
func trimPkgPrefix(symbol, pkgPath string) (string, bool) {
prefix := pkgPath + "."
if strings.HasPrefix(symbol, prefix) {
return strings.TrimPrefix(symbol, prefix), true
}
return "", false
}
func applyInjections(src string, injections map[int][]string) string {
if len(injections) == 0 {
return src
}
offsets := make([]int, 0, len(injections))
for offset := range injections {
offsets = append(offsets, offset)
}
sort.Sort(sort.Reverse(sort.IntSlice(offsets)))
out := src
for _, offset := range offsets {
block := strings.Join(injections[offset], "")
out = out[:offset] + block + out[offset:]
}
return out
}
func formatDirectiveBlock(indent string, lines []string) string {
var b strings.Builder
for _, line := range lines {
b.WriteString(indent)
b.WriteString(line)
b.WriteByte('\n')
}
b.WriteByte('\n')
return b.String()
}
func stripCheckDirectives(src string) string {
src = strings.ReplaceAll(src, "\r\n", "\n")
lines := strings.SplitAfter(src, "\n")
if len(lines) == 0 {
return src
}
out := make([]string, 0, len(lines))
for _, line := range lines {
trimmed := strings.TrimRight(line, "\n")
if checkLineRE.MatchString(trimmed) {
continue
}
out = append(out, line)
}
return strings.Join(out, "")
}
func lineStart(src string, offset int) int {
if offset < 0 {
return 0
}
if offset > len(src) {
return len(src)
}
for offset > 0 && src[offset-1] != '\n' {
offset--
}
return offset
}
func indentAt(src string, offset int) string {
if offset >= len(src) {
return ""
}
start := lineStart(src, offset)
end := start
for end < len(src) && (src[end] == ' ' || src[end] == '\t') {
end++
}
return src[start:end]
}
func offsetOf(fset *token.FileSet, pos token.Pos) int {
return fset.PositionFor(pos, false).Offset
}
func findMarkedSourceFile(dir string) (string, bool, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return "", false, err
}
var marked string
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !isSourceSpecFile(name) {
continue
}
path := filepath.Join(dir, name)
ok, err := hasLITTESTMarker(path)
if err != nil {
return "", false, err
}
if !ok {
continue
}
if marked != "" {
return "", false, fmt.Errorf("%s: multiple // LITTEST sources found: %s, %s", dir, filepath.Base(marked), name)
}
marked = path
}
if marked == "" {
return "", false, nil
}
return marked, true, nil
}
func hasLITTESTMarker(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {
return false, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
return false, scanner.Err()
}
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "//") {
return false, nil
}
return strings.TrimSpace(strings.TrimPrefix(line, "//")) == littestMarker, nil
}
func isSourceSpecFile(name string) bool {
return filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go")
}
+185
View File
@@ -0,0 +1,185 @@
package main
import (
"strings"
"testing"
)
func TestRewriteSource_InsertsMainClosureAndStub(t *testing.T) {
const src = `// LITTEST
package main
func main() {
fn := func() {}
fn()
}
`
const ir = `define void @"example.com/p.main"() {
_llgo_0:
%0 = call ptr @"example.com/p.main$1"()
ret void
}
define void @"example.com/p.main$1"() {
_llgo_0:
ret void
}
define linkonce void @"__llgo_stub.example.com/p.main$1"(ptr %0) {
_llgo_0:
tail call void @"example.com/p.main$1"()
ret void
}
`
got, err := rewriteSource(src, "in.go", "example.com/p", "example.com", ir)
if err != nil {
t.Fatal(err)
}
mainCheck := `// CHECK-LABEL: define void @"{{.*}}/p.main"() {`
mainDecl := "func main() {"
if !strings.Contains(got, mainCheck) {
t.Fatalf("main checks not inserted before func main:\n%s", got)
}
if strings.Index(got, mainCheck) > strings.Index(got, mainDecl) {
t.Fatalf("main checks should appear before func main:\n%s", got)
}
closureCheck := "\t// CHECK-LABEL: define void @\"{{.*}}/p.main$1\"() {"
closureStmt := "\tfn := func() {}"
if !strings.Contains(got, closureCheck) {
t.Fatalf("closure checks not inserted before func literal:\n%s", got)
}
if strings.Index(got, closureCheck) > strings.Index(got, closureStmt) {
t.Fatalf("closure checks should appear before func literal:\n%s", got)
}
if !strings.Contains(got, `// CHECK-LABEL: define linkonce void @"__llgo_stub.{{.*}}/p.main$1"(ptr %0) {`) {
t.Fatalf("stub checks missing:\n%s", got)
}
if strings.Index(got, `// CHECK-LABEL: define linkonce void @"__llgo_stub.{{.*}}/p.main$1"(ptr %0) {`) < strings.Index(got, "func main()") {
t.Fatalf("stub checks should be appended after source:\n%s", got)
}
}
func TestRewriteSource_AddsInitAndCheckEmptyAndSkipsHelpers(t *testing.T) {
const src = `// LITTEST
package main
var x = 1
func main() {}
`
const ir = `define void @"example.com/p.init"() {
_llgo_0:
br i1 true, label %_llgo_1, label %_llgo_2
_llgo_1:
ret void
_llgo_2:
ret void
}
define i1 @"example.com/runtime/internal/runtime.strequal"(ptr %0, ptr %1) {
_llgo_0:
ret i1 true
}
define void @"example.com/p.main"() {
_llgo_0:
ret void
}
`
got, err := rewriteSource(src, "in.go", "example.com/p", "example.com", ir)
if err != nil {
t.Fatal(err)
}
initCheck := `// CHECK-LABEL: define void @"{{.*}}/p.init"() {`
if !strings.Contains(got, initCheck) {
t.Fatalf("init checks not inserted before var decl:\n%s", got)
}
if strings.Index(got, initCheck) > strings.Index(got, "var x = 1") {
t.Fatalf("init checks should appear before var decl:\n%s", got)
}
if !strings.Contains(got, "// CHECK-EMPTY:") {
t.Fatalf("blank IR lines should use CHECK-EMPTY:\n%s", got)
}
if strings.Contains(got, "runtime.strequal") {
t.Fatalf("runtime.strequal helper should be skipped:\n%s", got)
}
}
func TestRewriteSource_PreservesIROrderWhenAnchorMovesBackward(t *testing.T) {
const src = `// LITTEST
package main
var seed = 40
func add(x, y int) int {
return x + y
}
func main() {}
`
const ir = `define i64 @"example.com/p.add"(i64 %0, i64 %1) {
_llgo_0:
%2 = add i64 %0, %1
ret i64 %2
}
define void @"example.com/p.init"() {
_llgo_0:
ret void
}
define void @"example.com/p.main"() {
_llgo_0:
ret void
}
`
got, err := rewriteSource(src, "in.go", "example.com/p", "example.com", ir)
if err != nil {
t.Fatal(err)
}
addCheck := `// CHECK-LABEL: define i64 @"{{.*}}/p.add"(i64 %0, i64 %1) {`
initCheck := `// CHECK-LABEL: define void @"{{.*}}/p.init"() {`
if strings.Index(got, addCheck) < 0 || strings.Index(got, initCheck) < 0 {
t.Fatalf("missing checks:\n%s", got)
}
if strings.Index(got, addCheck) > strings.Index(got, initCheck) {
t.Fatalf("IR order should be preserved even if init anchor is earlier:\n%s", got)
}
}
func TestRewriteSource_AddsReferencedNumericGlobalsAtTop(t *testing.T) {
const src = `// LITTEST
package main
func main() {}
`
const ir = `@0 = private unnamed_addr constant [4 x i8] c"Hi\0A\00", align 1
@1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1
@"example.com/p.named" = global i64 1
define void @"example.com/p.main"() {
_llgo_0:
call void @puts(ptr @0)
call void @printf(ptr @1)
ret void
}
`
got, err := rewriteSource(src, "in.go", "example.com/p", "example.com", ir)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, `// CHECK-LINE: @0 = 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-LINE: @1 = 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-LINE: @"{{.*}}/p.named" = global i64 1`) {
t.Fatalf("named globals should not be emitted by default:\n%s", got)
}
if strings.Index(got, `// CHECK-LINE: @0 = 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)
}
}
+51
View File
@@ -0,0 +1,51 @@
// LITTEST
package main
var seed = 40
// CHECK-LABEL: define i64 @"{{.*}}/cl/_testrt/litdemo.add"(i64 %0, i64 %1) {
// CHECK-NEXT: _llgo_0:
// CHECK-NEXT: %2 = add i64 %0, %1
// CHECK-NEXT: ret i64 %2
// CHECK-NEXT: }
// CHECK-LABEL: define void @"{{.*}}/cl/_testrt/litdemo.init"() {
// CHECK-NEXT: _llgo_0:
// CHECK-NEXT: %0 = load i1, ptr @"{{.*}}/cl/_testrt/litdemo.init$guard", align 1
// CHECK-NEXT: br i1 %0, label %_llgo_2, label %_llgo_1
// CHECK-EMPTY:
// CHECK-NEXT: _llgo_1: ; preds = %_llgo_0
// CHECK-NEXT: store i1 true, ptr @"{{.*}}/cl/_testrt/litdemo.init$guard", align 1
// CHECK-NEXT: store i64 40, ptr @"{{.*}}/cl/_testrt/litdemo.seed", align 4
// CHECK-NEXT: br label %_llgo_2
// CHECK-EMPTY:
// CHECK-NEXT: _llgo_2: ; preds = %_llgo_1, %_llgo_0
// CHECK-NEXT: ret void
// CHECK-NEXT: }
func add(x, y int) int {
return x + y
}
// CHECK-LABEL: define void @"{{.*}}/cl/_testrt/litdemo.main"() {
// CHECK-NEXT: _llgo_0:
// CHECK-NEXT: %0 = load i64, ptr @"{{.*}}/cl/_testrt/litdemo.seed", align 4
// CHECK-NEXT: %1 = call i64 @"{{.*}}/cl/_testrt/litdemo.main$1"(i64 %0)
// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %1)
// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10)
// CHECK-NEXT: ret void
// CHECK-NEXT: }
func main() {
// CHECK-LABEL: define i64 @"{{.*}}/cl/_testrt/litdemo.main$1"(i64 %0) {
// CHECK-NEXT: _llgo_0:
// CHECK-NEXT: %1 = call i64 @"{{.*}}/cl/_testrt/litdemo.add"(i64 %0, i64 2)
// CHECK-NEXT: ret i64 %1
// CHECK-NEXT: }
plusTwo := func(v int) int {
return add(v, 2)
}
println(plusTwo(seed))
}
+63
View File
@@ -71,3 +71,66 @@ You can control demo parallelism via `LLGO_DEMO_JOBS` (defaults to up to 4 jobs)
- If `[command...]` is omitted, it starts an interactive `bash`.
- If `[command...]` is provided, it runs that command and exits.
- You must run it from within the repo (within `LLGO_ROOT`), and it will start in the matching repo subdirectory inside the container.
## 6) Refresh test goldens
LLGo currently has two different golden-test refresh flows:
- `gentests` for directory-based golden files such as `out.ll` and `expect.txt`
- `litgen` for source-embedded `// LITTEST` FileCheck directives
### `gentests`
Run:
```bash
go run ./chore/gentests
```
Behavior:
- Refreshes `out.ll` for the built-in test suites under `cl/_testlibc`, `cl/_testlibgo`, `cl/_testrt`, `cl/_testgo`, `cl/_testpy`, and `cl/_testdata`.
- Refreshes `expect.txt` for the same directories using the existing runtime execution flow.
- Preserves the existing skip convention where `out.ll` or `expect.txt` containing only `;` means "do not refresh".
- New behavior: if a test case directory contains a non-test Go source file whose first line is exactly `// LITTEST`, `gentests` skips `llgen` for that directory and does not regenerate `out.ll` there.
Use `gentests` when the test still stores LLVM IR in `out.ll`.
### `litgen`
Run on a single marked file:
```bash
go run ./chore/litgen path/to/in.go
```
Run on a directory tree:
```bash
go run ./chore/litgen cl/_testrt/litdemo
go run ./chore/litgen 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.
- 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`.
### Marker convention
Source-embedded IR checks are enabled by putting this marker on the first line of the source file:
```go
// LITTEST
```
The generated directives are consumed by the existing `littest`/FileCheck path in the compiler tests.
Example:
- [cl/_testrt/litdemo/in.go](../cl/_testrt/litdemo/in.go) is a minimal `_testrt` case that demonstrates `litgen` output.