diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 90cea762ca..731147b722 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -47,7 +47,7 @@ jobs: - macos-15-intel - ubuntu-latest llvm: [19] - go: ["1.21.13", "1.24.2"] + go: ["1.21.13", "1.24.2", "1.26.0"] runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v6 @@ -97,9 +97,15 @@ jobs: pip3.12 install --break-system-packages "${py_deps[@]}" - name: Set up Go for build + if: ${{ !startsWith(matrix.go, '1.26') }} uses: ./.github/actions/setup-go with: go-version: "1.24.2" + - name: Set up Go 1.26 for build + if: ${{ startsWith(matrix.go, '1.26') }} + uses: ./.github/actions/setup-go + with: + go-version: "1.26.0" - name: Install run: | @@ -192,7 +198,7 @@ jobs: - macos-latest - ubuntu-latest llvm: [19] - go: ["1.24.2"] + go: ["1.24.2", "1.26.0"] shard: ["0", "1", "2", "3"] runs-on: ${{matrix.os}} steps: @@ -210,9 +216,15 @@ jobs: pip3.12 install --break-system-packages "${py_deps[@]}" - name: Set up Go for build + if: ${{ !startsWith(matrix.go, '1.26') }} uses: ./.github/actions/setup-go with: go-version: "1.24.2" + - name: Set up Go 1.26 for build + if: ${{ startsWith(matrix.go, '1.26') }} + uses: ./.github/actions/setup-go + with: + go-version: "1.26.0" - name: Install run: | @@ -268,7 +280,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] llvm: [19] - go: ["1.21.13", "1.24.2"] + go: ["1.21.13", "1.24.2", "1.26.0"] runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v6 @@ -277,10 +289,16 @@ jobs: with: llvm-version: ${{matrix.llvm}} - - name: Set up Go 1.23 for building llgo + - name: Set up Go for build + if: ${{ !startsWith(matrix.go, '1.26') }} uses: ./.github/actions/setup-go with: go-version: "1.24.2" + - name: Set up Go 1.26 for build + if: ${{ startsWith(matrix.go, '1.26') }} + uses: ./.github/actions/setup-go + with: + go-version: "1.26.0" - name: Install llgo run: | @@ -293,28 +311,28 @@ jobs: go-version: ${{matrix.go}} - name: Test Hello World with go.mod 1.21 - if: startsWith(matrix.go, '1.21') || startsWith(matrix.go, '1.22') || startsWith(matrix.go, '1.23') || startsWith(matrix.go, '1.24') + if: startsWith(matrix.go, '1.21') || startsWith(matrix.go, '1.22') || startsWith(matrix.go, '1.23') || startsWith(matrix.go, '1.24') || startsWith(matrix.go, '1.25') || startsWith(matrix.go, '1.26') uses: ./.github/actions/test-helloworld with: go-version: ${{matrix.go}} mod-version: "1.21" - name: Test Hello World with go.mod 1.22 - if: startsWith(matrix.go, '1.22') || startsWith(matrix.go, '1.23') || startsWith(matrix.go, '1.24') + if: startsWith(matrix.go, '1.22') || startsWith(matrix.go, '1.23') || startsWith(matrix.go, '1.24') || startsWith(matrix.go, '1.25') || startsWith(matrix.go, '1.26') uses: ./.github/actions/test-helloworld with: go-version: ${{matrix.go}} mod-version: "1.22" - name: Test Hello World with go.mod 1.23 - if: startsWith(matrix.go, '1.23') || startsWith(matrix.go, '1.24') + if: startsWith(matrix.go, '1.23') || startsWith(matrix.go, '1.24') || startsWith(matrix.go, '1.25') || startsWith(matrix.go, '1.26') uses: ./.github/actions/test-helloworld with: go-version: ${{matrix.go}} mod-version: "1.23" - name: Test Hello World with go.mod 1.24 - if: startsWith(matrix.go, '1.24') + if: startsWith(matrix.go, '1.24') || startsWith(matrix.go, '1.25') || startsWith(matrix.go, '1.26') uses: ./.github/actions/test-helloworld with: go-version: ${{matrix.go}} diff --git a/cl/compile_test.go b/cl/compile_test.go index 657fe3b923..454f2a8bd1 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -384,3 +384,40 @@ _llgo_2: ; preds = %_llgo_1, %_llgo_0 } `) } + +func TestIntrinsicBoolToUint8(t *testing.T) { + testCompile(t, `package foo + +import _ "unsafe" + +//go:linkname boolToUint8 llgo.boolToUint8 +func boolToUint8(b bool) uint8 + +func use(b bool) uint8 { + return boolToUint8(b) +} +`, `; ModuleID = 'foo' +source_filename = "foo" + +@"foo.init$guard" = global i1 false, align 1 + +define void @foo.init() { +_llgo_0: + %0 = load i1, ptr @"foo.init$guard", align 1 + br i1 %0, label %_llgo_2, label %_llgo_1 + +_llgo_1: ; preds = %_llgo_0 + store i1 true, ptr @"foo.init$guard", align 1 + br label %_llgo_2 + +_llgo_2: ; preds = %_llgo_1, %_llgo_0 + ret void +} + +define i8 @foo.use(i1 %0) { +_llgo_0: + %1 = select i1 %0, i8 1, i8 0 + ret i8 %1 +} +`) +} diff --git a/cl/import.go b/cl/import.go index 2af85a1a73..fa15f5605a 100644 --- a/cl/import.go +++ b/cl/import.go @@ -552,6 +552,7 @@ const ( llgoSyscall = llgoInstrBase + 0x44 llgoAtomicCmpXchgOK = llgoInstrBase + 0x45 llgoAtomicAddReturnNew = llgoInstrBase + 0x46 + llgoBoolToUint8 = llgoInstrBase + 0x47 llgoAtomicOpLast = llgoAtomicOpBase + int(llssa.OpUMin) ) diff --git a/cl/instr.go b/cl/instr.go index c1d9933a50..f699831622 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -551,6 +551,16 @@ func (p *context) atomicCmpXchgOK(b llssa.Builder, args []llssa.Expr) llssa.Expr return b.Extract(ret, 1) } +func (p *context) boolToUint8(b llssa.Builder, args []llssa.Expr) llssa.Expr { + if len(args) == 1 { + retType := p.type_(types.Typ[types.Uint8], llssa.InGo) + one := p.prog.IntVal(1, retType) + zero := p.prog.Zero(retType) + return b.SelectValue(args[0], one, zero) + } + panic("boolToUint8(b bool) uint8: invalid arguments") +} + // ----------------------------------------------------------------------------- var llgoInstrs = map[string]int{ @@ -567,6 +577,7 @@ var llgoInstrs = map[string]int{ "funcPCABI0": llgoFuncPCABI0, "skip": llgoSkip, "syscall": llgoSyscall, + "boolToUint8": llgoBoolToUint8, "pystr": llgoPyStr, "pyList": llgoPyList, "pyTuple": llgoPyTuple, @@ -790,6 +801,11 @@ func (p *context) call(b llssa.Builder, act llssa.DoAction, call *ssa.CallCommon } case llgoSyscall: ret = p.syscallIntrinsic(b, args, call.Signature().Results()) + case llgoBoolToUint8: + args := p.compileValues(b, args, kind) + ret = b.Do(act, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + return p.boolToUint8(b, args) + }, args...) case llgoUnreachable: // func unreachable() b.Unreachable() case llgoAtomicLoad: diff --git a/go.mod b/go.mod index e5162166fb..ae896f8285 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( ) require ( - github.com/goplus/plan9asm v0.0.0-20260307134905-822503d6bf44 + github.com/goplus/plan9asm v0.1.1-0.20260327232615-ec1a30b9181f github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84 github.com/mattn/go-tty v0.0.7 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 diff --git a/go.sum b/go.sum index 25b6036285..9c260e8abc 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/goplus/plan9asm v0.0.0-20260307044640-a5f1e3b27fc1 h1:hlVAtc3x34x6CQo github.com/goplus/plan9asm v0.0.0-20260307044640-a5f1e3b27fc1/go.mod h1:nnr49+IlbnOI5c0yb1fkbilBRcr50RPX0JAreDdYTUI= github.com/goplus/plan9asm v0.0.0-20260307134905-822503d6bf44 h1:zZXTz5CBO1uZIJGZSGWov5gE1CbulJ1QH6ruTMv3UdY= github.com/goplus/plan9asm v0.0.0-20260307134905-822503d6bf44/go.mod h1:gpS4VVNoRykYTtc8kPFBowraN5SrBj8lIjW8/lNjaG4= +github.com/goplus/plan9asm v0.1.1-0.20260327232615-ec1a30b9181f h1:DICfzr9FWlZPnKJcS0tMnIRQ1oWk1sFU4S1uYJ0jOTo= +github.com/goplus/plan9asm v0.1.1-0.20260327232615-ec1a30b9181f/go.mod h1:gpS4VVNoRykYTtc8kPFBowraN5SrBj8lIjW8/lNjaG4= github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84 h1:hyAgCuG5nqTMDeUD8KZs7HSPs6KprPgPP8QmGV8nyvk= github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84/go.mod h1:Pb6XcsXyropB9LNHhnqaknG/vEwYztLkQzVCHv8sQ3M= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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_go126_test.go b/internal/build/source_patch_go126_test.go new file mode 100644 index 0000000000..e43e67c73c --- /dev/null +++ b/internal/build/source_patch_go126_test.go @@ -0,0 +1,93 @@ +//go:build go1.26 +// +build go1.26 + +package build + +import ( + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/goplus/llgo/internal/env" + llruntime "github.com/goplus/llgo/runtime" +) + +func TestBuildSourcePatchOverlayForInternalSync(t *testing.T) { + overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), runtime.GOROOT(), sourcePatchBuildContext{ + goos: runtime.GOOS, + goarch: runtime.GOARCH, + goversion: "go1.26.0", + }) + if err != nil { + t.Fatal(err) + } + + syncDir := filepath.Join(runtime.GOROOT(), "src", "internal", "sync") + patchFile := filepath.Join(syncDir, "z_llgo_patch_hashtriemap.go") + patchSrc, ok := overlay[patchFile] + if !ok { + t.Fatalf("missing source patch file %s", patchFile) + } + if !strings.Contains(string(patchSrc), "type HashTrieMap") { + t.Fatalf("source patch file %s does not contain HashTrieMap replacement", patchFile) + } + if !strings.HasPrefix(string(patchSrc), sourcePatchLineDirective(filepath.Join(env.LLGoRuntimeDir(), "_patch", "internal", "sync", "hashtriemap.go"))) { + t.Fatalf("source patch file %s is missing line directive, got:\n%s", patchFile, patchSrc) + } + + stdFile := filepath.Join(syncDir, "hashtriemap.go") + stdSrc, ok := overlay[stdFile] + if !ok { + t.Fatalf("missing stub overlay for %s", stdFile) + } + got := string(stdSrc) + if !strings.Contains(got, "package sync") { + t.Fatalf("stub overlay for %s lost package clause", stdFile) + } + if strings.Contains(got, "type HashTrieMap") { + t.Fatalf("stub overlay for %s still contains original declarations", stdFile) + } +} + +func TestInternalSyncUsesSourcePatchInsteadOfAltPkg(t *testing.T) { + if !llruntime.HasSourcePatchPkg("internal/sync") { + t.Fatal("internal/sync should be registered as a source patch package") + } + if llruntime.HasAltPkg("internal/sync") { + t.Fatal("internal/sync should not remain an alt package") + } +} + +func TestBuildSourcePatchOverlayForCryptoInternalConstanttime(t *testing.T) { + overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), runtime.GOROOT(), sourcePatchBuildContext{ + goos: runtime.GOOS, + goarch: runtime.GOARCH, + goversion: "go1.26.0", + }) + if err != nil { + t.Fatal(err) + } + + pkgDir := filepath.Join(runtime.GOROOT(), "src", "crypto", "internal", "constanttime") + patchFile := filepath.Join(pkgDir, "z_llgo_patch_constant_time.go") + patchSrc, ok := overlay[patchFile] + if !ok { + t.Fatalf("missing source patch file %s", patchFile) + } + if !strings.Contains(string(patchSrc), "//go:linkname boolToUint8 llgo.boolToUint8") { + t.Fatalf("source patch file %s does not contain boolToUint8 linkname", patchFile) + } + if !strings.HasPrefix(string(patchSrc), sourcePatchLineDirective(filepath.Join(env.LLGoRuntimeDir(), "_patch", "crypto", "internal", "constanttime", "constant_time.go"))) { + t.Fatalf("source patch file %s is missing line directive, got:\n%s", patchFile, patchSrc) + } +} + +func TestCryptoInternalConstanttimeUsesSourcePatchInsteadOfAltPkg(t *testing.T) { + if !llruntime.HasSourcePatchPkg("crypto/internal/constanttime") { + t.Fatal("crypto/internal/constanttime should be registered as a source patch package") + } + if llruntime.HasAltPkg("crypto/internal/constanttime") { + t.Fatal("crypto/internal/constanttime should not remain an alt package") + } +} 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/internal/plan9asm/translate.go b/internal/plan9asm/translate.go index dcc674c99e..f988f09016 100644 --- a/internal/plan9asm/translate.go +++ b/internal/plan9asm/translate.go @@ -209,5 +209,21 @@ func extraAsmSigsAndDeclMap(pkgPath string, goarch string) map[string]extplan9as manual["internal/bytealg.memeqbody"] = extplan9asm.FuncSig{Args: []extplan9asm.LLVMType{extplan9asm.Ptr, extplan9asm.Ptr, extplan9asm.I64}, Ret: extplan9asm.I1, ArgRegs: []extplan9asm.Reg{extplan9asm.SI, extplan9asm.DI, extplan9asm.BX}} } } + if pkgPath == "internal/runtime/gc/scan" && goarch == "amd64" { + // Go 1.26 emits local AVX512 scan expanders with no Go declarations. + // They still need signatures so the translator can keep the asm entry + // points and wire the packed mask argument through AX. + for _, n := range []int{ + 1, 2, 3, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 36, 40, 44, 48, 52, 56, 60, 64, + } { + name := fmt.Sprintf("internal/runtime/gc/scan.expandAVX512_%d", n) + manual[name] = extplan9asm.FuncSig{ + Args: []extplan9asm.LLVMType{extplan9asm.Ptr}, + Ret: extplan9asm.Void, + ArgRegs: []extplan9asm.Reg{extplan9asm.AX}, + } + } + } return manual } 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/_patch/crypto/internal/constanttime/constant_time.go b/runtime/_patch/crypto/internal/constanttime/constant_time.go new file mode 100644 index 0000000000..05b1911ced --- /dev/null +++ b/runtime/_patch/crypto/internal/constanttime/constant_time.go @@ -0,0 +1,12 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.26 + +package constanttime + +import _ "unsafe" + +//go:linkname boolToUint8 llgo.boolToUint8 +func boolToUint8(b bool) uint8 diff --git a/runtime/_patch/internal/sync/hashtriemap.go b/runtime/_patch/internal/sync/hashtriemap.go new file mode 100644 index 0000000000..7b3fda63d6 --- /dev/null +++ b/runtime/_patch/internal/sync/hashtriemap.go @@ -0,0 +1,201 @@ +//go:build go1.26 + +//llgo:skipall +package sync + +import ( + "sync/atomic" + "unsafe" +) + +type hashFunc func(unsafe.Pointer, uintptr) uintptr +type equalFunc func(unsafe.Pointer, unsafe.Pointer) bool + +const ( + nChildrenLog2 = 4 + nChildren = 1 << nChildrenLog2 +) + +type node[K comparable, V any] struct { + isEntry bool +} + +type indirect[K comparable, V any] struct { + node[K, V] + dead atomic.Bool + mu Mutex + parent *indirect[K, V] + children [nChildren]atomic.Pointer[node[K, V]] +} + +type entry[K comparable, V any] struct { + node[K, V] + overflow atomic.Pointer[entry[K, V]] + key K + value V +} + +// HashTrieMap is a compatibility implementation for llgo. +// It preserves the zero-value and concurrency semantics expected by sync.Map, +// but uses a simple mutex-protected Go map instead of the runtime hash trie. +type HashTrieMap[K comparable, V any] struct { + // Keep the leading field layout compatible with the upstream type so + // imported stdlib code that still selects these fields by index can build. + inited atomic.Uint32 + initMu Mutex + root atomic.Pointer[indirect[K, V]] + keyHash hashFunc + valEqual equalFunc + seed uintptr + + mu Mutex + m []hashTrieEntry[K, V] +} + +type hashTrieEntry[K comparable, V any] struct { + key K + value V +} + +func (ht *HashTrieMap[K, V]) ensureMap() { + if ht.m == nil { + ht.m = make([]hashTrieEntry[K, V], 0) + } +} + +func (ht *HashTrieMap[K, V]) Load(key K) (value V, ok bool) { + ht.mu.Lock() + if i := ht.findIndex(key); i >= 0 { + value, ok = ht.m[i].value, true + } + ht.mu.Unlock() + return +} + +func (ht *HashTrieMap[K, V]) LoadOrStore(key K, value V) (result V, loaded bool) { + ht.mu.Lock() + ht.ensureMap() + if i := ht.findIndex(key); i >= 0 { + existing := ht.m[i].value + ht.mu.Unlock() + return existing, true + } + ht.m = append(ht.m, hashTrieEntry[K, V]{key: key, value: value}) + ht.mu.Unlock() + return value, false +} + +func (ht *HashTrieMap[K, V]) Store(key K, value V) { + _, _ = ht.Swap(key, value) +} + +func (ht *HashTrieMap[K, V]) Swap(key K, new V) (previous V, loaded bool) { + ht.mu.Lock() + ht.ensureMap() + if i := ht.findIndex(key); i >= 0 { + previous = ht.m[i].value + ht.m[i].value = new + loaded = true + } else { + ht.m = append(ht.m, hashTrieEntry[K, V]{key: key, value: new}) + } + ht.mu.Unlock() + return +} + +func (ht *HashTrieMap[K, V]) CompareAndSwap(key K, old, new V) bool { + ht.mu.Lock() + defer ht.mu.Unlock() + if i := ht.findIndex(key); i < 0 { + return false + } else if !hashTrieValueEqual(ht.m[i].value, old) { + return false + } else { + ht.m[i].value = new + return true + } +} + +func (ht *HashTrieMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + ht.mu.Lock() + if i := ht.findIndex(key); i >= 0 { + value, loaded = ht.m[i].value, true + ht.deleteIndex(i) + } + ht.mu.Unlock() + return +} + +func (ht *HashTrieMap[K, V]) Delete(key K) { + _, _ = ht.LoadAndDelete(key) +} + +func (ht *HashTrieMap[K, V]) CompareAndDelete(key K, old V) bool { + ht.mu.Lock() + defer ht.mu.Unlock() + if i := ht.findIndex(key); i < 0 { + return false + } else if !hashTrieValueEqual(ht.m[i].value, old) { + return false + } else { + ht.deleteIndex(i) + return true + } +} + +func (ht *HashTrieMap[K, V]) All() func(yield func(K, V) bool) { + return func(yield func(K, V) bool) { + entries := ht.snapshot() + for _, entry := range entries { + if !yield(entry.key, entry.value) { + return + } + } + } +} + +func (ht *HashTrieMap[K, V]) Range(yield func(K, V) bool) { + ht.All()(yield) +} + +func (ht *HashTrieMap[K, V]) Clear() { + ht.mu.Lock() + ht.m = nil + ht.mu.Unlock() +} + +func (ht *HashTrieMap[K, V]) snapshot() []hashTrieEntry[K, V] { + ht.mu.Lock() + defer ht.mu.Unlock() + if len(ht.m) == 0 { + return nil + } + entries := make([]hashTrieEntry[K, V], len(ht.m)) + copy(entries, ht.m) + return entries +} + +func hashTrieValueEqual[V any](a, b V) bool { + return any(a) == any(b) +} + +func hashTrieKeyEqual[K comparable](a, b K) bool { + return any(a) == any(b) +} + +func (ht *HashTrieMap[K, V]) findIndex(key K) int { + for i := range ht.m { + if hashTrieKeyEqual(ht.m[i].key, key) { + return i + } + } + return -1 +} + +func (ht *HashTrieMap[K, V]) deleteIndex(i int) { + last := len(ht.m) - 1 + ht.m[i] = ht.m[last] + var zero hashTrieEntry[K, V] + ht.m[last] = zero + ht.m = ht.m[:last] +} diff --git a/runtime/_patch/internal/sync/mutex.go b/runtime/_patch/internal/sync/mutex.go new file mode 100644 index 0000000000..ff6f23104f --- /dev/null +++ b/runtime/_patch/internal/sync/mutex.go @@ -0,0 +1,151 @@ +//go:build go1.26 + +package sync + +import ( + "internal/race" + "sync/atomic" + "unsafe" +) + +// A Mutex is a mutual exclusion lock. +// +// See package [sync.Mutex] documentation. +type Mutex struct { + state int32 + sema uint32 +} + +const ( + mutexLocked = 1 << iota + mutexWoken + mutexStarving + mutexWaiterShift = iota + + starvationThresholdNs = 1e6 +) + +func (m *Mutex) Lock() { + if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { + if race.Enabled { + race.Acquire(unsafe.Pointer(m)) + } + return + } + m.lockSlow() +} + +func (m *Mutex) TryLock() bool { + old := m.state + if old&(mutexLocked|mutexStarving) != 0 { + return false + } + if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) { + return false + } + if race.Enabled { + race.Acquire(unsafe.Pointer(m)) + } + return true +} + +func (m *Mutex) lockSlow() { + var waitStartTime int64 + starving := false + awoke := false + iter := 0 + old := m.state + for { + if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { + if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && + atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { + awoke = true + } + runtime_doSpin() + iter++ + old = m.state + continue + } + new := old + if old&mutexStarving == 0 { + new |= mutexLocked + } + if old&(mutexLocked|mutexStarving) != 0 { + new += 1 << mutexWaiterShift + } + if starving && old&mutexLocked != 0 { + new |= mutexStarving + } + if awoke { + if new&mutexWoken == 0 { + throw("sync: inconsistent mutex state") + } + new &^= mutexWoken + } + if atomic.CompareAndSwapInt32(&m.state, old, new) { + if old&(mutexLocked|mutexStarving) == 0 { + break + } + queueLifo := waitStartTime != 0 + if waitStartTime == 0 { + waitStartTime = runtime_nanotime() + } + runtime_SemacquireMutex(&m.sema, queueLifo, 2) + starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs + old = m.state + if old&mutexStarving != 0 { + if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { + throw("sync: inconsistent mutex state") + } + delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 { + delta -= mutexStarving + } + atomic.AddInt32(&m.state, delta) + break + } + awoke = true + iter = 0 + } else { + old = m.state + } + } + + if race.Enabled { + race.Acquire(unsafe.Pointer(m)) + } +} + +func (m *Mutex) Unlock() { + if race.Enabled { + _ = m.state + race.Release(unsafe.Pointer(m)) + } + + new := atomic.AddInt32(&m.state, -mutexLocked) + if new != 0 { + m.unlockSlow(new) + } +} + +func (m *Mutex) unlockSlow(new int32) { + if (new+mutexLocked)&mutexLocked == 0 { + fatal("sync: unlock of unlocked mutex") + } + if new&mutexStarving == 0 { + old := new + for { + if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { + return + } + new = (old - 1<> (64 - bitSize) + return x != trunc + } + panic("reflect: OverflowInt of non-int type " + t.String()) +} + +func (t *rtype) OverflowUint(x uint64) bool { + k := t.Kind() + switch k { + case Uint, Uintptr, Uint8, Uint16, Uint32, Uint64: + bitSize := t.Size() * 8 + trunc := (x << (64 - bitSize)) >> (64 - bitSize) + return x != trunc + } + panic("reflect: OverflowUint of non-uint type " + t.String()) +} + +func (t *rtype) Methods() iter.Seq[Method] { + return func(yield func(Method) bool) { + for i := 0; i < t.NumMethod(); i++ { + if !yield(t.Method(i)) { + return + } + } + } +} + +func (t *rtype) Fields() iter.Seq[StructField] { + if t.Kind() != Struct { + panic("reflect: Fields of non-struct type " + t.String()) + } + return func(yield func(StructField) bool) { + for i := 0; i < t.NumField(); i++ { + if !yield(t.Field(i)) { + return + } + } + } +} + +func (t *rtype) Ins() iter.Seq[Type] { + if t.Kind() != Func { + panic("reflect: Ins of non-func type " + t.String()) + } + return func(yield func(Type) bool) { + for i := 0; i < t.NumIn(); i++ { + if !yield(t.In(i)) { + return + } + } + } +} + +func (t *rtype) Outs() iter.Seq[Type] { + if t.Kind() != Func { + panic("reflect: Outs of non-func type " + t.String()) + } + return func(yield func(Type) bool) { + for i := 0; i < t.NumOut(); i++ { + if !yield(t.Out(i)) { + return + } + } + } +} + +func (t *rtype) CanSeq() bool { + switch t.Kind() { + case Int8, Int16, Int32, Int64, Int, Uint8, Uint16, Uint32, Uint64, Uint, Uintptr, Array, Slice, Chan, String, Map: + return true + case Func: + return canRangeFunc(&t.t) + case Pointer: + return t.Elem().Kind() == Array + } + return false +} + +func canRangeFunc(t *abi.Type) bool { + if t.Kind() != abi.Func { + return false + } + f := t.FuncType() + if len(f.In) != 1 || len(f.Out) != 0 { + return false + } + y := f.In[0] + if y.Kind() != abi.Func { + return false + } + yield := y.FuncType() + return len(yield.In) == 1 && len(yield.Out) == 1 && yield.Out[0].Kind() == abi.Bool +} + +func (t *rtype) CanSeq2() bool { + switch t.Kind() { + case Array, Slice, String, Map: + return true + case Func: + return canRangeFunc2(&t.t) + case Pointer: + return t.Elem().Kind() == Array + } + return false +} + +func canRangeFunc2(t *abi.Type) bool { + if t.Kind() != abi.Func { + return false + } + f := t.FuncType() + if len(f.In) != 1 || len(f.Out) != 0 { + return false + } + y := f.In[0] + if y.Kind() != abi.Func { + return false + } + yield := y.FuncType() + return len(yield.In) == 2 && len(yield.Out) == 1 && yield.Out[0].Kind() == abi.Bool +} diff --git a/runtime/internal/lib/reflect/value.go b/runtime/internal/lib/reflect/value.go index 61b9d4489a..a093605239 100644 --- a/runtime/internal/lib/reflect/value.go +++ b/runtime/internal/lib/reflect/value.go @@ -140,36 +140,42 @@ func packEface(v Value) any { t := v.typ() var i any e := (*emptyInterface)(unsafe.Pointer(&i)) - // First, fill in the data portion of the interface. + e.word = packEfaceData(v) + // Now, fill in the type portion. We're very careful here not + // to have any operation between the e.word and e.typ assignments + // that would let the garbage collector observe the partially-built + // interface value. + e.typ = t + return i +} + +// packEfaceData is a helper that packs the data word as if v were stored in +// an empty interface. Go 1.26's reflect.TypeAssert refers to it directly. +func packEfaceData(v Value) unsafe.Pointer { + t := v.typ() switch { case t.IfaceIndir(): if v.flag&flagIndir == 0 { panic("bad indir") } - // Value is indirect, and so is the interface we're making. ptr := v.ptr if v.flag&flagAddr != 0 { - // TODO: pass safe boolean from valueInterface so - // we don't need to copy if safe==true? c := unsafe_New(t) typedmemmove(t, c, ptr) ptr = c } - e.word = ptr + return ptr case v.flag&flagIndir != 0: - // Value is indirect, but interface is direct. We need - // to load the data at v.ptr into the interface data word. - e.word = *(*unsafe.Pointer)(v.ptr) + return *(*unsafe.Pointer)(v.ptr) default: - // Value is direct, and so is the interface. - e.word = v.ptr + return v.ptr } - // Now, fill in the type portion. We're very careful here not - // to have any operation between the e.word and e.typ assignments - // that would let the garbage collector observe the partially-built - // interface value. - e.typ = t - return i +} + +// packIfaceValueIntoEmptyIface is used by Go 1.26's reflect.TypeAssert helpers +// when reboxing a non-empty interface value into an empty interface. +func packIfaceValueIntoEmptyIface(v Value) any { + return packEface(v) } // unpackEface converts the empty interface i to a Value. diff --git a/runtime/internal/lib/runtime/dit_linkname_llgo.go b/runtime/internal/lib/runtime/dit_linkname_llgo.go new file mode 100644 index 0000000000..b0807fe6ba --- /dev/null +++ b/runtime/internal/lib/runtime/dit_linkname_llgo.go @@ -0,0 +1,17 @@ +//go:build go1.26 + +package runtime + +import _ "unsafe" + +// llgo does not currently model per-goroutine DIT state. +// Provide minimal linkname shims so crypto/subtle can build and +// fall back to its existing non-DIT behavior when unsupported. + +//go:linkname dit_setEnabled crypto/subtle.setDITEnabled +func dit_setEnabled() bool { + return false +} + +//go:linkname dit_setDisabled crypto/subtle.setDITDisabled +func dit_setDisabled() {} diff --git a/runtime/internal/lib/runtime/fipsbypass_llgo.go b/runtime/internal/lib/runtime/fipsbypass_llgo.go new file mode 100644 index 0000000000..c4f21c462b --- /dev/null +++ b/runtime/internal/lib/runtime/fipsbypass_llgo.go @@ -0,0 +1,37 @@ +//go:build go1.26 + +package runtime + +import ( + _ "unsafe" + + latomic "github.com/goplus/llgo/runtime/internal/lib/sync/atomic" +) + +// llgo does not currently expose runtime.getg in this compatibility layer. +// Keep a minimal process-wide nesting count so crypto/fips140 can link and +// enforce bypass scopes without pulling more runtime internals into this tree. +var fipsBypassCount uint32 + +//go:linkname fips140_setBypass crypto/fips140.setBypass +func fips140_setBypass() { + latomic.AddUint32(&fipsBypassCount, 1) +} + +//go:linkname fips140_unsetBypass crypto/fips140.unsetBypass +func fips140_unsetBypass() { + for { + n := latomic.LoadUint32(&fipsBypassCount) + if n == 0 { + return + } + if latomic.CompareAndSwapUint32(&fipsBypassCount, n, n-1) { + return + } + } +} + +//go:linkname fips140_isBypassed crypto/fips140.isBypassed +func fips140_isBypassed() bool { + return latomic.LoadUint32(&fipsBypassCount) != 0 +} diff --git a/runtime/internal/lib/runtime/pprof_linkname_llgo.go b/runtime/internal/lib/runtime/pprof_linkname_llgo.go index 16c0138370..e7222ac61e 100644 --- a/runtime/internal/lib/runtime/pprof_linkname_llgo.go +++ b/runtime/internal/lib/runtime/pprof_linkname_llgo.go @@ -60,6 +60,19 @@ func pprof_goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) return 0, true } +//go:linkname runtime_goroutineLeakGC runtime/pprof.runtime_goroutineLeakGC +func runtime_goroutineLeakGC() {} + +//go:linkname runtime_goroutineleakcount runtime/pprof.runtime_goroutineleakcount +func runtime_goroutineleakcount() int { + return 0 +} + +//go:linkname pprof_goroutineLeakProfileWithLabels runtime.pprof_goroutineLeakProfileWithLabels +func pprof_goroutineLeakProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) { + return 0, true +} + //go:linkname pprof_memProfileInternal runtime.pprof_memProfileInternal func pprof_memProfileInternal(p []MemProfileRecord, inuseZero bool) (n int, ok bool) { return 0, true diff --git a/runtime/internal/lib/runtime/runtime_clearenv_llgo.go b/runtime/internal/lib/runtime/runtime_clearenv_llgo.go new file mode 100644 index 0000000000..5a425953b8 --- /dev/null +++ b/runtime/internal/lib/runtime/runtime_clearenv_llgo.go @@ -0,0 +1,12 @@ +//go:build go1.26 && (linux || darwin) + +package runtime + +import _ "unsafe" + +//go:linkname syscall_runtimeClearenv syscall.runtimeClearenv +func syscall_runtimeClearenv(env map[string]int) { + for k := range env { + syscall_runtimeUnsetenv(k) + } +} diff --git a/runtime/internal/lib/runtime/synctest_llgo.go b/runtime/internal/lib/runtime/synctest_llgo.go index b7c3e679bd..3707509f61 100644 --- a/runtime/internal/lib/runtime/synctest_llgo.go +++ b/runtime/internal/lib/runtime/synctest_llgo.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin || linux package runtime diff --git a/runtime/internal/lib/runtime/syscall_darwin_go126_llgo.go b/runtime/internal/lib/runtime/syscall_darwin_go126_llgo.go new file mode 100644 index 0000000000..a9eee3feba --- /dev/null +++ b/runtime/internal/lib/runtime/syscall_darwin_go126_llgo.go @@ -0,0 +1,63 @@ +//go:build darwin && go1.26 + +package runtime + +import _ "unsafe" + +//go:linkname llgo_syscall5f64 llgo.syscall +func llgo_syscall5f64(fn, a1, a2, a3, a4, a5 uintptr, f1 float64) (r1, r2, err uintptr) + +//go:linkname llgo_rawSyscall llgo.syscall +func llgo_rawSyscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) + +//go:linkname llgo_rawSyscall6 llgo.syscall +func llgo_rawSyscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) + +//go:linkname llgo_rawSyscall9 llgo.syscall +func llgo_rawSyscall9(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, err uintptr) + +// Go 1.26 moves errno normalization into syscall/syscall_darwin.go wrappers +// such as syscall9/rawSyscall9/syscallPtr, so runtime mirrors the raw runtime +// entry points and returns the libc errno unchanged here. + +//go:linkname syscall_syscalln syscall.syscalln +func syscall_syscalln(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { + entersyscall() + r1, r2, err = syscall_rawsyscalln(fn, args...) + exitsyscall() + return r1, r2, err +} + +//go:linkname syscall_rawsyscalln syscall.rawsyscalln +func syscall_rawsyscalln(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { + switch len(args) { + case 0: + return llgo_rawSyscall(fn, 0, 0, 0) + case 1: + return llgo_rawSyscall(fn, args[0], 0, 0) + case 2: + return llgo_rawSyscall(fn, args[0], args[1], 0) + case 3: + return llgo_rawSyscall(fn, args[0], args[1], args[2]) + case 4: + return llgo_rawSyscall6(fn, args[0], args[1], args[2], args[3], 0, 0) + case 5: + return llgo_rawSyscall6(fn, args[0], args[1], args[2], args[3], args[4], 0) + case 6: + return llgo_rawSyscall6(fn, args[0], args[1], args[2], args[3], args[4], args[5]) + case 7: + return llgo_rawSyscall9(fn, args[0], args[1], args[2], args[3], args[4], args[5], args[6], 0, 0) + case 8: + return llgo_rawSyscall9(fn, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], 0) + case 9: + return llgo_rawSyscall9(fn, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]) + default: + panic("runtime: unsupported darwin syscall arg count") + } +} + +//go:linkname crypto_x509_syscall crypto/x509/internal/macos.syscall +func crypto_x509_syscall(fn, a1, a2, a3, a4, a5 uintptr, f1 float64) uintptr { + r1, _, _ := llgo_syscall5f64(fn, a1, a2, a3, a4, a5, f1) + return r1 +} diff --git a/runtime/internal/lib/runtime/syscall_darwin_llgo.go b/runtime/internal/lib/runtime/syscall_darwin_llgo.go index 9ac2a258aa..12eb29a559 100644 --- a/runtime/internal/lib/runtime/syscall_darwin_llgo.go +++ b/runtime/internal/lib/runtime/syscall_darwin_llgo.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin && !go1.26 package runtime diff --git a/runtime/internal/lib/unique/clone.go b/runtime/internal/lib/unique/clone.go index 4211c8dd12..626790dd95 100644 --- a/runtime/internal/lib/unique/clone.go +++ b/runtime/internal/lib/unique/clone.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// llgo:skipall + package unique import ( diff --git a/runtime/internal/lib/unique/handle.go b/runtime/internal/lib/unique/handle.go new file mode 100644 index 0000000000..d7ddabe4c1 --- /dev/null +++ b/runtime/internal/lib/unique/handle.go @@ -0,0 +1,231 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// llgo:skipall + +package unique + +import ( + "runtime" + "sync" + "unsafe" + "weak" + + "github.com/goplus/llgo/runtime/abi" +) + +var zero uintptr + +// Handle is a globally unique identity for some value of type T. +// +// Two handles compare equal exactly if the two values used to create the handles +// would have also compared equal. The comparison of two handles is trivial and +// typically much more efficient than comparing the values used to create them. +type Handle[T comparable] struct { + value *T +} + +// Value returns a shallow copy of the T value that produced the Handle. +// Value is safe for concurrent use by multiple goroutines. +func (h Handle[T]) Value() T { + return *h.value +} + +// Make returns a globally unique handle for a value of type T. Handles +// are equal if and only if the values used to produce them are equal. +// Make is safe for concurrent use by multiple goroutines. +func Make[T comparable](value T) Handle[T] { + typ := abi.TypeFor[T]() + if typ.Size() == 0 { + return Handle[T]{(*T)(unsafe.Pointer(&zero))} + } + ma, ok := loadUniqueMap(typ) + if !ok { + ensureSetupMake() + ma = addUniqueMap[T](typ) + } + m := ma.(*uniqueMap[T]) + + var ( + toInsert *T + toInsertWeak weak.Pointer[T] + ) + newValue := func() (T, weak.Pointer[T]) { + if toInsert == nil { + toInsert = new(T) + *toInsert = clone(value, &m.cloneSeq) + toInsertWeak = weak.Make(toInsert) + } + return *toInsert, toInsertWeak + } + var ptr *T + for { + wp, ok := m.Load(value) + if !ok { + k, v := newValue() + wp, _ = m.LoadOrStore(k, v) + } + ptr = wp.Value() + if ptr != nil { + break + } + m.CompareAndDelete(value, wp) + } + runtime.KeepAlive(toInsert) + return Handle[T]{ptr} +} + +var ( + uniqueMapsMu sync.Mutex + uniqueMaps map[*abi.Type]any + + cleanupMu sync.Mutex + cleanupFuncsMu sync.Mutex + cleanupFuncs []func() + cleanupNotify []func() +) + +type uniqueMap[T comparable] struct { + mu sync.Mutex + m map[T]weak.Pointer[T] + cloneSeq +} + +type uniqueEntry[T comparable] struct { + key T + value weak.Pointer[T] +} + +func addUniqueMap[T comparable](typ *abi.Type) *uniqueMap[T] { + m := &uniqueMap[T]{ + m: make(map[T]weak.Pointer[T]), + cloneSeq: makeCloneSeq(typ), + } + a, loaded := loadOrStoreUniqueMap(typ, m) + if !loaded { + cleanupFuncsMu.Lock() + cleanupFuncs = append(cleanupFuncs, func() { + m.All()(func(key T, wp weak.Pointer[T]) bool { + if wp.Value() == nil { + m.CompareAndDelete(key, wp) + } + return true + }) + }) + cleanupFuncsMu.Unlock() + } + return a.(*uniqueMap[T]) +} + +var ( + setupMakeMu sync.Mutex + setupMade bool +) + +func ensureSetupMake() { + setupMakeMu.Lock() + if !setupMade { + registerCleanup() + setupMade = true + } + setupMakeMu.Unlock() +} + +func loadUniqueMap(typ *abi.Type) (any, bool) { + uniqueMapsMu.Lock() + if uniqueMaps == nil { + uniqueMapsMu.Unlock() + return nil, false + } + v, ok := uniqueMaps[typ] + uniqueMapsMu.Unlock() + return v, ok +} + +func loadOrStoreUniqueMap(typ *abi.Type, value any) (any, bool) { + uniqueMapsMu.Lock() + if uniqueMaps == nil { + uniqueMaps = make(map[*abi.Type]any) + } + if existing, ok := uniqueMaps[typ]; ok { + uniqueMapsMu.Unlock() + return existing, true + } + uniqueMaps[typ] = value + uniqueMapsMu.Unlock() + return value, false +} + +func (m *uniqueMap[T]) Load(key T) (weak.Pointer[T], bool) { + m.mu.Lock() + v, ok := m.m[key] + m.mu.Unlock() + if !ok { + var zero weak.Pointer[T] + return zero, false + } + return v, true +} + +func (m *uniqueMap[T]) LoadOrStore(key T, value weak.Pointer[T]) (weak.Pointer[T], bool) { + m.mu.Lock() + if existing, ok := m.m[key]; ok { + m.mu.Unlock() + return existing, true + } + m.m[key] = value + m.mu.Unlock() + return value, false +} + +func (m *uniqueMap[T]) CompareAndDelete(key T, old weak.Pointer[T]) bool { + m.mu.Lock() + defer m.mu.Unlock() + v, ok := m.m[key] + if !ok || v != old { + return false + } + delete(m.m, key) + return true +} + +func (m *uniqueMap[T]) All() func(func(T, weak.Pointer[T]) bool) { + return func(yield func(T, weak.Pointer[T]) bool) { + m.mu.Lock() + items := make([]uniqueEntry[T], 0, len(m.m)) + for k, v := range m.m { + items = append(items, uniqueEntry[T]{key: k, value: v}) + } + m.mu.Unlock() + for _, item := range items { + if !yield(item.key, item.value) { + return + } + } + } +} + +func registerCleanup() { + runtime_registerUniqueMapCleanup(func() { + cleanupMu.Lock() + + cleanupFuncsMu.Lock() + cf := cleanupFuncs + cleanupFuncsMu.Unlock() + + for _, f := range cf { + f() + } + + for _, f := range cleanupNotify { + f() + } + cleanupNotify = nil + + cleanupMu.Unlock() + }) +} + +//go:linkname runtime_registerUniqueMapCleanup +func runtime_registerUniqueMapCleanup(cleanup func()) diff --git a/runtime/internal/lib/unique/unique.go b/runtime/internal/lib/unique/unique.go deleted file mode 100644 index faaee1b155..0000000000 --- a/runtime/internal/lib/unique/unique.go +++ /dev/null @@ -1,6 +0,0 @@ -package unique - -import _ "unsafe" - -//go:linkname runtime_registerUniqueMapCleanup -func runtime_registerUniqueMapCleanup(cleanup func())