mirror of
https://github.com/goplus/llgo.git
synced 2026-04-22 15:57:31 +08:00
test: add litgen tool for source IR checks
This commit is contained in:
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user