diff --git a/internal/build/build.go b/internal/build/build.go index 243dfd8665..2b2609f28d 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -297,6 +297,23 @@ func Do(args []string, conf *Config) ([]Package, error) { if patterns == nil { patterns = []string{"."} } + sourcePatchGOROOT, err := env.GOROOTWithEnv(cfg.Env) + if err != nil { + return nil, err + } + sourcePatchGoVersion, err := env.GOVERSIONWithEnv(cfg.Env) + if err != nil { + return nil, err + } + cfg.Overlay, err = buildSourcePatchOverlayForGOROOT(cfg.Overlay, env.LLGoRuntimeDir(), sourcePatchGOROOT, sourcePatchBuildContext{ + goos: conf.Goos, + goarch: conf.Goarch, + goversion: sourcePatchGoVersion, + buildFlags: cfg.BuildFlags, + }) + if err != nil { + return nil, err + } initial, err := packages.LoadEx(dedup, sizes, cfg, patterns...) if err != nil { return nil, err diff --git a/internal/build/source_patch.go b/internal/build/source_patch.go new file mode 100644 index 0000000000..bb04943690 --- /dev/null +++ b/internal/build/source_patch.go @@ -0,0 +1,741 @@ +package build + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/build" + "go/parser" + "go/token" + "os" + "path/filepath" + "runtime" + "slices" + "strconv" + "strings" + "syscall" + + llruntime "github.com/goplus/llgo/runtime" + "golang.org/x/tools/go/ast/astutil" +) + +func cloneOverlay(src map[string][]byte) map[string][]byte { + if len(src) == 0 { + return nil + } + dup := make(map[string][]byte, len(src)) + for k, v := range src { + dup[k] = slices.Clone(v) + } + return dup +} + +type sourcePatchBuildContext struct { + goos string + goarch string + goversion string + buildFlags []string +} + +func buildSourcePatchOverlay(base map[string][]byte, runtimeDir string) (map[string][]byte, error) { + return buildSourcePatchOverlayForGOROOT(base, runtimeDir, runtime.GOROOT(), sourcePatchBuildContext{}) +} + +func buildSourcePatchOverlayForGOROOT(base map[string][]byte, runtimeDir, goroot string, ctx sourcePatchBuildContext) (map[string][]byte, error) { + var out map[string][]byte + for _, pkgPath := range llruntime.SourcePatchPkgPaths() { + changed, next, err := applySourcePatchForPkg(base, out, runtimeDir, goroot, pkgPath, ctx) + if err != nil { + return nil, err + } + if !changed { + continue + } + out = next + } + if out == nil { + return base, nil + } + return out, nil +} + +func applySourcePatchForPkg(base, current map[string][]byte, runtimeDir, goroot, pkgPath string, ctx sourcePatchBuildContext) (bool, map[string][]byte, error) { + patchDir := filepath.Join(runtimeDir, "_patch", filepath.FromSlash(pkgPath)) + entries, err := os.ReadDir(patchDir) + if err != nil { + if os.IsNotExist(err) { + return false, current, nil + } + return false, nil, fmt.Errorf("read source patch dir %s: %w", pkgPath, err) + } + + srcDir := filepath.Join(goroot, "src", filepath.FromSlash(pkgPath)) + srcEntries, err := os.ReadDir(srcDir) + if err != nil { + if os.IsNotExist(err) { + return false, current, nil + } + if errors.Is(err, syscall.EACCES) || errors.Is(err, syscall.EPERM) { + return false, current, nil + } + return false, nil, fmt.Errorf("read stdlib dir %s: %w", pkgPath, err) + } + + var ( + out = current + changed bool + patchSrcs = make(map[string][]byte) + skipAll bool + skips = make(map[string]struct{}) + ) + readOverlay := func(filename string) ([]byte, error) { + if out != nil { + if src, ok := out[filename]; ok { + return src, nil + } + } + if src, ok := base[filename]; ok { + return src, nil + } + return os.ReadFile(filename) + } + buildCtx, err := newSourcePatchMatchContext(goroot, ctx) + if err != nil { + return false, nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + match, err := buildCtx.MatchFile(patchDir, name) + if err != nil { + return false, nil, fmt.Errorf("match source patch file %s: %w", filepath.Join(patchDir, name), err) + } + if !match { + continue + } + filename := filepath.Join(patchDir, name) + src, err := readOverlay(filename) + if err != nil { + return false, nil, fmt.Errorf("read source patch file %s: %w", filename, err) + } + directives, err := collectSourcePatchDirectives(src) + if err != nil { + return false, nil, fmt.Errorf("parse source patch directives %s: %w", filename, err) + } + patchSrcs[name] = buildInjectedSourcePatchFile(filename, src) + if directives.skipAll { + skipAll = true + } + for name := range directives.skips { + skips[name] = struct{}{} + } + } + if len(patchSrcs) == 0 { + return false, current, nil + } + + ensureOverlay := func() { + if out == nil { + out = cloneOverlay(base) + if out == nil { + out = make(map[string][]byte) + } + } + } + + if skipAll { + for _, entry := range srcEntries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + filename := filepath.Join(srcDir, name) + src, err := readOverlay(filename) + if err != nil { + return false, nil, fmt.Errorf("read stdlib source file %s: %w", filename, err) + } + stub, err := packageStubSource(src) + if err != nil { + return false, nil, fmt.Errorf("build stdlib stub %s: %w", filename, err) + } + ensureOverlay() + out[filename] = stub + changed = true + } + } else if len(skips) != 0 { + for _, entry := range srcEntries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + filename := filepath.Join(srcDir, name) + src, err := readOverlay(filename) + if err != nil { + return false, nil, fmt.Errorf("read stdlib source file %s: %w", filename, err) + } + filtered, changedFile, err := filterSourcePatchFile(src, skips) + if err != nil { + return false, nil, fmt.Errorf("filter stdlib source file %s: %w", filename, err) + } + if !changedFile { + continue + } + ensureOverlay() + out[filename] = filtered + changed = true + } + } + + for name, src := range patchSrcs { + target := filepath.Join(srcDir, "z_llgo_patch_"+name) + ensureOverlay() + out[target] = src + changed = true + } + return changed, out, nil +} + +func newSourcePatchMatchContext(goroot string, ctx sourcePatchBuildContext) (build.Context, error) { + buildCtx := build.Default + if goroot != "" { + buildCtx.GOROOT = goroot + } + if ctx.goos != "" { + buildCtx.GOOS = ctx.goos + } + if ctx.goarch != "" { + buildCtx.GOARCH = ctx.goarch + } + buildCtx.BuildTags = parseSourcePatchBuildTags(ctx.buildFlags) + if ctx.goversion != "" { + releaseTags, err := releaseTagsForGoVersion(ctx.goversion) + if err != nil { + return build.Context{}, err + } + buildCtx.ReleaseTags = releaseTags + } + return buildCtx, nil +} + +func parseSourcePatchBuildTags(buildFlags []string) []string { + var tags []string + for i := 0; i < len(buildFlags); i++ { + flag := buildFlags[i] + if flag == "-tags" && i+1 < len(buildFlags) { + tags = append(tags, splitSourcePatchBuildTags(buildFlags[i+1])...) + i++ + continue + } + if strings.HasPrefix(flag, "-tags=") { + tags = append(tags, splitSourcePatchBuildTags(strings.TrimPrefix(flag, "-tags="))...) + } + } + return slices.Compact(tags) +} + +func splitSourcePatchBuildTags(s string) []string { + return strings.FieldsFunc(s, func(r rune) bool { + return r == ',' || r == ' ' + }) +} + +func releaseTagsForGoVersion(goVersion string) ([]string, error) { + goVersion = strings.TrimPrefix(goVersion, "go") + parts := strings.Split(goVersion, ".") + if len(parts) < 2 { + return nil, fmt.Errorf("unsupported Go version %q", goVersion) + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, fmt.Errorf("parse Go major version %q: %w", goVersion, err) + } + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, fmt.Errorf("parse Go minor version %q: %w", goVersion, err) + } + if major != 1 || minor < 1 { + return nil, fmt.Errorf("unsupported Go version %q", goVersion) + } + tags := make([]string, 0, minor) + for v := 1; v <= minor; v++ { + tags = append(tags, fmt.Sprintf("go1.%d", v)) + } + return tags, nil +} + +func packageStubSource(src []byte) ([]byte, error) { + lines := strings.SplitAfter(string(src), "\n") + var buf strings.Builder + for _, line := range lines { + buf.WriteString(line) + if strings.HasPrefix(strings.TrimSpace(line), "package ") { + return []byte(buf.String()), nil + } + } + return nil, fmt.Errorf("package clause not found") +} + +type sourcePatchDirectives struct { + skipAll bool + skips map[string]struct{} +} + +func collectSourcePatchDirectives(src []byte) (sourcePatchDirectives, error) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "", src, parser.ParseComments) + if err != nil { + return sourcePatchDirectives{}, err + } + d := sourcePatchDirectives{skips: make(map[string]struct{})} + for _, group := range file.Comments { + for _, comment := range group.List { + line := strings.TrimSpace(comment.Text) + skipAll, names, ok := parseSourcePatchDirective(line) + if !ok { + continue + } + if skipAll { + d.skipAll = true + } + for _, name := range names { + d.skips[name] = struct{}{} + } + } + } + for _, decl := range file.Decls { + for _, name := range declPatchKeys(decl) { + d.skips[name] = struct{}{} + } + } + return d, nil +} + +// parseSourcePatchDirective recognizes build-time source-patch directives. +// +// Supported directives: +// - //llgo:skipall: clear every stdlib .go file in the patched package to a package stub +// - //llgo:skip A B: comment the named declarations from the stdlib package view +// +// Unlike cl/import.go directives, these are consumed only while constructing the +// load-time overlay and are rewritten to plain comments before type checking. +func parseSourcePatchDirective(line string) (skipAll bool, names []string, ok bool) { + const ( + llgo1 = "//llgo:" + llgo2 = "// llgo:" + go1 = "//go:" + ) + if strings.HasPrefix(line, go1) { + return false, nil, false + } + var tail string + switch { + case strings.HasPrefix(line, llgo1): + tail = line[len(llgo1):] + case strings.HasPrefix(line, llgo2): + tail = line[len(llgo2):] + default: + return false, nil, false + } + switch { + case tail == "skipall": + return true, nil, true + case strings.HasPrefix(tail, "skip "): + return false, strings.Fields(tail[len("skip "):]), true + default: + return false, nil, false + } +} + +func sanitizeSourcePatchDirectiveLines(src []byte) []byte { + out := slices.Clone(src) + lines := bytes.SplitAfter(out, []byte{'\n'}) + changed := false + markDirective := func(line []byte, prefix []byte) bool { + idx := bytes.Index(line, prefix) + if idx < 0 { + return false + } + line[idx+len(prefix)-1] = '_' + return true + } + for _, line := range lines { + trimmed := strings.TrimSpace(string(line)) + if _, _, ok := parseSourcePatchDirective(trimmed); !ok { + continue + } + switch { + case markDirective(line, []byte("//llgo:")): + changed = true + case markDirective(line, []byte("// llgo:")): + changed = true + } + } + if !changed { + return src + } + return out +} + +func buildInjectedSourcePatchFile(filename string, src []byte) []byte { + sanitized := sanitizeSourcePatchDirectiveLines(src) + var out bytes.Buffer + out.WriteString("//line ") + out.WriteString(filepath.ToSlash(filename)) + out.WriteString(":1\n") + out.Write(sanitized) + return out.Bytes() +} + +func filterSourcePatchFile(src []byte, skips map[string]struct{}) ([]byte, bool, error) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "", src, parser.ParseComments) + if err != nil { + return nil, false, err + } + tokFile := fset.File(file.Pos()) + if tokFile == nil { + return nil, false, fmt.Errorf("missing file positions") + } + changed := false + var spans []sourcePatchSpan + decls := file.Decls[:0] + for _, decl := range file.Decls { + filteredDecl, removed, rmSpans, err := filterSourcePatchDecl(tokFile, decl, skips) + if err != nil { + return nil, false, err + } + if removed { + changed = true + } + spans = append(spans, rmSpans...) + if filteredDecl != nil { + decls = append(decls, filteredDecl) + } + } + if !changed { + return src, false, nil + } + file.Decls = decls + spans = append(spans, collectUnusedImportSpans(tokFile, file)...) + if len(spans) == 0 { + return src, false, nil + } + return commentSourcePatchSpans(src, spans), true, nil +} + +type sourcePatchSpan struct { + start int + end int +} + +func filterSourcePatchDecl(tokFile *token.File, decl ast.Decl, skips map[string]struct{}) (ast.Decl, bool, []sourcePatchSpan, error) { + switch decl := decl.(type) { + case *ast.FuncDecl: + if _, ok := skips[declPatchKeyFunc(decl)]; ok { + return nil, true, nodeAndCommentsSpans(tokFile, decl), nil + } + return decl, false, nil, nil + case *ast.GenDecl: + switch decl.Tok { + case token.TYPE, token.VAR, token.CONST: + default: + return decl, false, nil, nil + } + specs := decl.Specs[:0] + removedAny := false + var removedSpans []sourcePatchSpan + for _, spec := range decl.Specs { + filteredSpec, removed, spans := filterSourcePatchSpec(tokFile, spec, skips) + if removed { + removedAny = true + } + removedSpans = append(removedSpans, spans...) + if filteredSpec != nil { + specs = append(specs, filteredSpec) + } + } + if len(specs) == 0 { + return nil, removedAny, nodeAndCommentsSpans(tokFile, decl), nil + } + if !removedAny { + return decl, false, nil, nil + } + out := *decl + out.Specs = specs + return &out, true, removedSpans, nil + default: + return decl, false, nil, nil + } +} + +func filterSourcePatchSpec(tokFile *token.File, spec ast.Spec, skips map[string]struct{}) (ast.Spec, bool, []sourcePatchSpan) { + switch spec := spec.(type) { + case *ast.TypeSpec: + if _, ok := skips[spec.Name.Name]; ok { + return nil, true, nodeAndCommentsSpans(tokFile, spec) + } + return spec, false, nil + case *ast.ValueSpec: + keep := make([]*ast.Ident, 0, len(spec.Names)) + removed := false + for _, name := range spec.Names { + if _, ok := skips[name.Name]; ok { + removed = true + continue + } + keep = append(keep, name) + } + if !removed { + return spec, false, nil + } + if len(keep) == 0 { + return nil, true, nodeAndCommentsSpans(tokFile, spec) + } + if len(keep) != len(spec.Names) { + // Keep the transformation simple and deterministic. Mixed multi-name specs + // can be split later if we need them in real patches. + return nil, true, nodeAndCommentsSpans(tokFile, spec) + } + out := *spec + out.Names = keep + return &out, true, nil + default: + return spec, false, nil + } +} + +func collectUnusedImportSpans(tokFile *token.File, file *ast.File) []sourcePatchSpan { + var spans []sourcePatchSpan + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.IMPORT { + continue + } + removable := make([]*ast.ImportSpec, 0, len(gen.Specs)) + keptCount := 0 + for _, spec := range gen.Specs { + imp := spec.(*ast.ImportSpec) + if imp.Name != nil && imp.Name.Name == "_" { + keptCount++ + continue + } + if astutil.UsesImport(file, strings.Trim(imp.Path.Value, `"`)) { + keptCount++ + continue + } + removable = append(removable, imp) + } + if len(removable) == 0 { + continue + } + if keptCount == 0 || !gen.Lparen.IsValid() || len(gen.Specs) == 1 { + spans = append(spans, nodeAndCommentsSpans(tokFile, gen)...) + continue + } + for _, imp := range removable { + spans = append(spans, nodeAndCommentsSpans(tokFile, imp)...) + } + } + return spans +} + +func nodeAndCommentsSpans(tokFile *token.File, node ast.Node) []sourcePatchSpan { + spans := []sourcePatchSpan{nodeSpan(tokFile, node)} + switch node := node.(type) { + case *ast.FuncDecl: + spans = append(spans, commentGroupsToSpans(tokFile, compactCommentGroups(node.Doc))...) + case *ast.GenDecl: + spans = append(spans, commentGroupsToSpans(tokFile, compactCommentGroups(node.Doc))...) + for _, spec := range node.Specs { + spans = append(spans, commentGroupsToSpans(tokFile, collectSpecComments(spec))...) + } + case *ast.ImportSpec: + spans = append(spans, commentGroupsToSpans(tokFile, compactCommentGroups(node.Doc, node.Comment))...) + default: + if spec, ok := node.(ast.Spec); ok { + spans = append(spans, commentGroupsToSpans(tokFile, collectSpecComments(spec))...) + } + } + return spans +} + +func commentGroupsToSpans(tokFile *token.File, groups []*ast.CommentGroup) []sourcePatchSpan { + out := make([]sourcePatchSpan, 0, len(groups)) + for _, group := range groups { + if group == nil { + continue + } + out = append(out, nodeSpan(tokFile, group)) + } + return out +} + +func nodeSpan(tokFile *token.File, node ast.Node) sourcePatchSpan { + return sourcePatchSpan{ + start: tokFile.Offset(node.Pos()), + end: tokFile.Offset(node.End()), + } +} + +func commentSourcePatchSpans(src []byte, spans []sourcePatchSpan) []byte { + if len(spans) == 0 { + return slices.Clone(src) + } + out := make([]byte, 0, len(src)+len(spans)*4) + slices.SortFunc(spans, func(a, b sourcePatchSpan) int { + switch { + case a.start < b.start: + return -1 + case a.start > b.start: + return 1 + case a.end < b.end: + return -1 + case a.end > b.end: + return 1 + default: + return 0 + } + }) + merged := spans[:0] + for _, span := range spans { + if span.start < 0 { + span.start = 0 + } + if span.end > len(src) { + span.end = len(src) + } + if span.start >= span.end { + continue + } + if len(merged) == 0 || span.start > merged[len(merged)-1].end { + merged = append(merged, span) + continue + } + if span.end > merged[len(merged)-1].end { + merged[len(merged)-1].end = span.end + } + } + cursor := 0 + for _, span := range merged { + if span.start > cursor { + out = append(out, src[cursor:span.start]...) + } + out = appendCommentedSourcePatchSpan(out, src[span.start:span.end]) + cursor = span.end + } + out = append(out, src[cursor:]...) + return out +} + +func appendCommentedSourcePatchSpan(dst, src []byte) []byte { + lines := bytes.SplitAfter(src, []byte{'\n'}) + for _, line := range lines { + if len(line) == 0 { + continue + } + newline := len(line) + if line[newline-1] == '\n' { + newline-- + } + body := line[:newline] + suffix := line[newline:] + if len(bytes.TrimSpace(body)) == 0 { + dst = append(dst, line...) + continue + } + indent := 0 + for indent < len(body) && (body[indent] == ' ' || body[indent] == '\t') { + indent++ + } + dst = append(dst, body[:indent]...) + dst = append(dst, '/', '/') + if indent < len(body) { + dst = append(dst, ' ') + dst = append(dst, body[indent:]...) + } + dst = append(dst, suffix...) + } + return dst +} + +func collectSpecComments(spec ast.Spec) []*ast.CommentGroup { + switch spec := spec.(type) { + case *ast.TypeSpec: + return compactCommentGroups(spec.Doc, spec.Comment) + case *ast.ValueSpec: + return compactCommentGroups(spec.Doc, spec.Comment) + default: + return nil + } +} + +func compactCommentGroups(groups ...*ast.CommentGroup) []*ast.CommentGroup { + var out []*ast.CommentGroup + for _, group := range groups { + if group != nil { + out = append(out, group) + } + } + return out +} + +func declPatchKeys(decl ast.Decl) []string { + switch decl := decl.(type) { + case *ast.FuncDecl: + return []string{declPatchKeyFunc(decl)} + case *ast.GenDecl: + var out []string + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + out = append(out, spec.Name.Name) + case *ast.ValueSpec: + for _, name := range spec.Names { + out = append(out, name.Name) + } + } + } + return out + default: + return nil + } +} + +func declPatchKeyFunc(decl *ast.FuncDecl) string { + if decl.Recv == nil || len(decl.Recv.List) == 0 { + return decl.Name.Name + } + return recvPatchKey(decl.Recv.List[0].Type) + "." + decl.Name.Name +} + +func recvPatchKey(expr ast.Expr) string { + switch expr := expr.(type) { + case *ast.Ident: + return expr.Name + case *ast.ParenExpr: + return recvPatchKey(expr.X) + case *ast.StarExpr: + return "(*" + recvPatchKey(expr.X) + ")" + case *ast.IndexExpr: + return recvPatchKey(expr.X) + case *ast.IndexListExpr: + return recvPatchKey(expr.X) + case *ast.SelectorExpr: + return expr.Sel.Name + default: + panic(fmt.Sprintf("unhandled expression type in recvPatchKey: %T", expr)) + } +} diff --git a/internal/build/source_patch_test.go b/internal/build/source_patch_test.go new file mode 100644 index 0000000000..65ac611d30 --- /dev/null +++ b/internal/build/source_patch_test.go @@ -0,0 +1,471 @@ +package build + +import ( + "go/ast" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "testing" + + "github.com/goplus/llgo/internal/env" + llruntime "github.com/goplus/llgo/runtime" +) + +func TestBuildSourcePatchOverlayForIter(t *testing.T) { + overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), runtime.GOROOT(), sourcePatchBuildContext{}) + if err != nil { + t.Fatal(err) + } + + iterDir := filepath.Join(runtime.GOROOT(), "src", "iter") + patchFile := filepath.Join(iterDir, "z_llgo_patch_iter.go") + patchSrc, ok := overlay[patchFile] + if !ok { + t.Fatalf("missing source patch file %s", patchFile) + } + if !strings.Contains(string(patchSrc), "func Pull[V any]") { + t.Fatalf("source patch file %s does not contain iter replacement", patchFile) + } + if !strings.HasPrefix(string(patchSrc), sourcePatchLineDirective(filepath.Join(env.LLGoRuntimeDir(), "_patch", "iter", "iter.go"))) { + t.Fatalf("source patch file %s is missing line directive, got:\n%s", patchFile, patchSrc) + } + + stdFile := filepath.Join(iterDir, "iter.go") + stdSrc, ok := overlay[stdFile] + if !ok { + t.Fatalf("missing stub overlay for %s", stdFile) + } + got := string(stdSrc) + if !strings.Contains(got, "package iter") { + t.Fatalf("stub overlay for %s lost package clause", stdFile) + } + if strings.Contains(got, "func Pull") { + t.Fatalf("stub overlay for %s still contains original declarations", stdFile) + } +} + +func TestIterUsesSourcePatchInsteadOfAltPkg(t *testing.T) { + if !llruntime.HasSourcePatchPkg("iter") { + t.Fatal("iter should be registered as a source patch package") + } + if llruntime.HasAltPkg("iter") { + t.Fatal("iter should not remain an alt package") + } +} + +func TestSyncAtomicRemainsAltPkg(t *testing.T) { + if llruntime.HasSourcePatchPkg("sync/atomic") { + t.Fatal("sync/atomic should not be registered as a source patch package") + } + if !llruntime.HasAltPkg("sync/atomic") { + t.Fatal("sync/atomic should remain an alt package") + } +} + +func TestInternalRuntimeMapsRemainsAltPkg(t *testing.T) { + if llruntime.HasSourcePatchPkg("internal/runtime/maps") { + t.Fatal("internal/runtime/maps should not be registered as a source patch package") + } + if !llruntime.HasAltPkg("internal/runtime/maps") { + t.Fatal("internal/runtime/maps should remain an alt package") + } +} + +func TestInternalRuntimeSysRemainsAltPkg(t *testing.T) { + if llruntime.HasSourcePatchPkg("internal/runtime/sys") { + t.Fatal("internal/runtime/sys should not be registered as a source patch package") + } + if !llruntime.HasAltPkg("internal/runtime/sys") { + t.Fatal("internal/runtime/sys should remain an alt package") + } + if !llruntime.HasAdditiveAltPkg("internal/runtime/sys") { + t.Fatal("internal/runtime/sys should remain an additive alt package") + } +} + +func TestApplySourcePatchForPkg_Cases(t *testing.T) { + for _, caseName := range []string{ + "default-override", + "generic-constraints-and-interface", + "generic-type-and-method", + "multi-file-skipall", + "multi-file-with-asm", + "skip-and-override", + "skipall", + "type-alias-and-grouped-values", + } { + t.Run(caseName, func(t *testing.T) { + runSourcePatchCase(t, caseName) + }) + } +} + +func TestApplySourcePatchForPkg_MissingStdlibPkg(t *testing.T) { + goroot := t.TempDir() + runtimeDir := t.TempDir() + pkgPath := "iter" + patchDir := filepath.Join(runtimeDir, "_patch", pkgPath) + mustWriteFile(t, filepath.Join(patchDir, "iter.go"), `package iter + +//llgo:skipall + +func Pull[V any](seq func(func(V) bool)) {} +`) + + changed, overlay, err := applySourcePatchForPkg(nil, nil, runtimeDir, goroot, pkgPath, sourcePatchBuildContext{}) + if err != nil { + t.Fatal(err) + } + if changed { + t.Fatal("expected missing stdlib package to skip source patching") + } + if overlay != nil { + t.Fatalf("expected no overlay for missing stdlib package, got %v entries", len(overlay)) + } +} + +func TestApplySourcePatchForPkg_BuildTaggedPatch(t *testing.T) { + goroot := t.TempDir() + runtimeDir := t.TempDir() + pkgPath := "demo" + srcDir := filepath.Join(goroot, "src", pkgPath) + patchDir := filepath.Join(runtimeDir, "_patch", pkgPath) + mustWriteFile(t, filepath.Join(srcDir, "demo.go"), `package demo + +func Old() string { return "old" } +`) + mustWriteFile(t, filepath.Join(patchDir, "patch.go"), `//go:build go1.26 +//llgo:skipall +package demo + +const Only = "patched" +`) + + changed, overlay, err := applySourcePatchForPkg(nil, nil, runtimeDir, goroot, pkgPath, sourcePatchBuildContext{ + goos: runtime.GOOS, + goarch: runtime.GOARCH, + goversion: "go1.24.11", + }) + if err != nil { + t.Fatal(err) + } + if changed { + t.Fatalf("expected go1.26-tagged patch to be ignored on go1.24, got overlay: %#v", overlay) + } + + changed, overlay, err = applySourcePatchForPkg(nil, nil, runtimeDir, goroot, pkgPath, sourcePatchBuildContext{ + goos: runtime.GOOS, + goarch: runtime.GOARCH, + goversion: "go1.26.0", + }) + if err != nil { + t.Fatal(err) + } + if !changed { + t.Fatal("expected go1.26-tagged patch to apply on go1.26") + } +} + +func TestApplySourcePatchForPkg_UnreadableStdlibPkg(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("chmod-based permission test is Unix-only") + } + goroot := t.TempDir() + runtimeDir := t.TempDir() + pkgPath := "iter" + srcDir := filepath.Join(goroot, "src", pkgPath) + patchDir := filepath.Join(runtimeDir, "_patch", pkgPath) + if err := os.MkdirAll(srcDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.Chmod(srcDir, 0); err != nil { + t.Fatal(err) + } + defer os.Chmod(srcDir, 0755) + mustWriteFile(t, filepath.Join(patchDir, "iter.go"), `package iter + +//llgo:skipall + +func Pull[V any](seq func(func(V) bool)) {} +`) + + changed, overlay, err := applySourcePatchForPkg(nil, nil, runtimeDir, goroot, pkgPath, sourcePatchBuildContext{}) + if err != nil { + t.Fatal(err) + } + if changed { + t.Fatal("expected unreadable stdlib package to skip source patching") + } + if overlay != nil { + t.Fatalf("expected no overlay for unreadable stdlib package, got %v entries", len(overlay)) + } +} + +func runSourcePatchCase(t *testing.T, caseName string) { + t.Helper() + + assetRoot := filepath.Join(env.LLGoRuntimeDir(), "_patch", "_test", caseName) + goroot := t.TempDir() + runtimeDir := t.TempDir() + const pkgPath = "demo" + srcDir := filepath.Join(goroot, "src", pkgPath) + patchDir := filepath.Join(runtimeDir, "_patch", pkgPath) + + copyTree(t, filepath.Join(assetRoot, "pkg"), srcDir) + copyTree(t, filepath.Join(assetRoot, "patch"), patchDir) + + changed, overlay, err := applySourcePatchForPkg(nil, nil, runtimeDir, goroot, pkgPath, sourcePatchBuildContext{}) + if err != nil { + t.Fatal(err) + } + if !changed { + t.Fatal("expected source patch overlay to change package") + } + + assertOverlayMatchesOutput(t, overlay, srcDir, filepath.Join(assetRoot, "output"), runtimeDir) + assertGeneratedPatchPositions(t, overlay, srcDir, patchDir) +} + +func copyTree(t *testing.T, srcRoot, dstRoot string) { + t.Helper() + err := filepath.WalkDir(srcRoot, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(srcRoot, path) + if err != nil { + return err + } + target := filepath.Join(dstRoot, rel) + if d.IsDir() { + return os.MkdirAll(target, 0755) + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(target, data, 0644) + }) + if err != nil { + t.Fatal(err) + } +} + +func assertOverlayMatchesOutput(t *testing.T, overlay map[string][]byte, srcRoot, outputRoot, runtimeDir string) { + t.Helper() + + got := overlayFilesUnderRoot(t, overlay, srcRoot) + want := readTextFiles(t, outputRoot, runtimeDir) + + gotNames := sortedMapKeys(got) + wantNames := sortedMapKeys(want) + assertExactString(t, "overlay file list", strings.Join(gotNames, "\n"), strings.Join(wantNames, "\n")) + + for _, name := range wantNames { + assertExactString(t, "overlay file "+name, got[name], want[name]) + } +} + +func overlayFilesUnderRoot(t *testing.T, overlay map[string][]byte, root string) map[string]string { + t.Helper() + out := make(map[string]string) + for filename, src := range overlay { + rel, err := filepath.Rel(root, filename) + if err != nil { + t.Fatal(err) + } + if rel == "." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || rel == ".." { + continue + } + out[filepath.ToSlash(rel)] = string(src) + } + return out +} + +func readTextFiles(t *testing.T, root, runtimeDir string) map[string]string { + t.Helper() + out := make(map[string]string) + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + key := filepath.ToSlash(rel) + if strings.HasSuffix(key, ".txt") { + key = strings.TrimSuffix(key, ".txt") + } + out[key] = expandSourcePatchOutputTemplate(string(data), runtimeDir) + return nil + }) + if err != nil { + t.Fatal(err) + } + return out +} + +func expandSourcePatchOutputTemplate(src, runtimeDir string) string { + patchRoot := filepath.ToSlash(filepath.Join(runtimeDir, "_patch")) + return strings.ReplaceAll(src, "{{PATCH_ROOT}}", patchRoot) +} + +func sortedMapKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func assertGeneratedPatchPositions(t *testing.T, overlay map[string][]byte, srcRoot, patchRoot string) { + t.Helper() + for rel, src := range overlayFilesUnderRoot(t, overlay, srcRoot) { + base := filepath.Base(rel) + if !strings.HasPrefix(base, "z_llgo_patch_") { + continue + } + original := strings.TrimPrefix(base, "z_llgo_patch_") + patchFile := filepath.Join(patchRoot, filepath.Dir(rel), original) + for _, target := range patchedTargetsOfFile(t, patchFile) { + assertPatchedPosition(t, src, filepath.Join(srcRoot, filepath.FromSlash(rel)), patchFile, target.key, target.line) + } + } +} + +type patchedTarget struct { + key string + line int +} + +func patchedTargetsOfFile(t *testing.T, filename string) []patchedTarget { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + targets := []patchedTarget{{ + key: "package", + line: fset.Position(file.Package).Line, + }} + for _, decl := range file.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + key := "func:" + decl.Name.Name + if decl.Recv != nil && len(decl.Recv.List) != 0 { + key = "method:" + recvPatchKey(decl.Recv.List[0].Type) + "." + decl.Name.Name + } + targets = append(targets, patchedTarget{ + key: key, + line: fset.Position(decl.Name.Pos()).Line, + }) + case *ast.GenDecl: + kind := strings.ToLower(decl.Tok.String()) + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + targets = append(targets, patchedTarget{ + key: "type:" + spec.Name.Name, + line: fset.Position(spec.Name.Pos()).Line, + }) + case *ast.ValueSpec: + for _, name := range spec.Names { + targets = append(targets, patchedTarget{ + key: kind + ":" + name.Name, + line: fset.Position(name.Pos()).Line, + }) + } + } + } + } + } + return targets +} + +func mustWriteFile(t *testing.T, filename, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filename, []byte(content), 0644); err != nil { + t.Fatal(err) + } +} + +func sourcePatchLineDirective(filename string) string { + return "//line " + filepath.ToSlash(filename) + ":1\n" +} + +func assertExactString(t *testing.T, label, got, want string) { + t.Helper() + if got != want { + t.Fatalf("%s mismatch\nwant:\n%q\n\ngot:\n%q", label, want, got) + } +} + +func assertPatchedPosition(t *testing.T, src, generatedFilename, wantFilename, target string, wantLine int) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, generatedFilename, src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + pos, ok := findPatchedPosition(file, target) + if !ok { + t.Fatalf("target %q not found", target) + } + got := fset.Position(pos) + if filepath.ToSlash(got.Filename) != filepath.ToSlash(wantFilename) || got.Line != wantLine { + t.Fatalf("target %q position mismatch: want %s:%d, got %s:%d", target, filepath.ToSlash(wantFilename), wantLine, filepath.ToSlash(got.Filename), got.Line) + } +} + +func findPatchedPosition(file *ast.File, target string) (token.Pos, bool) { + if target == "package" { + return file.Package, true + } + for _, decl := range file.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + key := "func:" + decl.Name.Name + if decl.Recv != nil && len(decl.Recv.List) != 0 { + key = "method:" + recvPatchKey(decl.Recv.List[0].Type) + "." + decl.Name.Name + } + if key == target { + return decl.Name.Pos(), true + } + case *ast.GenDecl: + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + if "type:"+spec.Name.Name == target { + return spec.Name.Pos(), true + } + case *ast.ValueSpec: + kind := strings.ToLower(decl.Tok.String()) + for _, name := range spec.Names { + if kind+":"+name.Name == target { + return name.Pos(), true + } + } + } + } + } + } + return token.NoPos, false +} diff --git a/internal/env/env.go b/internal/env/env.go index 4da842f7b4..8af6da65e1 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -18,7 +18,30 @@ const ( ) func GOROOT() (string, error) { + return GOROOTWithEnv(nil) +} + +func GOROOTWithEnv(env []string) (string, error) { cmd := exec.Command("go", "env", "GOROOT") + if len(env) != 0 { + cmd.Env = env + } + var out bytes.Buffer + var buf bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &buf + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("%s, %w", buf.String(), err) + } + return strings.TrimSpace(out.String()), nil +} + +func GOVERSIONWithEnv(env []string) (string, error) { + cmd := exec.Command("go", "env", "GOVERSION") + if len(env) != 0 { + cmd.Env = env + } var out bytes.Buffer var buf bytes.Buffer cmd.Stdout = &out diff --git a/runtime/_patch/_test/default-override/output/demo.go.txt b/runtime/_patch/_test/default-override/output/demo.go.txt new file mode 100644 index 0000000000..c1892cc226 --- /dev/null +++ b/runtime/_patch/_test/default-override/output/demo.go.txt @@ -0,0 +1,4 @@ +package demo + +// func Old() string { return "old" } +func Keep() string { return "keep" } diff --git a/runtime/_patch/_test/default-override/output/z_llgo_patch_patch.go.txt b/runtime/_patch/_test/default-override/output/z_llgo_patch_patch.go.txt new file mode 100644 index 0000000000..3376cfb845 --- /dev/null +++ b/runtime/_patch/_test/default-override/output/z_llgo_patch_patch.go.txt @@ -0,0 +1,5 @@ +//line {{PATCH_ROOT}}/demo/patch.go:1 +package demo + +func Old() string { return "new" } +func Added() string { return "added" } diff --git a/runtime/_patch/_test/default-override/patch/patch.go b/runtime/_patch/_test/default-override/patch/patch.go new file mode 100644 index 0000000000..fc5951b856 --- /dev/null +++ b/runtime/_patch/_test/default-override/patch/patch.go @@ -0,0 +1,4 @@ +package demo + +func Old() string { return "new" } +func Added() string { return "added" } diff --git a/runtime/_patch/_test/default-override/pkg/demo.go b/runtime/_patch/_test/default-override/pkg/demo.go new file mode 100644 index 0000000000..6a9652a579 --- /dev/null +++ b/runtime/_patch/_test/default-override/pkg/demo.go @@ -0,0 +1,4 @@ +package demo + +func Old() string { return "old" } +func Keep() string { return "keep" } diff --git a/runtime/_patch/_test/generic-constraints-and-interface/output/demo.go.txt b/runtime/_patch/_test/generic-constraints-and-interface/output/demo.go.txt new file mode 100644 index 0000000000..18996bd934 --- /dev/null +++ b/runtime/_patch/_test/generic-constraints-and-interface/output/demo.go.txt @@ -0,0 +1,11 @@ +package demo + +// type Number interface { + // ~int | ~int64 +// } + +// type Set[T comparable] interface { + // Has(T) bool +// } + +// func Sum[T Number](a, b T) T { return a + b } diff --git a/runtime/_patch/_test/generic-constraints-and-interface/output/z_llgo_patch_patch.go.txt b/runtime/_patch/_test/generic-constraints-and-interface/output/z_llgo_patch_patch.go.txt new file mode 100644 index 0000000000..37c500a684 --- /dev/null +++ b/runtime/_patch/_test/generic-constraints-and-interface/output/z_llgo_patch_patch.go.txt @@ -0,0 +1,13 @@ +//line {{PATCH_ROOT}}/demo/patch.go:1 +package demo + +type Number interface { + ~int | ~int32 | ~int64 +} + +type Set[T comparable] interface { + Has(T) bool + Len() int +} + +func Sum[T Number](a, b T) T { return a } diff --git a/runtime/_patch/_test/generic-constraints-and-interface/patch/patch.go b/runtime/_patch/_test/generic-constraints-and-interface/patch/patch.go new file mode 100644 index 0000000000..8ce6fd6691 --- /dev/null +++ b/runtime/_patch/_test/generic-constraints-and-interface/patch/patch.go @@ -0,0 +1,12 @@ +package demo + +type Number interface { + ~int | ~int32 | ~int64 +} + +type Set[T comparable] interface { + Has(T) bool + Len() int +} + +func Sum[T Number](a, b T) T { return a } diff --git a/runtime/_patch/_test/generic-constraints-and-interface/pkg/demo.go b/runtime/_patch/_test/generic-constraints-and-interface/pkg/demo.go new file mode 100644 index 0000000000..aa117ba443 --- /dev/null +++ b/runtime/_patch/_test/generic-constraints-and-interface/pkg/demo.go @@ -0,0 +1,11 @@ +package demo + +type Number interface { + ~int | ~int64 +} + +type Set[T comparable] interface { + Has(T) bool +} + +func Sum[T Number](a, b T) T { return a + b } diff --git a/runtime/_patch/_test/generic-type-and-method/output/demo.go.txt b/runtime/_patch/_test/generic-type-and-method/output/demo.go.txt new file mode 100644 index 0000000000..76ca83b2be --- /dev/null +++ b/runtime/_patch/_test/generic-type-and-method/output/demo.go.txt @@ -0,0 +1,7 @@ +package demo + +// type Box[T any] struct{ V T } + +// func (b Box[T]) Old() T { return b.V } + +func Keep[T any](v T) T { return v } diff --git a/runtime/_patch/_test/generic-type-and-method/output/z_llgo_patch_patch.go.txt b/runtime/_patch/_test/generic-type-and-method/output/z_llgo_patch_patch.go.txt new file mode 100644 index 0000000000..abced97d39 --- /dev/null +++ b/runtime/_patch/_test/generic-type-and-method/output/z_llgo_patch_patch.go.txt @@ -0,0 +1,8 @@ +//line {{PATCH_ROOT}}/demo/patch.go:1 +package demo + +type Box[T any] struct{ Value T } + +func (b Box[T]) Old() T { return b.Value } + +func Added[T any](v T) T { return v } diff --git a/runtime/_patch/_test/generic-type-and-method/patch/patch.go b/runtime/_patch/_test/generic-type-and-method/patch/patch.go new file mode 100644 index 0000000000..2b3234965c --- /dev/null +++ b/runtime/_patch/_test/generic-type-and-method/patch/patch.go @@ -0,0 +1,7 @@ +package demo + +type Box[T any] struct{ Value T } + +func (b Box[T]) Old() T { return b.Value } + +func Added[T any](v T) T { return v } diff --git a/runtime/_patch/_test/generic-type-and-method/pkg/demo.go b/runtime/_patch/_test/generic-type-and-method/pkg/demo.go new file mode 100644 index 0000000000..f2191f8991 --- /dev/null +++ b/runtime/_patch/_test/generic-type-and-method/pkg/demo.go @@ -0,0 +1,7 @@ +package demo + +type Box[T any] struct{ V T } + +func (b Box[T]) Old() T { return b.V } + +func Keep[T any](v T) T { return v } diff --git a/runtime/_patch/_test/multi-file-skipall/output/a.go.txt b/runtime/_patch/_test/multi-file-skipall/output/a.go.txt new file mode 100644 index 0000000000..bed5a16777 --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/output/a.go.txt @@ -0,0 +1 @@ +package demo diff --git a/runtime/_patch/_test/multi-file-skipall/output/b.go.txt b/runtime/_patch/_test/multi-file-skipall/output/b.go.txt new file mode 100644 index 0000000000..bed5a16777 --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/output/b.go.txt @@ -0,0 +1 @@ +package demo diff --git a/runtime/_patch/_test/multi-file-skipall/output/z_llgo_patch_a.go.txt b/runtime/_patch/_test/multi-file-skipall/output/z_llgo_patch_a.go.txt new file mode 100644 index 0000000000..35ad108d19 --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/output/z_llgo_patch_a.go.txt @@ -0,0 +1,5 @@ +//line {{PATCH_ROOT}}/demo/a.go:1 +//llgo_skipall +package demo + +const Only = "patched" diff --git a/runtime/_patch/_test/multi-file-skipall/output/z_llgo_patch_b.go.txt b/runtime/_patch/_test/multi-file-skipall/output/z_llgo_patch_b.go.txt new file mode 100644 index 0000000000..484081b95d --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/output/z_llgo_patch_b.go.txt @@ -0,0 +1,4 @@ +//line {{PATCH_ROOT}}/demo/b.go:1 +package demo + +func Added() string { return "added" } diff --git a/runtime/_patch/_test/multi-file-skipall/patch/a.go b/runtime/_patch/_test/multi-file-skipall/patch/a.go new file mode 100644 index 0000000000..95592af1cf --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/patch/a.go @@ -0,0 +1,4 @@ +//llgo:skipall +package demo + +const Only = "patched" diff --git a/runtime/_patch/_test/multi-file-skipall/patch/b.go b/runtime/_patch/_test/multi-file-skipall/patch/b.go new file mode 100644 index 0000000000..b93ce6201c --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/patch/b.go @@ -0,0 +1,3 @@ +package demo + +func Added() string { return "added" } diff --git a/runtime/_patch/_test/multi-file-skipall/pkg/a.go b/runtime/_patch/_test/multi-file-skipall/pkg/a.go new file mode 100644 index 0000000000..9d0980f3eb --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/pkg/a.go @@ -0,0 +1,3 @@ +package demo + +func OldA() string { return "old-a" } diff --git a/runtime/_patch/_test/multi-file-skipall/pkg/b.go b/runtime/_patch/_test/multi-file-skipall/pkg/b.go new file mode 100644 index 0000000000..cdca36d583 --- /dev/null +++ b/runtime/_patch/_test/multi-file-skipall/pkg/b.go @@ -0,0 +1,3 @@ +package demo + +func OldB() string { return "old-b" } diff --git a/runtime/_patch/_test/multi-file-with-asm/output/a.go.txt b/runtime/_patch/_test/multi-file-with-asm/output/a.go.txt new file mode 100644 index 0000000000..4b0b133d73 --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/output/a.go.txt @@ -0,0 +1,9 @@ +package demo + +// import "fmt" + +// var DropA = fmt.Sprint("drop-a") + +func KeepA() string { return "keep-a" } + +// func OldA() string { return fmt.Sprint("old-a") } diff --git a/runtime/_patch/_test/multi-file-with-asm/output/b.go.txt b/runtime/_patch/_test/multi-file-with-asm/output/b.go.txt new file mode 100644 index 0000000000..7f19b093c6 --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/output/b.go.txt @@ -0,0 +1,4 @@ +package demo + +func KeepB() string { return "keep-b" } +// func OldB() string { return "old-b" } diff --git a/runtime/_patch/_test/multi-file-with-asm/output/z_llgo_patch_a.go.txt b/runtime/_patch/_test/multi-file-with-asm/output/z_llgo_patch_a.go.txt new file mode 100644 index 0000000000..1b00d7ab05 --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/output/z_llgo_patch_a.go.txt @@ -0,0 +1,10 @@ +//line {{PATCH_ROOT}}/demo/a.go:1 +package demo + +//llgo_skip DropA + +import "strings" + +var AddedA = strings.ToUpper("added-a") + +func OldA() string { return "new-a" } diff --git a/runtime/_patch/_test/multi-file-with-asm/output/z_llgo_patch_b.go.txt b/runtime/_patch/_test/multi-file-with-asm/output/z_llgo_patch_b.go.txt new file mode 100644 index 0000000000..b4a55d47e5 --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/output/z_llgo_patch_b.go.txt @@ -0,0 +1,5 @@ +//line {{PATCH_ROOT}}/demo/b.go:1 +package demo + +func OldB() string { return "new-b" } +func AddedB() string { return "added-b" } diff --git a/runtime/_patch/_test/multi-file-with-asm/patch/a.go b/runtime/_patch/_test/multi-file-with-asm/patch/a.go new file mode 100644 index 0000000000..f862e66bdc --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/patch/a.go @@ -0,0 +1,9 @@ +package demo + +//llgo:skip DropA + +import "strings" + +var AddedA = strings.ToUpper("added-a") + +func OldA() string { return "new-a" } diff --git a/runtime/_patch/_test/multi-file-with-asm/patch/b.go b/runtime/_patch/_test/multi-file-with-asm/patch/b.go new file mode 100644 index 0000000000..e855ede358 --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/patch/b.go @@ -0,0 +1,4 @@ +package demo + +func OldB() string { return "new-b" } +func AddedB() string { return "added-b" } diff --git a/runtime/_patch/_test/multi-file-with-asm/pkg/a.go b/runtime/_patch/_test/multi-file-with-asm/pkg/a.go new file mode 100644 index 0000000000..53836221f8 --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/pkg/a.go @@ -0,0 +1,9 @@ +package demo + +import "fmt" + +var DropA = fmt.Sprint("drop-a") + +func KeepA() string { return "keep-a" } + +func OldA() string { return fmt.Sprint("old-a") } diff --git a/runtime/_patch/_test/multi-file-with-asm/pkg/asm.s b/runtime/_patch/_test/multi-file-with-asm/pkg/asm.s new file mode 100644 index 0000000000..36edcf1aa8 --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/pkg/asm.s @@ -0,0 +1,2 @@ +TEXT ·Asm(SB),$0-0 + RET diff --git a/runtime/_patch/_test/multi-file-with-asm/pkg/b.go b/runtime/_patch/_test/multi-file-with-asm/pkg/b.go new file mode 100644 index 0000000000..929842775d --- /dev/null +++ b/runtime/_patch/_test/multi-file-with-asm/pkg/b.go @@ -0,0 +1,4 @@ +package demo + +func KeepB() string { return "keep-b" } +func OldB() string { return "old-b" } diff --git a/runtime/_patch/_test/skip-and-override/output/demo.go.txt b/runtime/_patch/_test/skip-and-override/output/demo.go.txt new file mode 100644 index 0000000000..dfdcf7df52 --- /dev/null +++ b/runtime/_patch/_test/skip-and-override/output/demo.go.txt @@ -0,0 +1,13 @@ +package demo + +// import "fmt" + +const Keep = "keep" + +// var Drop = fmt.Sprint("drop") + +type T struct{} + +// func Old() string { return fmt.Sprint("old") } +func KeepFn() string { return Keep } +// func (T) M() string { return fmt.Sprint("old method") } diff --git a/runtime/_patch/_test/skip-and-override/output/z_llgo_patch_patch.go.txt b/runtime/_patch/_test/skip-and-override/output/z_llgo_patch_patch.go.txt new file mode 100644 index 0000000000..102cd5cdfa --- /dev/null +++ b/runtime/_patch/_test/skip-and-override/output/z_llgo_patch_patch.go.txt @@ -0,0 +1,11 @@ +//line {{PATCH_ROOT}}/demo/patch.go:1 +package demo + +//llgo_skip Drop + +import "strings" + +var Added = strings.ToUpper("added") + +func Old() string { return "new" } +func (T) M() string { return "new method" } diff --git a/runtime/_patch/_test/skip-and-override/patch/patch.go b/runtime/_patch/_test/skip-and-override/patch/patch.go new file mode 100644 index 0000000000..2a4175bc2a --- /dev/null +++ b/runtime/_patch/_test/skip-and-override/patch/patch.go @@ -0,0 +1,10 @@ +package demo + +//llgo:skip Drop + +import "strings" + +var Added = strings.ToUpper("added") + +func Old() string { return "new" } +func (T) M() string { return "new method" } diff --git a/runtime/_patch/_test/skip-and-override/pkg/demo.go b/runtime/_patch/_test/skip-and-override/pkg/demo.go new file mode 100644 index 0000000000..ca7c281777 --- /dev/null +++ b/runtime/_patch/_test/skip-and-override/pkg/demo.go @@ -0,0 +1,13 @@ +package demo + +import "fmt" + +const Keep = "keep" + +var Drop = fmt.Sprint("drop") + +type T struct{} + +func Old() string { return fmt.Sprint("old") } +func KeepFn() string { return Keep } +func (T) M() string { return fmt.Sprint("old method") } diff --git a/runtime/_patch/_test/skipall/output/demo.go.txt b/runtime/_patch/_test/skipall/output/demo.go.txt new file mode 100644 index 0000000000..bed5a16777 --- /dev/null +++ b/runtime/_patch/_test/skipall/output/demo.go.txt @@ -0,0 +1 @@ +package demo diff --git a/runtime/_patch/_test/skipall/output/z_llgo_patch_patch.go.txt b/runtime/_patch/_test/skipall/output/z_llgo_patch_patch.go.txt new file mode 100644 index 0000000000..5e67d8d20f --- /dev/null +++ b/runtime/_patch/_test/skipall/output/z_llgo_patch_patch.go.txt @@ -0,0 +1,5 @@ +//line {{PATCH_ROOT}}/demo/patch.go:1 +// llgo_skipall +package demo + +const Only = "patched" diff --git a/runtime/_patch/_test/skipall/patch/patch.go b/runtime/_patch/_test/skipall/patch/patch.go new file mode 100644 index 0000000000..b8a4a96457 --- /dev/null +++ b/runtime/_patch/_test/skipall/patch/patch.go @@ -0,0 +1,4 @@ +// llgo:skipall +package demo + +const Only = "patched" diff --git a/runtime/_patch/_test/skipall/pkg/demo.go b/runtime/_patch/_test/skipall/pkg/demo.go new file mode 100644 index 0000000000..1632daea6c --- /dev/null +++ b/runtime/_patch/_test/skipall/pkg/demo.go @@ -0,0 +1,3 @@ +package demo + +func Old() string { return "old" } diff --git a/runtime/_patch/_test/type-alias-and-grouped-values/output/demo.go.txt b/runtime/_patch/_test/type-alias-and-grouped-values/output/demo.go.txt new file mode 100644 index 0000000000..4a65916cdb --- /dev/null +++ b/runtime/_patch/_test/type-alias-and-grouped-values/output/demo.go.txt @@ -0,0 +1,13 @@ +package demo + +// type Text = string + +const ( + // ReplaceConst = "old" + KeepConst = "keep" +) + +var ( + // ReplaceVar = "old" + KeepVar = "keep" +) diff --git a/runtime/_patch/_test/type-alias-and-grouped-values/output/z_llgo_patch_patch.go.txt b/runtime/_patch/_test/type-alias-and-grouped-values/output/z_llgo_patch_patch.go.txt new file mode 100644 index 0000000000..43b94f97f8 --- /dev/null +++ b/runtime/_patch/_test/type-alias-and-grouped-values/output/z_llgo_patch_patch.go.txt @@ -0,0 +1,14 @@ +//line {{PATCH_ROOT}}/demo/patch.go:1 +package demo + +type Text = []byte + +const ( + ReplaceConst = "new" + AddedConst = "added" +) + +var ( + ReplaceVar = "new" + AddedVar = "added" +) diff --git a/runtime/_patch/_test/type-alias-and-grouped-values/patch/patch.go b/runtime/_patch/_test/type-alias-and-grouped-values/patch/patch.go new file mode 100644 index 0000000000..f39a26517f --- /dev/null +++ b/runtime/_patch/_test/type-alias-and-grouped-values/patch/patch.go @@ -0,0 +1,13 @@ +package demo + +type Text = []byte + +const ( + ReplaceConst = "new" + AddedConst = "added" +) + +var ( + ReplaceVar = "new" + AddedVar = "added" +) diff --git a/runtime/_patch/_test/type-alias-and-grouped-values/pkg/demo.go b/runtime/_patch/_test/type-alias-and-grouped-values/pkg/demo.go new file mode 100644 index 0000000000..8ccda75fe3 --- /dev/null +++ b/runtime/_patch/_test/type-alias-and-grouped-values/pkg/demo.go @@ -0,0 +1,13 @@ +package demo + +type Text = string + +const ( + ReplaceConst = "old" + KeepConst = "keep" +) + +var ( + ReplaceVar = "old" + KeepVar = "keep" +) diff --git a/runtime/internal/lib/iter/iter.go b/runtime/_patch/iter/iter.go similarity index 99% rename from runtime/internal/lib/iter/iter.go rename to runtime/_patch/iter/iter.go index 58b556327e..f36443a993 100644 --- a/runtime/internal/lib/iter/iter.go +++ b/runtime/_patch/iter/iter.go @@ -1,3 +1,4 @@ +// llgo:skipall package iter // Seq is an iterator over sequences of individual values. diff --git a/runtime/build.go b/runtime/build.go index c2c6709c97..3838de6ca8 100644 --- a/runtime/build.go +++ b/runtime/build.go @@ -1,5 +1,7 @@ package runtime +import "sort" + type altPkgMode uint8 const ( @@ -23,15 +25,32 @@ func HasAdditiveAltPkg(path string) bool { return altPkgs[path] == altPkgAdditive } +func HasSourcePatchPkg(path string) bool { + _, ok := sourcePatchPkgs[path] + return ok +} + +func SourcePatchPkgPaths() []string { + paths := make([]string, 0, len(sourcePatchPkgs)) + for path := range sourcePatchPkgs { + paths = append(paths, path) + } + sort.Strings(paths) + return paths +} + var altPkgs = map[string]altPkgMode{ "internal/abi": altPkgReplace, "internal/reflectlite": altPkgReplace, "internal/runtime/maps": altPkgReplace, "internal/runtime/sys": altPkgAdditive, - "iter": altPkgReplace, "reflect": altPkgReplace, "runtime": altPkgReplace, - "unique": altPkgReplace, - "syscall/js": altPkgReplace, "sync/atomic": altPkgReplace, + "syscall/js": altPkgReplace, + "unique": altPkgReplace, +} + +var sourcePatchPkgs = map[string]struct{}{ + "iter": {}, }