From c82832530d1828f4d609c3b90933d0c6d84ef535 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jul 2026 06:44:29 +0800 Subject: [PATCH 01/20] compiler: add standalone locality directive analysis --- internal/directive/directive.go | 77 +++++ internal/directive/directive_test.go | 47 +++ internal/locality/locality.go | 90 +++++ internal/locality/locality_test.go | 479 +++++++++++++++++++++++++++ internal/locality/prepare.go | 221 ++++++++++++ internal/locality/scan.go | 193 +++++++++++ 6 files changed, 1107 insertions(+) create mode 100644 internal/directive/directive.go create mode 100644 internal/directive/directive_test.go create mode 100644 internal/locality/locality.go create mode 100644 internal/locality/locality_test.go create mode 100644 internal/locality/prepare.go create mode 100644 internal/locality/scan.go diff --git a/internal/directive/directive.go b/internal/directive/directive.go new file mode 100644 index 0000000000..2678eb69ff --- /dev/null +++ b/internal/directive/directive.go @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package directive parses Go and LLGo source directives without assigning +// feature-specific semantics to them. +package directive + +import ( + "go/ast" + "go/token" + "strings" +) + +// Directive is one normalized Go or LLGo source directive. +type Directive struct { + Name string + Args string + Raw string + Pos token.Pos +} + +// Parse normalizes comment when it uses a supported directive spelling. +func Parse(comment *ast.Comment) (Directive, bool) { + if comment == nil { + return Directive{}, false + } + raw := comment.Text + var namespace, body string + switch { + case strings.HasPrefix(raw, "//go:"): + namespace, body = "go:", raw[len("//go:"):] + case strings.HasPrefix(raw, "//llgo:"): + namespace, body = "llgo:", raw[len("//llgo:"):] + case strings.HasPrefix(raw, "// llgo:"): + namespace, body = "llgo:", raw[len("// llgo:"):] + case strings.HasPrefix(raw, "//export "): + return Directive{Name: "export", Args: strings.TrimSpace(raw[len("//export "):]), Raw: raw, Pos: comment.Pos()}, true + default: + return Directive{}, false + } + body = strings.TrimSpace(body) + if body == "" { + return Directive{}, false + } + name, args := body, "" + if idx := strings.IndexAny(body, " \t"); idx >= 0 { + name, args = body[:idx], strings.TrimSpace(body[idx+1:]) + } + return Directive{Name: namespace + name, Args: args, Raw: raw, Pos: comment.Pos()}, true +} + +// ParseGroup returns all normalized directives in doc in source order. +func ParseGroup(doc *ast.CommentGroup) []Directive { + if doc == nil { + return nil + } + ret := make([]Directive, 0, len(doc.List)) + for _, comment := range doc.List { + if parsed, ok := Parse(comment); ok { + ret = append(ret, parsed) + } + } + return ret +} diff --git a/internal/directive/directive_test.go b/internal/directive/directive_test.go new file mode 100644 index 0000000000..c9bb47fd07 --- /dev/null +++ b/internal/directive/directive_test.go @@ -0,0 +1,47 @@ +package directive + +import ( + "go/ast" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + text string + name string + args string + ok bool + }{ + {text: "// ordinary"}, + {text: "//go:"}, + {text: "//go:noinline", name: "go:noinline", ok: true}, + {text: "//llgo:tls", name: "llgo:tls", ok: true}, + {text: "// llgo:type C", name: "llgo:type", args: "C", ok: true}, + {text: "//llgo:link\tF C.f", name: "llgo:link", args: "F C.f", ok: true}, + {text: "//export F", name: "export", args: "F", ok: true}, + } + if _, ok := Parse(nil); ok { + t.Fatal("nil comment parsed as a directive") + } + if ParseGroup(nil) != nil { + t.Fatal("nil comment group returned directives") + } + for _, test := range tests { + got, ok := Parse(&ast.Comment{Text: test.text}) + if ok != test.ok || got.Name != test.name || got.Args != test.args || got.Raw != map[bool]string{true: test.text}[test.ok] { + t.Fatalf("Parse(%q) = %+v, %v", test.text, got, ok) + } + } +} + +func TestParseGroupPreservesSourceOrder(t *testing.T) { + doc := &ast.CommentGroup{List: []*ast.Comment{ + {Text: "// ordinary"}, + {Text: "//go:noinline"}, + {Text: "//llgo:tls"}, + }} + got := ParseGroup(doc) + if len(got) != 2 || got[0].Name != "go:noinline" || got[1].Name != "llgo:tls" { + t.Fatalf("ParseGroup = %+v", got) + } +} diff --git a/internal/locality/locality.go b/internal/locality/locality.go new file mode 100644 index 0000000000..1e9a7bfca3 --- /dev/null +++ b/internal/locality/locality.go @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package locality defines the source-level TLS and GLS declaration model. +// It intentionally has no dependency on LLGo's SSA or LLVM lowering layers. +package locality + +import "fmt" + +const ( + ThreadDirective = "//llgo:tls" + GoroutineDirective = "//llgo:gls" + InitPrefix = "__llgo_local_init_" +) + +// Kind identifies the execution context that owns a package variable. +type Kind uint8 + +const ( + None Kind = iota + Thread + Goroutine +) + +// Info is the locality-specific part of a declaration's compiler metadata. +type Info struct { + Locality Kind + HasInitializer bool + InitFunc string + InitOrder int +} + +func (kind Kind) String() string { + switch kind { + case None: + return "" + case Thread: + return "tls" + case Goroutine: + return "gls" + default: + return fmt.Sprintf("invalid:%d", kind) + } +} + +// Parse converts the cache representation of a locality into a Kind. +func Parse(name string) (Kind, bool) { + switch name { + case "": + return None, true + case "tls": + return Thread, true + case "gls": + return Goroutine, true + default: + return None, false + } +} + +// Directive returns the source directive for kind. +func Directive(kind Kind) string { + if kind == Goroutine { + return GoroutineDirective + } + return ThreadDirective +} + +// Merge combines declaration- and spec-level locality directives. +func Merge(a, b Kind) (Kind, bool) { + if a != None && b != None && a != b { + return None, false + } + if b != None { + return b, true + } + return a, true +} diff --git a/internal/locality/locality_test.go b/internal/locality/locality_test.go new file mode 100644 index 0000000000..4a8ac7d44b --- /dev/null +++ b/internal/locality/locality_test.go @@ -0,0 +1,479 @@ +package locality + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" +) + +func TestKindEncodingAndNames(t *testing.T) { + tests := []struct { + kind Kind + name string + }{ + {None, ""}, + {Thread, "tls"}, + {Goroutine, "gls"}, + } + for _, test := range tests { + if got := test.kind.String(); got != test.name { + t.Fatalf("Kind(%d).String() = %q, want %q", test.kind, got, test.name) + } + if got, ok := Parse(test.name); !ok || got != test.kind { + t.Fatalf("Parse(%q) = %v, %v", test.name, got, ok) + } + } + if got := Kind(99).String(); got != "invalid:99" { + t.Fatalf("invalid locality name = %q", got) + } + if _, ok := Parse("invalid:99"); ok { + t.Fatal("Parse accepted invalid locality") + } + if Directive(Thread) != ThreadDirective || Directive(Goroutine) != GoroutineDirective { + t.Fatal("unexpected locality directive names") + } +} + +func TestMerge(t *testing.T) { + tests := []struct { + a, b Kind + want Kind + ok bool + }{ + {want: None, ok: true}, + {a: Thread, want: Thread, ok: true}, + {b: Goroutine, want: Goroutine, ok: true}, + {a: Thread, b: Thread, want: Thread, ok: true}, + {a: Thread, b: Goroutine, want: None}, + } + for _, test := range tests { + got, ok := Merge(test.a, test.b) + if got != test.want || ok != test.ok { + t.Fatalf("Merge(%v, %v) = %v, %v", test.a, test.b, got, ok) + } + } +} + +func TestScanPackageVar(t *testing.T) { + fset, file := parseFile(t, `package p + +//llgo:tls +var ( + first int + //llgo:gls + second int +) +`) + decl := file.Decls[0].(*ast.GenDecl) + if _, err := ScanPackageVar(fset, decl); err == nil || !strings.Contains(err.Error(), "cannot apply to the same variable declaration") { + t.Fatalf("ScanPackageVar conflict error = %v", err) + } + + fset, file = parseFile(t, `package p + +var ( + //llgo:tls + first, second = 1, 2 +) +`) + vars, err := ScanPackageVar(fset, file.Decls[0].(*ast.GenDecl)) + if err != nil { + t.Fatal(err) + } + if len(vars) != 2 || vars[0].Info.Locality != Thread || !vars[0].Info.HasInitializer || vars[1].Name != "second" { + t.Fatalf("ScanPackageVar = %+v", vars) + } +} + +func TestScanPackageVarBranches(t *testing.T) { + comment := func(text string) *ast.CommentGroup { + doc := &ast.CommentGroup{} + for _, line := range strings.Split(text, "\n") { + doc.List = append(doc.List, &ast.Comment{Text: line}) + } + return doc + } + tests := []struct { + name string + decl *ast.GenDecl + want string + }{ + { + name: "declaration error", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:tls extra")}, + want: "does not accept arguments", + }, + { + name: "spec error", + decl: &ast.GenDecl{Tok: token.VAR, Specs: []ast.Spec{&ast.ValueSpec{Doc: comment("//llgo:gls extra")}}}, + want: "does not accept arguments", + }, + { + name: "embed conflict", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:tls\n//go:embed value.txt"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("Value")}}}}, + want: "//go:embed", + }, + { + name: "blank name", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:tls"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("_")}}}}, + want: "blank identifier", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := ScanPackageVar(nil, test.decl); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ScanPackageVar error = %v, want %q", err, test.want) + } + }) + } + + ordinary := &ast.GenDecl{Tok: token.VAR, Specs: []ast.Spec{ + &ast.ImportSpec{}, + &ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("Value")}}, + }} + if vars, err := ScanPackageVar(nil, ordinary); err != nil || len(vars) != 0 { + t.Fatalf("ordinary ScanPackageVar = %+v, %v", vars, err) + } +} + +func TestDirectivePlacementDiagnostics(t *testing.T) { + tests := []struct { + name string + src string + want string + }{ + { + name: "grouped import spec", + src: `package p +import ( + //llgo:tls + "unsafe" +) +var _ = unsafe.Sizeof(0) +`, + want: "package-level var", + }, + { + name: "local var", + src: `package p +func f() { + //llgo:gls + var value int + _ = value +} +`, + want: "package-level var", + }, + { + name: "nested function local", + src: `package p +func f() { + _ = func() { + //llgo:tls extra + var value int + _ = value + } +} +`, + want: "does not accept arguments", + }, + { + name: "nested function in local initializer", + src: `package p +func f() { + var nested = func() { + //llgo:gls + var value int + _ = value + } + _ = nested +} +`, + want: "package-level var", + }, + { + name: "function", + src: `package p +//llgo:tls +func f() {} +`, + want: "package-level var", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fset, file := parseFile(t, test.src) + err := validateFile(fset, file) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("validation error = %v, want %q", err, test.want) + } + }) + } +} + +func TestDirectiveDiagnostics(t *testing.T) { + tests := []struct { + comment string + want string + }{ + {"//llgo:threadlocal", "use //llgo:tls"}, + {"//llgo:goroutinelocal", "use //llgo:gls"}, + {"//llgo:tls extra", "does not accept arguments"}, + {"//llgo:tls\n//llgo:gls", "cannot apply to the same variable declaration"}, + } + for _, test := range tests { + doc := &ast.CommentGroup{} + for _, line := range strings.Split(test.comment, "\n") { + doc.List = append(doc.List, &ast.Comment{Text: line}) + } + if _, _, err := FromDoc(nil, doc); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("FromDoc(%q) error = %v", test.comment, err) + } + } + if err := ValidateDoc(nil, nil); err != nil { + t.Fatal(err) + } + if kind, _, err := FromDoc(nil, &ast.CommentGroup{List: []*ast.Comment{{Text: "//go:noinline"}}}); err != nil || kind != None { + t.Fatalf("ordinary directive = %v, %v", kind, err) + } + if err := ValidateFuncBody(nil, nil); err != nil { + t.Fatal(err) + } + typeDecl := &ast.GenDecl{Tok: token.TYPE, Specs: []ast.Spec{ + &ast.TypeSpec{Name: ast.NewIdent("T"), Doc: &ast.CommentGroup{List: []*ast.Comment{{Text: "//llgo:tls"}}}}, + }} + if err := ValidateNonPackageVar(nil, typeDecl); err == nil || !strings.Contains(err.Error(), "package-level var") { + t.Fatalf("type spec validation error = %v", err) + } + if hasDirective(&ast.CommentGroup{List: []*ast.Comment{{Text: "//go:noinline"}}}, "go:embed") { + t.Fatal("hasDirective matched an unrelated directive") + } +} + +func TestInitializerLocalityDiagnostics(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + thread := types.NewVar(token.NoPos, pkg, "thread", types.Typ[types.Int]) + goroutine := types.NewVar(token.NoPos, pkg, "goroutine", types.Typ[types.Int]) + ordinary := types.NewVar(token.NoPos, pkg, "ordinary", types.Typ[types.Int]) + vars := map[string]Info{ + "thread": {Locality: Thread, HasInitializer: true}, + "goroutine": {Locality: Goroutine, HasInitializer: true}, + } + rhs := ast.NewIdent("rhs") + tests := []struct { + lhs []*types.Var + want string + }{ + {[]*types.Var{thread, ordinary}, "mix local and ordinary"}, + {[]*types.Var{ordinary, thread}, "mix local and ordinary"}, + {[]*types.Var{thread, goroutine}, "mix thread-local and goroutine-local"}, + } + for _, test := range tests { + if _, _, err := initializerLocality(nil, &types.Initializer{Lhs: test.lhs, Rhs: rhs}, vars); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("initializerLocality error = %v, want %q", err, test.want) + } + } + if kind, found, err := initializerLocality(nil, &types.Initializer{Lhs: []*types.Var{ordinary}, Rhs: rhs}, vars); err != nil || found || kind != None { + t.Fatalf("ordinary initializer = %v, %v, %v", kind, found, err) + } +} + +func TestPrepareIsIdempotentAcrossPrograms(t *testing.T) { + fset, file := parseFile(t, `package p +func makeValue() *int { value := 42; return &value } +//llgo:tls +var Value = makeValue() +`) + files := []*ast.File{file} + info := newTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/p", fset, files, info) + if err != nil { + t.Fatal(err) + } + vars, err := ScanPackageVar(fset, file.Decls[1].(*ast.GenDecl)) + if err != nil { + t.Fatal(err) + } + raw := make(map[string]Info) + for _, variable := range vars { + raw[variable.Name] = variable.Info + } + + prepared, err := Prepare(fset, pkg.Path(), pkg, info, files, raw) + if err != nil { + t.Fatal(err) + } + declCount := len(file.Decls) + scopeCount := len(pkg.Scope().Names()) + initName := prepared["Value"].InitFunc + if initName != "example.com/p.__llgo_local_init_0" || prepared["Value"].InitOrder != 1 { + t.Fatalf("prepared metadata = %+v", prepared["Value"]) + } + + again, err := Prepare(fset, pkg.Path(), pkg, info, files, prepared) + if err != nil { + t.Fatal(err) + } + reused, err := Prepare(fset, pkg.Path(), pkg, info, files, raw) + if err != nil { + t.Fatal(err) + } + if len(file.Decls) != declCount || len(pkg.Scope().Names()) != scopeCount { + t.Fatalf("repeated Prepare changed syntax/scope: decls=%d/%d scope=%d/%d", len(file.Decls), declCount, len(pkg.Scope().Names()), scopeCount) + } + if again["Value"] != prepared["Value"] || reused["Value"] != prepared["Value"] { + t.Fatalf("repeated metadata = %+v, reused = %+v, want %+v", again["Value"], reused["Value"], prepared["Value"]) + } +} + +func TestPrepareZeroValuePointerAndValidation(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + value := types.NewVar(token.NoPos, pkg, "Value", types.NewPointer(types.Typ[types.Int])) + pkg.Scope().Insert(value) + vars := map[string]Info{"Value": {Locality: Goroutine}} + prepared, err := Prepare(nil, pkg.Path(), pkg, &types.Info{}, nil, vars) + if err != nil { + t.Fatal(err) + } + if got := prepared["Value"]; got.InitFunc != "" || got.InitOrder != 0 { + t.Fatalf("zero-value initializer = %+v", got) + } + if err := ValidatePrepared(pkg.Path(), map[string]Info{"Value": {Locality: Thread, HasInitializer: true}}); err == nil { + t.Fatal("ValidatePrepared accepted missing initializer metadata") + } + if err := ValidatePrepared(pkg.Path(), map[string]Info{"Value": {Locality: Thread, InitFunc: "p.init", InitOrder: 1}}); err == nil { + t.Fatal("ValidatePrepared accepted initializer metadata on a zero-value declaration") + } + if err := ValidatePrepared(pkg.Path(), map[string]Info{ + "Ordinary": {}, + "Value": {Locality: Thread, HasInitializer: true, InitFunc: "p.init", InitOrder: 1}, + }); err != nil { + t.Fatal(err) + } +} + +func TestPrepareEarlyReturnsAndMissingFiles(t *testing.T) { + if got, err := Prepare(nil, "", nil, nil, nil, nil); err != nil || len(got) != 0 { + t.Fatalf("nil Prepare = %+v, %v", got, err) + } + pkg := types.NewPackage("example.com/p", "p") + value := types.NewVar(token.NoPos, pkg, "Value", types.Typ[types.Int]) + pkg.Scope().Insert(value) + info := &types.Info{InitOrder: []*types.Initializer{{Lhs: []*types.Var{value}, Rhs: ast.NewIdent("rhs")}}} + vars := map[string]Info{"Value": {Locality: Thread, HasInitializer: true}} + if _, err := Prepare(nil, pkg.Path(), pkg, info, nil, vars); err == nil || !strings.Contains(err.Error(), "without syntax files") { + t.Fatalf("Prepare without files error = %v", err) + } + if got, err := Prepare(nil, pkg.Path(), pkg, &types.Info{}, nil, nil); err != nil || len(got) != 0 { + t.Fatalf("Prepare without localities = %+v, %v", got, err) + } +} + +func TestPrepareInitializerBranches(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + local := types.NewVar(token.NoPos, pkg, "Local", types.Typ[types.Int]) + ordinary := types.NewVar(token.NoPos, pkg, "Ordinary", types.Typ[types.Int]) + pkg.Scope().Insert(local) + pkg.Scope().Insert(ordinary) + rhs := ast.NewIdent("rhs") + info := &types.Info{InitOrder: []*types.Initializer{ + {Lhs: []*types.Var{ordinary}, Rhs: ast.NewIdent("ordinary")}, + {Lhs: []*types.Var{local, ordinary}, Rhs: rhs}, + }} + vars := map[string]Info{"Local": {Locality: Thread, HasInitializer: true}} + if _, err := Prepare(nil, pkg.Path(), pkg, info, []*ast.File{{}}, vars); err == nil || !strings.Contains(err.Error(), "mix local and ordinary") { + t.Fatalf("mixed Prepare error = %v", err) + } + + info.InitOrder = []*types.Initializer{{Lhs: []*types.Var{local}, Rhs: rhs}} + prepared, err := Prepare(nil, pkg.Path(), pkg, info, []*ast.File{{}}, vars) + if err != nil { + t.Fatal(err) + } + if prepared["Local"].InitFunc == "" || info.Uses == nil || info.Defs == nil { + t.Fatalf("Prepare did not initialize type maps: %+v", prepared["Local"]) + } + + conflicting := &types.Initializer{Lhs: []*types.Var{local, ordinary}, Rhs: rhs} + if got := preparedInitName(conflicting, map[string]Info{ + "Local": {InitFunc: "p.first", InitOrder: 1}, + "Ordinary": {InitFunc: "p.second", InitOrder: 1}, + }, 1); got != "" { + t.Fatalf("preparedInitName accepted conflicting helpers: %q", got) + } + if got := qualify("", "Value"); got != "Value" { + t.Fatalf("qualify empty package = %q", got) + } +} + +func TestFindLocalInitializerRejectsLookalikes(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + value := types.NewVar(token.NoPos, pkg, "Value", types.Typ[types.Int]) + rhs := ast.NewIdent("rhs") + initializer := &types.Initializer{Lhs: []*types.Var{value}, Rhs: rhs} + files := []*ast.File{{Decls: []ast.Decl{ + &ast.FuncDecl{ + Name: ast.NewIdent(InitPrefix + "0"), + Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: rhs}}}, + }, + &ast.FuncDecl{ + Name: ast.NewIdent(InitPrefix + "1"), + Body: &ast.BlockStmt{List: []ast.Stmt{&ast.AssignStmt{ + Lhs: []ast.Expr{&ast.BasicLit{Kind: token.INT, Value: "1"}}, + Tok: token.ASSIGN, + Rhs: []ast.Expr{rhs}, + }}}, + }, + }}} + if got := findLocalInitializer(pkg, &types.Info{}, files, initializer); got != "" { + t.Fatalf("findLocalInitializer accepted lookalike %q", got) + } +} + +func parseFile(t *testing.T, src string) (*token.FileSet, *ast.File) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "source.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + return fset, file +} + +func validateFile(fset *token.FileSet, file *ast.File) error { + for _, node := range file.Decls { + switch decl := node.(type) { + case *ast.FuncDecl: + if err := ValidateDoc(fset, decl.Doc); err != nil { + return err + } + if err := ValidateFuncBody(fset, decl.Body); err != nil { + return err + } + case *ast.GenDecl: + if decl.Tok == token.VAR { + if _, err := ScanPackageVar(fset, decl); err != nil { + return err + } + } else if err := ValidateNonPackageVar(fset, decl); err != nil { + return err + } + } + } + return nil +} + +func newTypeInfo() *types.Info { + return &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } +} diff --git a/internal/locality/prepare.go b/internal/locality/prepare.go new file mode 100644 index 0000000000..429dd78834 --- /dev/null +++ b/internal/locality/prepare.go @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package locality + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" +) + +// Prepare rewrites local package initializers into replayable synthetic +// functions and returns updated metadata. It is idempotent for repeated calls, +// including calls made by another compiler Program reusing the same syntax and +// types.Package objects. +func Prepare(fset *token.FileSet, pkgPath string, pkg *types.Package, typeInfo *types.Info, files []*ast.File, vars map[string]Info) (map[string]Info, error) { + ret := cloneInfo(vars) + if pkg == nil || typeInfo == nil || !hasLocality(ret) { + return ret, nil + } + + nextName := 0 + for order, initializer := range typeInfo.InitOrder { + _, found, err := initializerLocality(fset, initializer, ret) + if err != nil { + return nil, err + } + if !found { + continue + } + initOrder := order + 1 + if initName := preparedInitName(initializer, ret, initOrder); initName != "" { + setInitializerNames(initializer, ret, initName, initOrder) + continue + } + if len(files) == 0 { + return nil, fmt.Errorf("cannot prepare local initializer for package %q without syntax files", pkgPath) + } + name := findLocalInitializer(pkg, typeInfo, files, initializer) + if name == "" { + for { + name = fmt.Sprintf("%s%d", InitPrefix, nextName) + nextName++ + if pkg.Scope().Lookup(name) == nil { + break + } + } + fnObj, decl := makeLocalInitializer(pkg, typeInfo, name, initializer) + pkg.Scope().Insert(fnObj) + files[len(files)-1].Decls = append(files[len(files)-1].Decls, decl) + } + setInitializerNames(initializer, ret, qualify(pkgPath, name), initOrder) + } + + return ret, nil +} + +// ValidatePrepared verifies that every explicit local initializer was prepared +// before Go SSA construction. +func ValidatePrepared(pkgPath string, vars map[string]Info) error { + names := make([]string, 0, len(vars)) + for name := range vars { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + info := vars[name] + if info.Locality == None { + continue + } + prepared := info.InitFunc != "" && info.InitOrder != 0 + if info.HasInitializer != prepared { + return fmt.Errorf("local variable %s has inconsistent initializer metadata before SSA compilation", qualify(pkgPath, name)) + } + } + return nil +} + +func initializerLocality(fset *token.FileSet, initializer *types.Initializer, vars map[string]Info) (Kind, bool, error) { + var kind Kind + localCount := 0 + for _, variable := range initializer.Lhs { + info, ok := vars[variable.Name()] + if !ok || info.Locality == None { + if localCount != 0 { + return None, false, errorAt(fset, initializer.Rhs.Pos(), "one initializer cannot mix local and ordinary package variables") + } + continue + } + if localCount == 0 { + kind = info.Locality + } else if kind != info.Locality { + return None, false, errorAt(fset, initializer.Rhs.Pos(), "one initializer cannot mix thread-local and goroutine-local variables") + } + localCount++ + } + if localCount != 0 && len(initializer.Lhs) != localCount { + return None, false, errorAt(fset, initializer.Rhs.Pos(), "one initializer cannot mix local and ordinary package variables") + } + return kind, localCount != 0, nil +} + +func preparedInitName(initializer *types.Initializer, vars map[string]Info, order int) string { + var name string + for _, variable := range initializer.Lhs { + info := vars[variable.Name()] + if info.InitFunc == "" || info.InitOrder != order { + return "" + } + if name == "" { + name = info.InitFunc + } else if name != info.InitFunc { + return "" + } + } + return name +} + +func findLocalInitializer(pkg *types.Package, info *types.Info, files []*ast.File, initializer *types.Initializer) string { + for _, file := range files { + for _, node := range file.Decls { + decl, ok := node.(*ast.FuncDecl) + if !ok || len(decl.Name.Name) < len(InitPrefix) || decl.Name.Name[:len(InitPrefix)] != InitPrefix || decl.Body == nil || len(decl.Body.List) != 1 { + continue + } + assign, ok := decl.Body.List[0].(*ast.AssignStmt) + if !ok || assign.Tok != token.ASSIGN || len(assign.Rhs) != 1 || assign.Rhs[0] != initializer.Rhs || len(assign.Lhs) != len(initializer.Lhs) { + continue + } + matches := true + for i, lhs := range assign.Lhs { + ident, ok := lhs.(*ast.Ident) + if !ok || info.Uses[ident] != initializer.Lhs[i] { + matches = false + break + } + } + if matches { + if object := pkg.Scope().Lookup(decl.Name.Name); object == info.Defs[decl.Name] { + return decl.Name.Name + } + } + } + } + return "" +} + +func makeLocalInitializer(pkg *types.Package, info *types.Info, name string, initializer *types.Initializer) (*types.Func, *ast.FuncDecl) { + if info.Uses == nil { + info.Uses = make(map[*ast.Ident]types.Object) + } + if info.Defs == nil { + info.Defs = make(map[*ast.Ident]types.Object) + } + lhs := make([]ast.Expr, len(initializer.Lhs)) + for i, variable := range initializer.Lhs { + ident := ast.NewIdent(variable.Name()) + info.Uses[ident] = variable + lhs[i] = ident + } + nameIdent := ast.NewIdent(name) + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + fnObj := types.NewFunc(token.NoPos, pkg, name, sig) + info.Defs[nameIdent] = fnObj + decl := &ast.FuncDecl{ + Name: nameIdent, + Type: &ast.FuncType{Params: &ast.FieldList{}}, + Body: &ast.BlockStmt{List: []ast.Stmt{ + &ast.AssignStmt{Lhs: lhs, Tok: token.ASSIGN, Rhs: []ast.Expr{initializer.Rhs}}, + }}, + } + return fnObj, decl +} + +func setInitializerNames(initializer *types.Initializer, vars map[string]Info, initName string, order int) { + for _, variable := range initializer.Lhs { + info := vars[variable.Name()] + info.InitFunc = initName + info.InitOrder = order + vars[variable.Name()] = info + } +} + +func cloneInfo(vars map[string]Info) map[string]Info { + ret := make(map[string]Info, len(vars)) + for name, info := range vars { + ret[name] = info + } + return ret +} + +func hasLocality(vars map[string]Info) bool { + for _, info := range vars { + if info.Locality != None { + return true + } + } + return false +} + +func qualify(pkgPath, name string) string { + if pkgPath == "" { + return name + } + return pkgPath + "." + name +} diff --git a/internal/locality/scan.go b/internal/locality/scan.go new file mode 100644 index 0000000000..b458a81795 --- /dev/null +++ b/internal/locality/scan.go @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package locality + +import ( + "fmt" + "go/ast" + "go/token" + + "github.com/goplus/llgo/internal/directive" +) + +const ( + legacyThread = "llgo:threadlocal" + legacyGoroutine = "llgo:goroutinelocal" +) + +// Variable records locality metadata collected for one package variable. +type Variable struct { + Name string + Info Info +} + +// ScanPackageVar validates and collects locality directives on a package-level +// var declaration. +func ScanPackageVar(fset *token.FileSet, decl *ast.GenDecl) ([]Variable, error) { + declKind, declPos, err := FromDoc(fset, decl.Doc) + if err != nil { + return nil, err + } + var ret []Variable + for _, node := range decl.Specs { + spec, ok := node.(*ast.ValueSpec) + if !ok { + continue + } + specKind, specPos, err := FromDoc(fset, spec.Doc) + if err != nil { + return nil, err + } + kind, _, err := mergeAt(fset, declKind, declPos, specKind, specPos) + if err != nil { + return nil, err + } + if kind == None { + continue + } + if hasDirective(decl.Doc, "go:embed") || hasDirective(spec.Doc, "go:embed") { + return nil, errorAt(fset, spec.Pos(), "%s and //go:embed cannot apply to the same variable declaration", Directive(kind)) + } + for _, ident := range spec.Names { + if ident.Name == "_" { + return nil, errorAt(fset, ident.Pos(), "locality directive cannot apply to the blank identifier") + } + ret = append(ret, Variable{ + Name: ident.Name, + Info: Info{Locality: kind, HasInitializer: len(spec.Values) != 0}, + }) + } + } + return ret, nil +} + +// ValidateNonPackageVar rejects locality directives on declarations other +// than package-level vars, including grouped import/type/const specs. +func ValidateNonPackageVar(fset *token.FileSet, decl *ast.GenDecl) error { + if err := ValidateDoc(fset, decl.Doc); err != nil { + return err + } + for _, node := range decl.Specs { + var doc *ast.CommentGroup + switch spec := node.(type) { + case *ast.ImportSpec: + doc = spec.Doc + case *ast.TypeSpec: + doc = spec.Doc + case *ast.ValueSpec: + doc = spec.Doc + } + if err := ValidateDoc(fset, doc); err != nil { + return err + } + } + return nil +} + +// ValidateFuncBody rejects locality directives on declarations nested inside +// a function or function literal. +func ValidateFuncBody(fset *token.FileSet, body *ast.BlockStmt) error { + if body == nil { + return nil + } + var firstErr error + ast.Inspect(body, func(node ast.Node) bool { + if firstErr != nil { + return false + } + stmt, ok := node.(*ast.DeclStmt) + if !ok { + return true + } + decl, ok := stmt.Decl.(*ast.GenDecl) + if ok { + firstErr = ValidateNonPackageVar(fset, decl) + } + return firstErr == nil + }) + return firstErr +} + +// FromDoc returns the locality directive attached to doc. +func FromDoc(fset *token.FileSet, doc *ast.CommentGroup) (Kind, token.Pos, error) { + var kind Kind + var pos token.Pos + for _, directive := range directive.ParseGroup(doc) { + var next Kind + switch directive.Name { + case "llgo:tls": + next = Thread + case "llgo:gls": + next = Goroutine + case legacyThread: + return None, token.NoPos, errorAt(fset, directive.Pos, "//%s is not supported; use %s", legacyThread, ThreadDirective) + case legacyGoroutine: + return None, token.NoPos, errorAt(fset, directive.Pos, "//%s is not supported; use %s", legacyGoroutine, GoroutineDirective) + default: + continue + } + if directive.Args != "" { + return None, token.NoPos, errorAt(fset, directive.Pos, "//%s does not accept arguments", directive.Name) + } + var err error + kind, pos, err = mergeAt(fset, kind, pos, next, directive.Pos) + if err != nil { + return None, token.NoPos, err + } + } + return kind, pos, nil +} + +// ValidateDoc rejects a locality directive outside a package-level var. +func ValidateDoc(fset *token.FileSet, doc *ast.CommentGroup) error { + kind, pos, err := FromDoc(fset, doc) + if err != nil { + return err + } + if kind != None { + return errorAt(fset, pos, "%s applies only to package-level var declarations", Directive(kind)) + } + return nil +} + +func mergeAt(fset *token.FileSet, a Kind, apos token.Pos, b Kind, bpos token.Pos) (Kind, token.Pos, error) { + merged, ok := Merge(a, b) + if !ok { + return None, token.NoPos, errorAt(fset, bpos, "%s and %s cannot apply to the same variable declaration", ThreadDirective, GoroutineDirective) + } + if b != None { + return merged, bpos, nil + } + return merged, apos, nil +} + +func hasDirective(doc *ast.CommentGroup, name string) bool { + for _, directive := range directive.ParseGroup(doc) { + if directive.Name == name { + return true + } + } + return false +} + +func errorAt(fset *token.FileSet, pos token.Pos, format string, args ...any) error { + msg := fmt.Sprintf(format, args...) + if fset == nil || pos == token.NoPos { + return fmt.Errorf("%s", msg) + } + return fmt.Errorf("%s: %s", fset.Position(pos), msg) +} From 9a8afa0fc4933f7bee892a7b8b34e097a3cfcc97 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jul 2026 19:08:20 +0800 Subject: [PATCH 02/20] runtime: add stack-rooted locality contexts --- runtime/internal/runtime/local_context.go | 136 ++++++++++++++++++ .../runtime/local_context_baremetal.go | 23 +++ .../internal/runtime/local_context_stub.go | 35 +++++ runtime/internal/runtime/local_context_tls.go | 26 ++++ runtime/internal/runtime/local_initializer.go | 62 ++++++++ runtime/internal/runtime/z_default.go | 1 + 6 files changed, 283 insertions(+) create mode 100644 runtime/internal/runtime/local_context.go create mode 100644 runtime/internal/runtime/local_context_baremetal.go create mode 100644 runtime/internal/runtime/local_context_stub.go create mode 100644 runtime/internal/runtime/local_context_tls.go create mode 100644 runtime/internal/runtime/local_initializer.go diff --git a/runtime/internal/runtime/local_context.go b/runtime/internal/runtime/local_context.go new file mode 100644 index 0000000000..95f6226f8f --- /dev/null +++ b/runtime/internal/runtime/local_context.go @@ -0,0 +1,136 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "unsafe" + +// LocalContext is rooted by the outer Go entry stack frame. The current +// one-thread-per-goroutine backend maps both logical locality kinds to this one +// physical package store. +type LocalContext struct { + blocks *localBlock +} + +type localBlock struct { + next *localBlock + key unsafe.Pointer +} + +func EnterLocalContext(ctx *LocalContext) uintptr { + previous := currentLocalContext + if previous == 0 { + if ctx == nil { + panic("runtime: nil local context") + } + currentLocalContext = uintptr(unsafe.Pointer(ctx)) + } + return previous +} + +func LeaveLocalContext(ctx *LocalContext, previous uintptr) { + if previous != 0 { + if currentLocalContext != previous { + panic("runtime: local context changed by nested entry") + } + return + } + if currentLocalContext != uintptr(unsafe.Pointer(ctx)) { + panic("runtime: leaving inactive local context") + } + currentLocalContext = 0 + releaseLocalBlocks(ctx) +} + +func leaveCurrentLocalContext() { + ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) + if ctx == nil { + return + } + currentLocalContext = 0 + releaseLocalBlocks(ctx) +} + +func releaseLocalBlocks(ctx *LocalContext) { + block := ctx.blocks + ctx.blocks = nil + for block != nil { + next := block.next + // Do not free block here: an address of a local variable may outlive its + // owner. Breaking the links lets the GC retain only escaped blocks. + block.next = nil + block = next + } +} + +// LocalPackage returns stable, zeroed storage for one package in the current +// physical owner. Repeated access to the most recently used package takes the +// head fast path; other accesses move the matching block to the front. +func LocalPackage(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { + ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) + if ctx == nil { + panic("runtime: local variable accessed outside a Go entry context") + } + if key == nil { + panic("runtime: nil local package key") + } + if align == 0 || align&(align-1) != 0 { + panic("runtime: invalid local package alignment") + } + first := ctx.blocks + if first != nil && first.key == key { + return localBlockData(first, align) + } + var previous *localBlock + for block := first; block != nil; block = block.next { + if block.key == key { + previous.next = block.next + block.next = first + ctx.blocks = block + return localBlockData(block, align) + } + previous = block + } + block := newLocalBlock(key, size, align) + block.next = first + ctx.blocks = block + return localBlockData(block, align) +} + +func newLocalBlock(key unsafe.Pointer, size, align uintptr) *localBlock { + header := unsafe.Sizeof(localBlock{}) + padding := align - 1 + if size == 0 { + size = 1 + } + if header > ^uintptr(0)-padding || header+padding > ^uintptr(0)-size { + panic("runtime: local package size overflow") + } + block := (*localBlock)(AllocZ(header + padding + size)) + if block == nil { + panic("runtime: failed to allocate local package") + } + block.key = key + return block +} + +func localBlockData(block *localBlock, align uintptr) unsafe.Pointer { + padding := align - 1 + data := (uintptr(unsafe.Pointer(block)) + unsafe.Sizeof(localBlock{}) + padding) &^ padding + return unsafe.Pointer(data) +} diff --git a/runtime/internal/runtime/local_context_baremetal.go b/runtime/internal/runtime/local_context_baremetal.go new file mode 100644 index 0000000000..30557385ec --- /dev/null +++ b/runtime/internal/runtime/local_context_baremetal.go @@ -0,0 +1,23 @@ +//go:build llgo && baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +// Bare-metal runtimes have one execution context and do not introduce a native +// TLS relocation merely by linking the locality runtime support. +var currentLocalContext uintptr diff --git a/runtime/internal/runtime/local_context_stub.go b/runtime/internal/runtime/local_context_stub.go new file mode 100644 index 0000000000..ebd713e575 --- /dev/null +++ b/runtime/internal/runtime/local_context_stub.go @@ -0,0 +1,35 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "unsafe" + +type LocalContext struct{} + +func EnterLocalContext(ctx *LocalContext) uintptr { + return 0 +} + +func LeaveLocalContext(ctx *LocalContext, previous uintptr) {} + +func leaveCurrentLocalContext() {} + +func LocalPackage(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { + return AllocZ(size) +} diff --git a/runtime/internal/runtime/local_context_tls.go b/runtime/internal/runtime/local_context_tls.go new file mode 100644 index 0000000000..a06aa1cd93 --- /dev/null +++ b/runtime/internal/runtime/local_context_tls.go @@ -0,0 +1,26 @@ +//go:build llgo && !baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +// currentLocalContext is only an address cache. The context itself is rooted by +// the outer entry frame, so this native TLS slot intentionally has no GC-visible +// pointer type. +// +//llgo:tls +var currentLocalContext uintptr diff --git a/runtime/internal/runtime/local_initializer.go b/runtime/internal/runtime/local_initializer.go new file mode 100644 index 0000000000..58cb8a7fc0 --- /dev/null +++ b/runtime/internal/runtime/local_initializer.go @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "unsafe" + +const ( + localInitUninitialized uint8 = iota + localInitInitializing + localInitReady + localInitFailed +) + +// EnsureLocalInitializer executes one package/locality dispatcher at most once +// in the current owner. Recursive access observes partial initialization; a +// recovered failure remains failed and re-panics on every later access. +func EnsureLocalInitializer(state *uint8, failureKey unsafe.Pointer, initialize func()) { + switch *state { + case localInitReady: + return + case localInitInitializing: + return + case localInitFailed: + panic(*localInitializerFailure(failureKey)) + case localInitUninitialized: + default: + panic("runtime: invalid local initializer state") + } + *state = localInitInitializing + completed := false + defer func() { + if completed { + return + } + value := recover() + *localInitializerFailure(failureKey) = value + *state = localInitFailed + panic(value) + }() + initialize() + completed = true + *state = localInitReady +} + +func localInitializerFailure(key unsafe.Pointer) *any { + var value any + return (*any)(LocalPackage(key, unsafe.Sizeof(value), unsafe.Alignof(value))) +} diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index d0757f9cb1..4764c2484a 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -40,6 +40,7 @@ func Rethrow(link *Defer) { fatal("no goroutines (main called runtime.Goexit) - deadlock!") c.Exit(2) } + leaveCurrentLocalContext() pthread.Exit(nil) } } From 5bda69c3dfc627edbf1446cd64887bb51611979e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jul 2026 19:08:50 +0800 Subject: [PATCH 03/20] compiler: lower TLS and GLS package variables --- cl/compile.go | 40 +- cl/import.go | 89 ++-- cl/import_coverage_test.go | 83 ++- cl/locality.go | 106 ++++ cl/locality_lower.go | 419 +++++++++++++++ cl/locality_lower_test.go | 188 +++++++ cl/locality_test.go | 647 ++++++++++++++++++++++++ cl/static_init.go | 5 + internal/build/build.go | 47 +- internal/build/build_test.go | 129 ++++- internal/build/main_module.go | 8 + internal/build/main_module_test.go | 40 ++ internal/locality/layout/layout.go | 209 ++++++++ internal/locality/layout/layout_test.go | 166 ++++++ ssa/decl.go | 19 +- ssa/goroutine.go | 8 + ssa/goroutine_patch_test.go | 23 + ssa/local_context.go | 38 ++ ssa/locality.go | 199 ++++++++ ssa/locality_test.go | 159 ++++++ ssa/package.go | 17 +- ssa/ssa_test.go | 21 +- ssa/type.go | 5 + 23 files changed, 2615 insertions(+), 50 deletions(-) create mode 100644 cl/locality.go create mode 100644 cl/locality_lower.go create mode 100644 cl/locality_lower_test.go create mode 100644 cl/locality_test.go create mode 100644 internal/locality/layout/layout.go create mode 100644 internal/locality/layout/layout_test.go create mode 100644 ssa/local_context.go create mode 100644 ssa/locality.go create mode 100644 ssa/locality_test.go diff --git a/cl/compile.go b/cl/compile.go index 6cb94df500..e6e44de783 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -210,6 +210,7 @@ type context struct { staticGlobalInits map[*ssa.Global]llssa.Expr staticInitStores map[*ssa.Store]none staticInitInstrs map[ssa.Instruction]none + locality localityLowering } func (p *context) rewriteValue(name string) (string, bool) { @@ -391,7 +392,10 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { return } dbgInstrln("==> NewVar", name, typ) - g := pkg.NewVar(name, typ, llssa.Background(vtype)) + g, skip := p.localityGlobalStorage(pkg, gbl, name, typ, llssa.Background(vtype)) + if skip { + return + } if p.tryEmbedGlobalInit(pkg, gbl, g, name) { return } @@ -601,12 +605,15 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark + oldLocalityFunction := p.locality.function p.fn = fn p.goFn = f p.callerFrameMark = llssa.Nil + p.locality.function = localityFunction{} p.state = state // restore pkgState when compiling funcBody defer func() { p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark + p.locality.function = oldLocalityFunction }() p.phis = nil if dbgSymsEnabled { @@ -624,6 +631,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun bodyPos := p.getFuncBodyPos(f) b.DebugFunction(fn, debugFunctionScope(f), pos, bodyPos) } + p.prepareExportedLocalContext(f) p.bvals = make(map[ssa.Value]llssa.Expr) p.methodNilDerefChecks = collectMethodNilDerefChecks(f) off := make([]int, len(f.Blocks)) @@ -832,6 +840,9 @@ func (p *context) debugParams(b llssa.Builder, f *ssa.Function) { } func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, doModInit bool) llssa.BasicBlock { + oldLocalBlock := p.locality.function.block + p.locality.function.block = block + defer func() { p.locality.function.block = oldLocalBlock }() var last int var pyModInit bool var prog = p.prog @@ -840,6 +851,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do var instrs = block.Instrs[n:] var ret = fn.Block(block.Index) b.SetBlock(ret) + if block.Index == 0 { + p.enterExportedLocalContext(b) + } if block.Index == 0 && p.shouldTrackCallerFrames() { p.pushCallerLocationFrame(b, block.Parent()) } @@ -852,6 +866,7 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do } if doModInit { + p.initializeLocalGuards(b) if p.state != pkgInPatch { p.applyEmbedInits(b) } @@ -1695,6 +1710,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if p.shouldTrackCallerFrames() { p.popCallerLocationFrame(b) } + p.leaveExportedLocalContext(b) b.Return(results...) case *ssa.If: fn := p.fn @@ -1793,7 +1809,7 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { if isCgoVar(varName) { p.cgoSymbols = append(p.cgoSymbols, val.Name()) } - if enableDbgSyms { + if enableDbgSyms && p.localityAllowsGlobalDebug(v) { pos := p.fset.Position(v.Pos()) b.DIGlobal(val, v.Name(), pos) } @@ -2029,6 +2045,15 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri pkg.Pkg = pkgTypes patch.Alt.Pkg = pkgTypes } + if err = ParsePkgSyntax(prog, pkgProg.Fset, pkgTypes, files); err != nil { + return nil, nil, err + } + if err = prog.ValidateLocalities(llssa.PathOf(pkgTypes)); err != nil { + return nil, nil, err + } + if err = validateLocalInitializers(prog, pkgTypes); err != nil { + return nil, nil, err + } if pkgPath == llssa.PkgRuntime { prog.SetRuntime(pkgTypes) } @@ -2130,6 +2155,17 @@ func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) { sort.Slice(members, func(i, j int) bool { return members[i].name < members[j].name }) + localGlobals := make([]*ssa.Global, 0) + for _, m := range members { + global, ok := m.val.(*ssa.Global) + if !ok || isCgoFuncPtrVar(global.Name()) { + continue + } + localGlobals = append(localGlobals, global) + } + // Address accessors and replay guards must exist before any function body + // can reference a local package variable, regardless of member sort order. + ctx.prepareLocalVariables(ret, localGlobals) for _, m := range members { member := m.val diff --git a/cl/import.go b/cl/import.go index eb7a0e2815..5b18496eac 100644 --- a/cl/import.go +++ b/cl/import.go @@ -28,7 +28,9 @@ import ( "golang.org/x/tools/go/ssa" + "github.com/goplus/llgo/internal/directive" "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/locality" llssa "github.com/goplus/llgo/ssa" ) @@ -218,30 +220,6 @@ func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { } } -// PreCollectLinknames scans syntax files before SSA compilation and populates -// prog.Linkname for package-level //go:linkname / //llgo:link declarations. -// It intentionally ignores //export because there is no package export context -// during the pre-collection phase. -func PreCollectLinknames(prog llssa.Program, pkgPath string, files []*ast.File) { - ctx := &context{prog: prog} - for _, file := range files { - for _, decl := range file.Decls { - switch decl := decl.(type) { - case *ast.FuncDecl: - fullName, inPkgName := astFuncName(pkgPath, decl) - ctx.processLinknameByDoc(decl.Doc, fullName, inPkgName, false, false) - case *ast.GenDecl: - if decl.Tok == token.VAR && len(decl.Specs) == 1 { - if names := decl.Specs[0].(*ast.ValueSpec).Names; len(names) == 1 { - inPkgName := names[0].Name - ctx.processLinknameByDoc(decl.Doc, pkgPath+"."+inPkgName, inPkgName, true, false) - } - } - } - } - } -} - // Collect skip names and skip other annotations, such as go: and llgo: // llgo:skip symbol1 symbol2 ... // llgo:skipall @@ -298,6 +276,21 @@ func (p *context) collectSkip(line string, prefix int) { } } +func collectLinknameByDoc(prog llssa.Program, doc *ast.CommentGroup, fullName, inPkgName string) { + directives := directive.ParseGroup(doc) + for n := len(directives) - 1; n >= 0; n-- { + directive := directives[n] + if directive.Name != "go:linkname" && directive.Name != "llgo:link" { + continue + } + fields := strings.Fields(directive.Args) + if len(fields) >= 2 && fields[0] == inPkgName { + prog.SetLinkname(fullName, strings.Join(fields[1:], " ")) + return + } + } +} + func (p *context) processLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgName string, isVar, allowExport bool) bool { if doc != nil { for n := len(doc.List) - 1; n >= 0; n-- { @@ -722,6 +715,9 @@ func (p *context) varOf(b llssa.Builder, v *ssa.Global) llssa.Expr { } panic("unreachable") } + if local, ok := p.localVariableAddress(b, v, name); ok { + return local + } ret := pkg.VarOf(name) if ret == nil { ret = pkg.NewVar(name, p.patchType(v.Type()), llssa.Background(vtype)) @@ -754,24 +750,59 @@ func (p *context) initPyModule() { } } -// ParsePkgSyntax parses AST of a package to check package-level compiler directives. -func ParsePkgSyntax(prog llssa.Program, pkg *types.Package, files []*ast.File) { +// ParsePkgSyntax collects declaration directives in one syntax pass before SSA +// creation. Directives that need an LLVM package (such as //export) are applied +// later by initFiles. +func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File) error { + if pkg == nil { + return nil + } + if prog.PackageSyntaxParsed(pkg) { + return nil + } ctx := &context{prog: prog} pkgPath := llssa.PathOf(pkg) for _, file := range files { for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.FuncDecl: - fullName, _ := astFuncName(pkgPath, decl) + if err := locality.ValidateDoc(fset, decl.Doc); err != nil { + return err + } + if err := locality.ValidateFuncBody(fset, decl.Body); err != nil { + return err + } + fullName, inPkgName := astFuncName(pkgPath, decl) + collectLinknameByDoc(prog, decl.Doc, fullName, inPkgName) ctx.processNoInterfaceByDoc(decl.Doc, fullName) case *ast.GenDecl: - switch decl.Tok { - case token.TYPE: + if decl.Tok == token.VAR { + if len(decl.Specs) == 1 { + if names := decl.Specs[0].(*ast.ValueSpec).Names; len(names) == 1 { + inPkgName := names[0].Name + collectLinknameByDoc(prog, decl.Doc, pkgPath+"."+inPkgName, inPkgName) + } + } + vars, err := locality.ScanPackageVar(fset, decl) + if err != nil { + return err + } + for _, variable := range vars { + prog.SetLocalityInfo(llssa.FullName(pkg, variable.Name), variable.Info) + } + continue + } + if err := locality.ValidateNonPackageVar(fset, decl); err != nil { + return err + } + if decl.Tok == token.TYPE { handleTypeDecl(prog, pkg, decl) } } } } + prog.MarkPackageSyntaxParsed(pkg) + return nil } func handleTypeDecl(prog llssa.Program, pkg *types.Package, decl *ast.GenDecl) { diff --git a/cl/import_coverage_test.go b/cl/import_coverage_test.go index 345693b29c..4afa5b0110 100644 --- a/cl/import_coverage_test.go +++ b/cl/import_coverage_test.go @@ -62,7 +62,9 @@ func (A) StackedHidden() {} } prog := llssa.NewProgram(nil) pkg := types.NewPackage("example.com/p", "p") - ParsePkgSyntax(prog, pkg, []*ast.File{file}) + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } ctx := &context{prog: prog} ctx.processNoInterfaceByDoc(nil, "example.com/p.NilDoc") @@ -70,6 +72,59 @@ func (A) StackedHidden() {} {Text: "// not a directive"}, {Text: "//go:nointerface"}, }}, "example.com/p.NonDirectiveStops") + + if !prog.PackageSyntaxParsed(pkg) { + t.Fatal("package syntax was not marked as parsed") + } + badFile, err := parser.ParseFile(fset, "bad.go", "package p\n//llgo:tls\nfunc Bad() {}\n", parser.ParseComments) + if err != nil { + t.Fatal(err) + } + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{badFile}); err != nil { + t.Fatalf("already parsed package was scanned again: %v", err) + } +} + +func TestParsePkgSyntaxReportsLocalityErrors(t *testing.T) { + prog := llssa.NewProgram(nil) + if err := ParsePkgSyntax(prog, nil, nil, nil); err != nil { + t.Fatal(err) + } + tests := []struct { + name string + src string + want string + }{ + { + name: "function body", + src: "package p\nfunc f() {\n//llgo:gls\nvar value int\n_ = value\n}\n", + want: "package-level var", + }, + { + name: "package var", + src: "package p\n//llgo:tls extra\nvar Value int\n", + want: "does not accept arguments", + }, + { + name: "non-var declaration", + src: "package p\n//llgo:gls\nconst Value = 1\n", + want: "package-level var", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "p.go", test.src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + pkg := types.NewPackage("example.com/"+strings.ReplaceAll(test.name, " ", "-"), "p") + err = ParsePkgSyntax(llssa.NewProgram(nil), fset, pkg, []*ast.File{file}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ParsePkgSyntax error = %v, want %q", err, test.want) + } + }) + } } func TestPkgSymInfoAddSymAndInitLinknamesCoverage(t *testing.T) { @@ -166,13 +221,14 @@ func TestAstAndTypesFuncNameCoverage(t *testing.T) { } } -func TestPreCollectLinknames(t *testing.T) { +func TestParsePkgSyntaxCollectsLinknames(t *testing.T) { cases := []struct { name string directive string want string }{ {name: "go-linkname", directive: "//go:linkname Sigsetjmp C.sigsetjmp", want: "C.sigsetjmp"}, + {name: "go-linkname-tabs", directive: "//go:linkname\tSigsetjmp\tC.sigsetjmp", want: "C.sigsetjmp"}, {name: "llgo-linkname", directive: "//llgo:link Sigsetjmp C.sigsetjmp", want: "C.sigsetjmp"}, {name: "llgo-linkname-spaced", directive: "// llgo:link Sigsetjmp C.sigsetjmp", want: "C.sigsetjmp"}, } @@ -185,12 +241,33 @@ func TestPreCollectLinknames(t *testing.T) { t.Fatalf("ParseFile failed: %v", err) } prog := llssa.NewProgram(nil) - PreCollectLinknames(prog, llssa.PkgRuntime, []*ast.File{file}) + pkg := types.NewPackage(llssa.PkgRuntime, "runtime") + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } if got, ok := prog.Linkname(llssa.PkgRuntime + ".Sigsetjmp"); !ok || got != tt.want { t.Fatalf("pre-collected linkname = (%q,%v), want (%q,%v)", got, ok, tt.want, true) } }) } + prog := llssa.NewProgram(nil) + collectLinknameByDoc(prog, &ast.CommentGroup{List: []*ast.Comment{{Text: "//go:linkname Other C.other"}}}, llssa.PkgRuntime+".Sigsetjmp", "Sigsetjmp") + if _, ok := prog.Linkname(llssa.PkgRuntime + ".Sigsetjmp"); ok { + t.Fatal("mismatched linkname was collected") + } +} + +func TestCollectLinknameByDocIgnoresOtherDirectives(t *testing.T) { + prog := llssa.NewProgram(nil) + doc := &ast.CommentGroup{List: []*ast.Comment{ + {Text: "//go:noinline"}, + {Text: "//llgo:tls"}, + }} + const fullName = "example.com/p.Value" + collectLinknameByDoc(prog, doc, fullName, "Value") + if _, ok := prog.Linkname(fullName); ok { + t.Fatal("non-link directives installed a linkname") + } } func TestBoolToUint8InvalidArgs(t *testing.T) { diff --git a/cl/locality.go b/cl/locality.go new file mode 100644 index 0000000000..0961685f30 --- /dev/null +++ b/cl/locality.go @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cl + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" + llssa "github.com/goplus/llgo/ssa" +) + +// PrepareLocalVariables extracts replayable initializer helpers before Go SSA +// construction, then records the storage strategy selected by the independent +// package-layout planner. +func PrepareLocalVariables(prog llssa.Program, fset *token.FileSet, pkg *types.Package, info *types.Info, files []*ast.File) error { + if pkg == nil || info == nil { + return nil + } + path := llssa.PathOf(pkg) + prepared, err := locality.Prepare(fset, path, pkg, info, files, packageLocalities(prog, path)) + if err != nil { + return err + } + for name, local := range prepared { + prog.SetLocalityInfo(llssa.FullName(pkg, name), local) + } + for fullName, local := range prog.PackageLocalities(path) { + name := strings.TrimPrefix(fullName, path+".") + object, _ := pkg.Scope().Lookup(name).(*types.Var) + if object == nil { + return fmt.Errorf("locality layout: package %s has no variable %s", path, name) + } + canonical, _, _, err := prog.ResolveLocality(fullName) + if err != nil { + return err + } + if canonical != fullName && local.HasInitializer { + return fmt.Errorf("locality layout: linkname alias %s cannot have an initializer", fullName) + } + prog.SetLocalStorage(fullName, localitylayout.StorageForType(object.Type())) + } + _, err = planLocalPackage(prog, pkg) + return err +} + +func validateLocalInitializers(prog llssa.Program, pkg *types.Package) error { + return locality.ValidatePrepared(llssa.PathOf(pkg), packageLocalities(prog, llssa.PathOf(pkg))) +} + +func packageLocalities(prog llssa.Program, pkgPath string) map[string]locality.Info { + prefix := pkgPath + "." + ret := make(map[string]locality.Info) + for name, info := range prog.PackageLocalities(pkgPath) { + ret[strings.TrimPrefix(name, prefix)] = info.Info + } + return ret +} + +func planLocalPackage(prog llssa.Program, pkg *types.Package) (localitylayout.Package, error) { + if pkg == nil { + return localitylayout.Package{}, nil + } + path := llssa.PathOf(pkg) + prefix := path + "." + decls := prog.PackageLocalities(path) + input := make([]localitylayout.Declaration, 0, len(decls)) + for fullName := range decls { + canonical, info, _, err := prog.ResolveLocality(fullName) + if err != nil { + return localitylayout.Package{}, err + } + if canonical != fullName { + target, targetOK := prog.VariableLocality(canonical) + if !targetOK || target.Locality == locality.None { + return localitylayout.Package{}, fmt.Errorf("locality layout: linkname target %s for %s is not a local variable", canonical, fullName) + } + continue + } + name := strings.TrimPrefix(fullName, prefix) + object, _ := pkg.Scope().Lookup(name).(*types.Var) + if object == nil { + return localitylayout.Package{}, fmt.Errorf("locality layout: package %s has no variable %s", path, name) + } + input = append(input, localitylayout.Declaration{Name: fullName, Type: object.Type(), Info: info.Info}) + } + return localitylayout.Plan(path, input) +} diff --git a/cl/locality_lower.go b/cl/locality_lower.go new file mode 100644 index 0000000000..10b6cd4cee --- /dev/null +++ b/cl/locality_lower.go @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cl + +import ( + "fmt" + "go/token" + "go/types" + "strings" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const localInitReady = 2 + +type localVariable struct { + planned localitylayout.Variable + owner *localPackage +} + +type localPackage struct { + plan localitylayout.Package + typ llssa.Type + blockFunc llssa.Function + direct map[string]llssa.Global + init map[locality.Kind]*localInitializer +} + +type localInitializer struct { + guard llssa.Global + failureKey llssa.Global + dispatch llssa.Function + ensure llssa.Function +} + +type localBaseCacheKey struct { + block *ssa.BasicBlock + owner *localPackage +} + +type localEnsureCacheKey struct { + block *ssa.BasicBlock + owner *localPackage + kind locality.Kind +} + +// localityLowering owns all compiler state for TLS/GLS lowering. Only this +// value is embedded in the general compiler context. +type localityLowering struct { + packages map[string]*localPackage + variables map[*ssa.Global]*localVariable + function localityFunction +} + +type localityFunction struct { + block *ssa.BasicBlock + packageBases map[localBaseCacheKey]llssa.Expr + packageEnsures map[localEnsureCacheKey]bool + entry *localEntryContext +} + +type localEntryContext struct { + context llssa.Expr + previous llssa.Expr + entered bool +} + +func (p *context) prepareExportedLocalContext(f *ssa.Function) { + if !p.prog.NeedsLocalContext() || f == nil || f.Pkg == nil { + return + } + fullName := funcName(f.Pkg.Pkg, f, false) + if _, exported := p.pkg.ExportFuncs()[fullName]; !exported { + return + } + p.locality.function.entry = &localEntryContext{} +} + +func (p *context) enterExportedLocalContext(b llssa.Builder) { + entry := p.locality.function.entry + if entry == nil || entry.entered { + return + } + context, previous := b.EnterLocalContext() + entry.context = context + entry.previous = previous + entry.entered = true +} + +func (p *context) leaveExportedLocalContext(b llssa.Builder) { + entry := p.locality.function.entry + if entry != nil && entry.entered { + b.LeaveLocalContext(entry.context, entry.previous) + } +} + +func (p *context) prepareLocalVariables(pkg llssa.Package, globals []*ssa.Global) { + _, err := p.localPackageFor(p.goTyps, pkg, true) + if err != nil { + panic(err) + } + if p.locality.variables == nil { + p.locality.variables = make(map[*ssa.Global]*localVariable) + } + for _, global := range globals { + variable, ok, err := p.localVariableFor(pkg, global, true) + if err != nil { + panic(err) + } + if !ok { + continue + } + p.locality.variables[global] = variable + } +} + +// localVariableFor resolves one Go SSA global to the canonical package plan. +// Both local definitions and imported references use this path so linkname and +// layout validation cannot diverge between the two lowering cases. +func (p *context) localVariableFor(pkg llssa.Package, global *ssa.Global, defineCurrent bool) (*localVariable, bool, error) { + fullName := llssa.FullName(global.Pkg.Pkg, global.Name()) + canonical, info, ok, err := p.prog.ResolveLocality(fullName) + if err != nil { + return nil, false, err + } + if !ok || info.Locality == locality.None { + return nil, false, nil + } + typesPkg := p.localTypesPackage(canonical) + if typesPkg == nil { + return nil, false, fmt.Errorf("missing types package for local variable %s", canonical) + } + owner, err := p.localPackageFor(typesPkg, pkg, defineCurrent && typesPkg == p.goTyps) + if err != nil { + return nil, false, err + } + planned, ok := owner.plan.Lookup(canonical) + if !ok { + return nil, false, fmt.Errorf("missing locality layout for %s", canonical) + } + return &localVariable{planned: planned, owner: owner}, true, nil +} + +func (p *context) localityGlobalStorage(pkg llssa.Package, global *ssa.Global, name string, typ types.Type, bg llssa.Background) (llssa.Global, bool) { + info, ok := p.resolveLocality(llssa.FullName(global.Pkg.Pkg, global.Name())) + if !ok || info.Locality == locality.None { + return pkg.NewVar(name, typ, bg), false + } + variable := p.locality.variables[global] + if variable == nil { + panic(fmt.Sprintf("missing locality layout for %s", name)) + } + if variable.planned.Storage == localitylayout.StoragePackage { + return nil, true + } + return variable.owner.direct[variable.planned.Name], false +} + +func (p *context) localityAllowsGlobalDebug(global *ssa.Global) bool { + variable := p.locality.variables[global] + return variable == nil || variable.planned.Storage == localitylayout.StorageNativeTLS +} + +func (p *context) localTypesPackage(fullName string) *types.Package { + matches := func(pkg *types.Package) bool { + if pkg == nil { + return false + } + prefix := llssa.PathOf(pkg) + "." + if !strings.HasPrefix(fullName, prefix) { + return false + } + name := strings.TrimPrefix(fullName, prefix) + _, ok := pkg.Scope().Lookup(name).(*types.Var) + return ok + } + if matches(p.goTyps) { + return p.goTyps + } + if p.goProg != nil { + for _, pkg := range p.goProg.AllPackages() { + if pkg != nil && matches(pkg.Pkg) { + return pkg.Pkg + } + } + } + for pkg := range p.loaded { + if matches(pkg) { + return pkg + } + } + return nil +} + +func (p *context) localPackageFor(typesPkg *types.Package, pkg llssa.Package, define bool) (*localPackage, error) { + if typesPkg == nil { + return nil, nil + } + path := llssa.PathOf(typesPkg) + if owner := p.locality.packages[path]; owner != nil { + return owner, nil + } + plan, err := planLocalPackage(p.prog, typesPkg) + if err != nil { + return nil, err + } + if len(plan.Variables) == 0 { + return nil, nil + } + if p.locality.packages == nil { + p.locality.packages = make(map[string]*localPackage) + } + owner := &localPackage{ + plan: plan, + direct: make(map[string]llssa.Global), + init: make(map[locality.Kind]*localInitializer), + } + p.locality.packages[path] = owner + p.buildLocalPackage(pkg, owner, define) + return owner, nil +} + +func (p *context) buildLocalPackage(pkg llssa.Package, owner *localPackage, define bool) { + for _, variable := range owner.plan.Variables { + if variable.Storage != localitylayout.StorageNativeTLS { + continue + } + typ := types.NewPointer(p.patchType(variable.Type)) + global := pkg.NewThreadLocalVar(variable.Name, typ, llssa.InGo) + owner.direct[variable.Name] = global + } + if len(owner.plan.Block) != 0 { + fields := make([]*types.Var, len(owner.plan.Block)) + for index, variable := range owner.plan.Block { + fields[index] = types.NewField(token.NoPos, nil, fmt.Sprintf("v%d", index), p.patchType(variable.Type), false) + } + structType := types.NewStruct(fields, nil) + owner.typ = p.prog.Type(structType, llssa.InGo) + key := pkg.NewVar(localitylayout.BlockKeyName(owner.plan.Path), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) + if define { + key.InitNil() + } + result := types.NewPointer(structType) + owner.blockFunc = pkg.NewFunc(localitylayout.BlockName(owner.plan.Path), noArgResultSignature(result), llssa.InGo) + owner.blockFunc.Inline(llssa.AlwaysInline) + if define && !owner.blockFunc.HasBody() { + b := owner.blockFunc.MakeBody(1) + raw := b.Call( + pkg.RuntimeFunc("LocalPackage"), + b.Convert(p.prog.VoidPtr(), key.Expr), + p.prog.IntVal(p.prog.SizeOf(owner.typ), p.prog.Uintptr()), + p.prog.IntVal(p.prog.AlignOf(owner.typ), p.prog.Uintptr()), + ) + b.Return(b.Convert(p.prog.Pointer(owner.typ), raw)) + b.EndBuild() + } + } + for _, kind := range []locality.Kind{locality.Thread, locality.Goroutine} { + initializers := owner.plan.Initializers(kind) + if len(initializers) == 0 { + continue + } + owner.init[kind] = p.buildLocalInitializer(pkg, owner, kind, initializers, define) + } +} + +func (p *context) buildLocalInitializer(pkg llssa.Package, owner *localPackage, kind locality.Kind, initializers []localitylayout.Initializer, define bool) *localInitializer { + ret := &localInitializer{} + ret.guard = pkg.NewThreadLocalVar(localitylayout.GuardName(owner.plan.Path, kind), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) + ret.failureKey = pkg.NewVar(localitylayout.FailureKeyName(owner.plan.Path, kind), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) + ret.dispatch = pkg.NewFunc(localitylayout.InitName(owner.plan.Path, kind), llssa.NoArgsNoRet, llssa.InGo) + ret.ensure = pkg.NewFunc(localitylayout.EnsureName(owner.plan.Path, kind), llssa.NoArgsNoRet, llssa.InGo) + ret.ensure.Inline(llssa.AlwaysInline) + if !define { + return ret + } + ret.guard.InitNil() + ret.failureKey.InitNil() + if !ret.dispatch.HasBody() { + b := ret.dispatch.MakeBody(1) + for _, initializer := range initializers { + helper := pkg.NewFunc(initializer.Name, llssa.NoArgsNoRet, llssa.InGo) + b.Call(helper.Expr) + } + b.Return() + b.EndBuild() + } + if !ret.ensure.HasBody() { + b := ret.ensure.MakeBody(3) + ready := b.BinOp(token.EQL, b.Load(ret.guard.Expr), p.prog.IntVal(localInitReady, p.prog.Byte())) + b.If(ready, ret.ensure.Block(2), ret.ensure.Block(1)) + b.SetBlock(ret.ensure.Block(1)) + closure := b.MakeClosure(ret.dispatch.Expr, nil) + b.Call( + pkg.RuntimeFunc("EnsureLocalInitializer"), + ret.guard.Expr, + b.Convert(p.prog.VoidPtr(), ret.failureKey.Expr), + closure, + ) + b.Jump(ret.ensure.Block(2)) + b.SetBlock(ret.ensure.Block(2)) + b.Return() + b.EndBuild() + } + return ret +} + +func noArgResultSignature(result types.Type) *types.Signature { + results := types.NewTuple(types.NewVar(token.NoPos, nil, "", result)) + return types.NewSignatureType(nil, nil, nil, nil, results, false) +} + +func (p *context) localVariableAddr(b llssa.Builder, v *ssa.Global, info llssa.VariableLocality, name string) llssa.Expr { + variable := p.locality.variables[v] + if variable == nil { + var ok bool + var err error + variable, ok, err = p.localVariableFor(p.pkg, v, false) + if err != nil { + panic(err) + } + if !ok { + panic(fmt.Sprintf("missing locality metadata for %s", name)) + } + p.locality.variables[v] = variable + } + p.ensureLocalInitializer(b, variable.owner, info.Locality) + if variable.planned.Storage == localitylayout.StorageNativeTLS { + direct := variable.owner.direct[variable.planned.Name] + if direct == nil { + panic(fmt.Sprintf("missing native TLS storage for %s", name)) + } + return direct.Expr + } + base := p.localPackageBase(b, variable.owner) + return b.FieldAddr(base, variable.planned.Field) +} + +func (p *context) localVariableAddress(b llssa.Builder, variable *ssa.Global, name string) (llssa.Expr, bool) { + info, ok := p.resolveLocality(llssa.FullName(variable.Pkg.Pkg, variable.Name())) + if !ok || info.Locality == locality.None { + return llssa.Expr{}, false + } + return p.localVariableAddr(b, variable, info, name), true +} + +func (p *context) resolveLocality(name string) (llssa.VariableLocality, bool) { + _, info, ok, err := p.prog.ResolveLocality(name) + if err != nil { + panic(err) + } + return info, ok +} + +func (p *context) localPackageBase(b llssa.Builder, owner *localPackage) llssa.Expr { + state := &p.locality.function + for block := state.block; block != nil; block = block.Idom() { + if base, ok := state.packageBases[localBaseCacheKey{block: block, owner: owner}]; ok { + return base + } + } + base := b.Call(owner.blockFunc.Expr) + if state.block != nil { + if state.packageBases == nil { + state.packageBases = make(map[localBaseCacheKey]llssa.Expr) + } + state.packageBases[localBaseCacheKey{block: state.block, owner: owner}] = base + } + return base +} + +func (p *context) ensureLocalInitializer(b llssa.Builder, owner *localPackage, kind locality.Kind) { + initializer := owner.init[kind] + if initializer == nil { + return + } + state := &p.locality.function + for block := state.block; block != nil; block = block.Idom() { + if state.packageEnsures[localEnsureCacheKey{block: block, owner: owner, kind: kind}] { + return + } + } + b.Call(initializer.ensure.Expr) + if state.block != nil { + if state.packageEnsures == nil { + state.packageEnsures = make(map[localEnsureCacheKey]bool) + } + state.packageEnsures[localEnsureCacheKey{block: state.block, owner: owner, kind: kind}] = true + } +} + +func (p *context) initializeLocalGuards(b llssa.Builder) { + owner := p.locality.packages[llssa.PathOf(p.goTyps)] + if owner == nil { + return + } + for _, kind := range []locality.Kind{locality.Thread, locality.Goroutine} { + if initializer := owner.init[kind]; initializer != nil { + b.Store(initializer.guard.Expr, p.prog.IntVal(localInitReady, p.prog.Byte())) + } + } +} diff --git a/cl/locality_lower_test.go b/cl/locality_lower_test.go new file mode 100644 index 0000000000..7610d70c13 --- /dev/null +++ b/cl/locality_lower_test.go @@ -0,0 +1,188 @@ +package cl + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" + llssa "github.com/goplus/llgo/ssa" + "github.com/goplus/llgo/ssa/ssatest" + "golang.org/x/tools/go/ssa" +) + +func localitySSAGlobal(t *testing.T, path string) (*types.Package, *ssa.Global) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", "package locality\nvar Value int\n", 0) + if err != nil { + t.Fatal(err) + } + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check(path, fset, []*ast.File{file}, info) + if err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, []*ast.File{file}, info, true) + global, ok := ssaPkg.Members["Value"].(*ssa.Global) + if !ok { + t.Fatalf("Value SSA member = %T", ssaPkg.Members["Value"]) + } + return pkg, global +} + +func TestLocalityLoweringResolution(t *testing.T) { + typesPkg, global := localitySSAGlobal(t, "example.com/lowering") + prog := ssatest.NewProgram(t, nil) + llvmPkg := prog.NewPackage(typesPkg.Name(), typesPkg.Path()) + ctx := &context{prog: prog, pkg: llvmPkg, goTyps: typesPkg} + + if variable, ok, err := ctx.localVariableFor(llvmPkg, global, true); err != nil || ok || variable != nil { + t.Fatalf("ordinary localVariableFor = %+v, %v, %v", variable, ok, err) + } + name := llssa.FullName(typesPkg, global.Name()) + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLocalStorage(name, llssa.LocalStorageNativeTLS) + variable, ok, err := ctx.localVariableFor(llvmPkg, global, true) + if err != nil || !ok || variable.planned.Storage != localitylayout.StorageNativeTLS { + t.Fatalf("local localVariableFor = %+v, %v, %v", variable, ok, err) + } + ctx.locality.variables = map[*ssa.Global]*localVariable{global: variable} + if !ctx.localityAllowsGlobalDebug(global) { + t.Fatal("native TLS was hidden from global debug info") + } + variable.planned.Storage = localitylayout.StoragePackage + if ctx.localityAllowsGlobalDebug(global) { + t.Fatal("package storage was exposed as a fixed debug global") + } + if !ctx.localityAllowsGlobalDebug(new(ssa.Global)) { + t.Fatal("ordinary global was hidden from debug info") + } + if owner, err := ctx.localPackageFor(typesPkg, llvmPkg, true); err != nil || owner != variable.owner { + t.Fatalf("cached localPackageFor = %p, %v; want %p", owner, err, variable.owner) + } + + loaded := types.NewPackage("example.com/loaded", "loaded") + loaded.Scope().Insert(types.NewVar(token.NoPos, loaded, "Value", types.Typ[types.Int])) + ctx.loaded = map[*types.Package]*pkgInfo{loaded: {kind: PkgDeclOnly}} + if got := ctx.localTypesPackage("example.com/loaded.Value"); got != loaded { + t.Fatalf("loaded localTypesPackage = %v, want %v", got, loaded) + } + if got := (&context{}).localTypesPackage("example.com/missing.Value"); got != nil { + t.Fatalf("missing localTypesPackage = %v", got) + } + if owner, err := ctx.localPackageFor(nil, llvmPkg, false); err != nil || owner != nil { + t.Fatalf("nil localPackageFor = %v, %v", owner, err) + } + empty := types.NewPackage("example.com/empty", "empty") + if owner, err := ctx.localPackageFor(empty, llvmPkg, false); err != nil || owner != nil { + t.Fatalf("empty localPackageFor = %v, %v", owner, err) + } +} + +func TestLocalityLoweringDiagnostics(t *testing.T) { + typesPkg, global := localitySSAGlobal(t, "example.com/diagnostic") + name := llssa.FullName(typesPkg, global.Name()) + newProgram := func() llssa.Program { + prog := ssatest.NewProgram(t, nil) + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + return prog + } + + t.Run("missing types package", func(t *testing.T) { + ctx := &context{prog: newProgram()} + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "missing types package") { + t.Fatalf("localVariableFor error = %v", err) + } + }) + + t.Run("invalid plan", func(t *testing.T) { + prog := newProgram() + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + ctx := &context{prog: prog, goTyps: typesPkg} + if _, err := ctx.localPackageFor(typesPkg, nil, false); err == nil || !strings.Contains(err.Error(), "inconsistent initializer metadata") { + t.Fatalf("localPackageFor error = %v", err) + } + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "inconsistent initializer metadata") { + t.Fatalf("localVariableFor error = %v", err) + } + }) + + t.Run("stale plan", func(t *testing.T) { + prog := newProgram() + ctx := &context{prog: prog, goTyps: typesPkg} + ctx.locality.packages = map[string]*localPackage{ + typesPkg.Path(): {plan: localitylayout.Package{Path: typesPkg.Path()}}, + } + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "missing locality layout") { + t.Fatalf("localVariableFor error = %v", err) + } + }) + + t.Run("linkname cycle", func(t *testing.T) { + prog := newProgram() + other := typesPkg.Path() + ".Other" + prog.SetLinkname(name, other) + prog.SetLinkname(other, name) + ctx := &context{prog: prog, goTyps: typesPkg} + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "linkname cycle") { + t.Fatalf("localVariableFor error = %v", err) + } + assertLocalityPanic(t, "resolveLocality", func() { ctx.resolveLocality(name) }) + assertLocalityPanic(t, "prepareLocalVariables", func() { ctx.prepareLocalVariables(nil, nil) }) + assertLocalityPanic(t, "localVariableAddr", func() { + ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + }) + }) + + t.Run("imported metadata error", func(t *testing.T) { + current := types.NewPackage("example.com/current", "current") + prog := newProgram() + other := typesPkg.Path() + ".Other" + prog.SetLinkname(name, other) + prog.SetLinkname(other, name) + ctx := &context{prog: prog, goTyps: current} + assertLocalityPanic(t, "prepare imported local", func() { ctx.prepareLocalVariables(nil, []*ssa.Global{global}) }) + }) + + t.Run("missing prepared layout", func(t *testing.T) { + ctx := &context{prog: newProgram()} + assertLocalityPanic(t, "localityGlobalStorage", func() { + ctx.localityGlobalStorage(nil, global, name, global.Type(), llssa.InGo) + }) + }) + + t.Run("missing address metadata", func(t *testing.T) { + ctx := &context{prog: ssatest.NewProgram(t, nil)} + assertLocalityPanic(t, "localVariableAddr", func() { + ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + }) + }) + + t.Run("missing native storage", func(t *testing.T) { + ctx := &context{locality: localityLowering{variables: map[*ssa.Global]*localVariable{ + global: { + planned: localitylayout.Variable{Declaration: localitylayout.Declaration{Name: name}, Storage: localitylayout.StorageNativeTLS}, + owner: &localPackage{direct: map[string]llssa.Global{}, init: map[locality.Kind]*localInitializer{}}, + }, + }}} + assertLocalityPanic(t, "localVariableAddr", func() { + ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + }) + }) +} + +func assertLocalityPanic(t *testing.T, name string, fn func()) { + t.Helper() + defer func() { + if recover() == nil { + t.Fatalf("%s did not panic", name) + } + }() + fn() +} diff --git a/cl/locality_test.go b/cl/locality_test.go new file mode 100644 index 0000000000..081e8b35e7 --- /dev/null +++ b/cl/locality_test.go @@ -0,0 +1,647 @@ +package cl + +import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "runtime" + "strings" + "testing" + + llssa "github.com/goplus/llgo/ssa" + "github.com/goplus/llgo/ssa/ssatest" + "golang.org/x/tools/go/ssa" +) + +func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + imp := importer.Default() + pkg, err := (&types.Config{Importer: imp}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgramEx(t, nil, imp) + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + prog.SetRuntime(localityRuntimePackage()) + if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, fset, pkg, info, files); err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, files, info, true) + ssaPkg.Build() + compiled, err := NewPackage(prog, ssaPkg, files) + if err != nil { + t.Fatal(err) + } + return prog, compiled.String() +} + +func newLocalityTypeInfo() *types.Info { + return &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } +} + +func localityRuntimePackage() *types.Package { + pkg := types.NewPackage(llssa.PkgRuntime, "runtime") + localContextName := types.NewTypeName(token.NoPos, pkg, "LocalContext", nil) + localContext := types.NewNamed(localContextName, types.NewStruct(nil, nil), nil) + pkg.Scope().Insert(localContextName) + + localPackageParams := types.NewTuple( + types.NewParam(token.NoPos, pkg, "key", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, pkg, "size", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, pkg, "align", types.Typ[types.Uintptr]), + ) + localPackageResults := types.NewTuple(types.NewParam(token.NoPos, pkg, "", types.Typ[types.UnsafePointer])) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "LocalPackage", types.NewSignatureType(nil, nil, nil, localPackageParams, localPackageResults, false))) + + callback := types.NewSignatureType(nil, nil, nil, nil, nil, false) + ensureParams := types.NewTuple( + types.NewParam(token.NoPos, pkg, "state", types.NewPointer(types.Typ[types.Uint8])), + types.NewParam(token.NoPos, pkg, "failureKey", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, pkg, "initialize", callback), + ) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "EnsureLocalInitializer", types.NewSignatureType(nil, nil, nil, ensureParams, nil, false))) + + contextPointer := types.NewPointer(localContext) + enterParams := types.NewTuple(types.NewParam(token.NoPos, pkg, "ctx", contextPointer)) + enterResults := types.NewTuple(types.NewParam(token.NoPos, pkg, "previous", types.Typ[types.Uintptr])) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "EnterLocalContext", types.NewSignatureType(nil, nil, nil, enterParams, enterResults, false))) + leaveParams := types.NewTuple( + types.NewParam(token.NoPos, pkg, "ctx", contextPointer), + types.NewParam(token.NoPos, pkg, "previous", types.Typ[types.Uintptr]), + ) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "LeaveLocalContext", types.NewSignatureType(nil, nil, nil, leaveParams, nil, false))) + return pkg +} + +func llvmFunction(t *testing.T, ir, name string) string { + t.Helper() + markerAt := strings.Index(ir, `@"`+name+`"(`) + if markerAt < 0 { + markerAt = strings.Index(ir, `@`+name+`(`) + } + if markerAt < 0 { + t.Fatalf("function %s not found:\n%s", name, ir) + } + start := strings.LastIndex(ir[:markerAt], "define ") + if start < 0 { + t.Fatalf("definition for %s not found", name) + } + end := strings.Index(ir[markerAt:], "\n}") + if end < 0 { + t.Fatalf("end of %s not found", name) + } + return ir[start : markerAt+end+2] +} + +func TestLocalityPlansNativeTLSAndSharedPointerBlock(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality + +var backing int +func scalar() int { return 42 } +func pointer() *int { return &backing } + +//llgo:tls +var TLSScalar = scalar() +//llgo:tls +var TLSPointer = pointer() +//llgo:gls +var GLSScalar = scalar() +//llgo:gls +var GLSPointer = pointer() + +func values() (int, *int, int, *int) { + return TLSScalar, TLSPointer, GLSScalar, GLSPointer +} +`) + + checks := map[string]struct { + kind llssa.Locality + storage llssa.LocalStorage + }{ + "TLSScalar": {llssa.ThreadLocal, llssa.LocalStorageNativeTLS}, + "TLSPointer": {llssa.ThreadLocal, llssa.LocalStoragePackage}, + "GLSScalar": {llssa.GoroutineLocal, llssa.LocalStorageNativeTLS}, + "GLSPointer": {llssa.GoroutineLocal, llssa.LocalStoragePackage}, + } + for name, want := range checks { + got, ok := prog.VariableLocality("example.com/locality." + name) + if !ok || got.Locality != want.kind || got.LocalStorage != want.storage { + t.Fatalf("%s metadata = %+v, %v", name, got, ok) + } + } + for _, name := range []string{"TLSScalar", "GLSScalar"} { + if !strings.Contains(ir, `@"example.com/locality.`+name+`" = thread_local global i64`) { + t.Fatalf("%s is not native TLS:\n%s", name, ir) + } + } + for _, name := range []string{"TLSPointer", "GLSPointer"} { + if strings.Contains(ir, `@"example.com/locality.`+name+`" = thread_local`) { + t.Fatalf("%s retained a pointer-bearing TLS global:\n%s", name, ir) + } + } + if got := strings.Count(ir, `@"example.com/locality.__llgo_local_key" =`); got != 1 { + t.Fatalf("package block keys = %d, want 1:\n%s", got, ir) + } + if got := strings.Count(ir, `call ptr @"github.com/goplus/llgo/runtime/internal/runtime.LocalPackage"`); got != 1 { + t.Fatalf("LocalPackage calls = %d, want one accessor definition:\n%s", got, ir) + } + values := llvmFunction(t, ir, "example.com/locality.values") + if got := strings.Count(values, `call ptr @"example.com/locality.__llgo_local_block"()`); got != 1 { + t.Fatalf("values package-base calls = %d, want 1:\n%s", got, values) + } + if got := strings.Count(values, `call void @"example.com/locality.__llgo_tls_init$ensure"()`); got != 1 { + t.Fatalf("values TLS ensure calls = %d, want 1:\n%s", got, values) + } + if got := strings.Count(values, `call void @"example.com/locality.__llgo_gls_init$ensure"()`); got != 1 { + t.Fatalf("values GLS ensure calls = %d, want 1:\n%s", got, values) + } + if !prog.NeedsLocalContext() { + t.Fatal("pointer-bearing local variables did not enable a local context") + } +} + +func TestLocalityInitializersPreserveGoOrderPerKind(t *testing.T) { + _, ir := compileLocalitySource(t, `package locality + +func mark(value int) int { return value } +//llgo:tls +var T0 = mark(0) +//llgo:gls +var G0 = mark(1) +//llgo:tls +var T1 = mark(2) +func values() (int, int, int) { return T0, T1, G0 } +`) + tls := llvmFunction(t, ir, "example.com/locality.__llgo_tls_init") + gls := llvmFunction(t, ir, "example.com/locality.__llgo_gls_init") + first := strings.Index(tls, `__llgo_local_init_0`) + second := strings.Index(tls, `__llgo_local_init_2`) + if first < 0 || second < first || strings.Contains(tls, `__llgo_local_init_1`) { + t.Fatalf("TLS dispatcher order is wrong:\n%s", tls) + } + if !strings.Contains(gls, `__llgo_local_init_1`) || strings.Contains(gls, `__llgo_local_init_0`) || strings.Contains(gls, `__llgo_local_init_2`) { + t.Fatalf("GLS dispatcher contains the wrong helpers:\n%s", gls) + } + initBody := llvmFunction(t, ir, "example.com/locality.init") + for _, guard := range []string{"__llgo_tls_init$guard", "__llgo_gls_init$guard"} { + if !strings.Contains(initBody, `store i8 2, ptr @"example.com/locality.`+guard+`"`) { + t.Fatalf("package init does not mark %s ready:\n%s", guard, initBody) + } + } +} + +func TestDirectInitializerStillRequiresFailureContext(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +func value() int { return 1 } +//llgo:tls +var Value = value() +func get() int { return Value } +`) + if !prog.NeedsLocalContext() { + t.Fatal("initializer failure storage did not enable a local context") + } + if strings.Contains(ir, `__llgo_local_block`) { + t.Fatalf("pointer-free package unexpectedly has a value block:\n%s", ir) + } + if !strings.Contains(ir, `@"example.com/locality.Value" = thread_local global i64`) || !strings.Contains(ir, `EnsureLocalInitializer`) { + t.Fatalf("direct initializer lowering is incomplete:\n%s", ir) + } +} + +func TestZeroValueDirectLocalsNeedNoContext(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +//llgo:tls +var T int +//llgo:gls +var G uintptr +func values() (int, uintptr) { return T, G } +`) + if prog.NeedsLocalContext() { + t.Fatal("pointer-free zero-value locals enabled a local context") + } + if strings.Contains(ir, `LocalPackage`) || strings.Contains(ir, `EnsureLocalInitializer`) { + t.Fatalf("zero-value direct locals emitted cold-path support:\n%s", ir) + } +} + +func TestExportedFunctionInstallsLocalContext(t *testing.T) { + _, ir := compileLocalitySource(t, `package locality +//llgo:gls +var Pointer *int +//export Exported +func Exported(useLocal bool) *int { + if useLocal { return Pointer } + return nil +} +`) + exported := llvmFunction(t, ir, "Exported") + if got := strings.Count(exported, "EnterLocalContext"); got != 1 { + t.Fatalf("exported function context entries = %d, want 1:\n%s", got, exported) + } + if got := strings.Count(exported, "LeaveLocalContext"); got != 2 { + t.Fatalf("exported function context leaves = %d, want 2:\n%s", got, exported) + } + assertTextOrder(t, exported, + "EnterLocalContext", + "__llgo_local_block", + "LeaveLocalContext", + "ret ptr", + ) +} + +func TestExportedNativeTLSNeedsNoLocalContext(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +//llgo:tls +var Scalar int +//export Exported +func Exported() int { return Scalar } +`) + if prog.NeedsLocalContext() { + t.Fatal("zero-value native TLS enabled a local context") + } + exported := llvmFunction(t, ir, "Exported") + if strings.Contains(exported, "LocalContext") { + t.Fatalf("native-TLS-only export installed a local context:\n%s", exported) + } +} + +func assertTextOrder(t *testing.T, text string, wants ...string) { + t.Helper() + offset := 0 + for _, want := range wants { + index := strings.Index(text[offset:], want) + if index < 0 { + t.Fatalf("%q not found after offset %d:\n%s", want, offset, text) + } + offset += index + len(want) + } +} + +func TestLocalityLinknameAliasesReuseCanonicalStorage(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality + +//llgo:gls +var Pointer *int +//go:linkname PointerAlias example.com/locality.Pointer +//llgo:gls +var PointerAlias *int + +//llgo:tls +var Scalar int +//go:linkname ScalarAlias example.com/locality.Scalar +//llgo:tls +var ScalarAlias int + +func values() (*int, *int, int, int) { return Pointer, PointerAlias, Scalar, ScalarAlias } +`) + if strings.Contains(ir, "PointerAlias") || strings.Contains(ir, "ScalarAlias") { + t.Fatalf("linkname aliases received independent LLVM storage:\n%s", ir) + } + if got := strings.Count(ir, `@"example.com/locality.Scalar" = thread_local global i64`); got != 1 { + t.Fatalf("canonical scalar globals = %d, want 1:\n%s", got, ir) + } + values := llvmFunction(t, ir, "example.com/locality.values") + if got := strings.Count(values, `call ptr @"example.com/locality.__llgo_local_block"()`); got != 1 { + t.Fatalf("alias package-base calls = %d, want 1:\n%s", got, values) + } + for name, want := range map[string]llssa.LocalStorage{ + "example.com/locality.PointerAlias": llssa.LocalStoragePackage, + "example.com/locality.ScalarAlias": llssa.LocalStorageNativeTLS, + } { + if got, ok := prog.VariableLocality(name); !ok || got.LocalStorage != want { + t.Fatalf("alias metadata %s = %+v, %v; want storage %v", name, got, ok, want) + } + } +} + +func TestLocalityCrossPackageAccessUsesDependencyStorage(t *testing.T) { + fset := token.NewFileSet() + parse := func(name, source string) *ast.File { + file, err := parser.ParseFile(fset, name, source, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + return file + } + depFile := parse("dep.go", `package dep +func initialScalar() int { return 1 } +//llgo:tls +var Scalar = initialScalar() +//llgo:gls +var Pointer *int +`) + rootFile := parse("root.go", `package root +import "example.com/dep" +func Values() (int, *int) { return dep.Scalar, dep.Pointer } +`) + check := func(path string, files []*ast.File, imp types.Importer) (*types.Package, *types.Info) { + info := newLocalityTypeInfo() + pkg, err := (&types.Config{Importer: imp}).Check(path, fset, files, info) + if err != nil { + t.Fatal(err) + } + return pkg, info + } + depPkg, depInfo := check("example.com/dep", []*ast.File{depFile}, nil) + rootPkg, rootInfo := check("example.com/root", []*ast.File{rootFile}, importerFunc(func(path string) (*types.Package, error) { + if path == depPkg.Path() { + return depPkg, nil + } + return nil, types.Error{Msg: "unexpected import " + path} + })) + + prog := ssatest.NewProgram(t, nil) + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + prog.SetRuntime(localityRuntimePackage()) + for _, input := range []struct { + pkg *types.Package + info *types.Info + files []*ast.File + }{ + {depPkg, depInfo, []*ast.File{depFile}}, + {rootPkg, rootInfo, []*ast.File{rootFile}}, + } { + if err := ParsePkgSyntax(prog, fset, input.pkg, input.files); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, fset, input.pkg, input.info, input.files); err != nil { + t.Fatal(err) + } + } + + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + depSSA := goProg.CreatePackage(depPkg, []*ast.File{depFile}, depInfo, true) + rootSSA := goProg.CreatePackage(rootPkg, []*ast.File{rootFile}, rootInfo, true) + goProg.Build() + if _, err := NewPackage(prog, depSSA, []*ast.File{depFile}); err != nil { + t.Fatal(err) + } + root, err := NewPackage(prog, rootSSA, []*ast.File{rootFile}) + if err != nil { + t.Fatal(err) + } + ir := root.String() + if !strings.Contains(ir, `@"example.com/dep.Scalar" = external thread_local global i64`) { + t.Fatalf("root package did not reference dependency TLS storage:\n%s", ir) + } + if !strings.Contains(ir, `declare ptr @"example.com/dep.__llgo_local_block"()`) { + t.Fatalf("root package did not reference dependency block accessor:\n%s", ir) + } + if !strings.Contains(ir, `declare void @"example.com/dep.__llgo_tls_init$ensure"()`) { + t.Fatalf("root package did not reference dependency initializer guard:\n%s", ir) + } + if strings.Contains(ir, `define ptr @"example.com/dep.__llgo_local_block"()`) { + t.Fatalf("root package redefined dependency block accessor:\n%s", ir) + } +} + +func TestPrepareRejectsLocalAliasInitializer(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality +//llgo:tls +var Target int +//go:linkname Alias example.com/locality.Target +//llgo:tls +var Alias = 1 +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + prog := llssa.NewProgram(nil) + if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, fset, pkg, info, files); err == nil || !strings.Contains(err.Error(), "linkname alias") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } +} + +func TestValidateLocalInitializers(t *testing.T) { + pkg := types.NewPackage("example.com/locality", "locality") + prog := ssatest.NewProgram(t, nil) + name := llssa.FullName(pkg, "value") + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + if err := validateLocalInitializers(prog, pkg); err == nil || !strings.Contains(err.Error(), "inconsistent initializer metadata") { + t.Fatalf("validateLocalInitializers error = %v", err) + } + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true, InitFunc: "example.com/locality.init", InitOrder: 1}) + if err := validateLocalInitializers(prog, pkg); err != nil { + t.Fatal(err) + } +} + +func TestNewPackageReportsLocalityPreparationErrors(t *testing.T) { + tests := []struct { + name string + src string + parseSyntax bool + wantError string + }{ + { + name: "invalid directive", + src: `package locality +//llgo:tls +func invalid() {} +`, + wantError: "applies only to package-level var declarations", + }, + { + name: "unprepared initializer", + src: `package locality +func initialValue() int { return 1 } +//llgo:tls +var value = initialValue() +`, + parseSyntax: true, + wantError: "inconsistent initializer metadata", + }, + { + name: "linkname locality mismatch", + src: `package locality +//llgo:tls +var Target int +//go:linkname Alias example.com/locality.Target +//llgo:gls +var Alias int +`, + wantError: "uses //llgo:gls", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", tt.src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, files, info, true) + ssaPkg.Build() + prog := ssatest.NewProgram(t, nil) + if tt.parseSyntax { + if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + t.Fatal(err) + } + } + if _, err := NewPackage(prog, ssaPkg, files); err == nil || !strings.Contains(err.Error(), tt.wantError) { + t.Fatalf("NewPackage error = %v, want %q", err, tt.wantError) + } + }) + } +} + +func TestPrepareRejectsLocalAliasWithoutLocalTarget(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality +//go:linkname Value C.value +//llgo:tls +var Value int +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgram(t, nil) + if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, fset, pkg, info, files); err == nil || !strings.Contains(err.Error(), "is not a local variable") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } +} + +func TestPrepareLocalVariablesEarlyReturns(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := types.NewPackage("example.com/locality", "locality") + info := &types.Info{} + if err := PrepareLocalVariables(prog, nil, nil, info, nil); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, nil, pkg, nil, nil); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, nil, pkg, info, nil); err != nil { + t.Fatal(err) + } + value := types.NewVar(token.NoPos, pkg, "value", types.Typ[types.Int]) + pkg.Scope().Insert(value) + prog.SetLocalityInfo(llssa.FullName(pkg, value.Name()), llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + info.InitOrder = []*types.Initializer{{Lhs: []*types.Var{value}, Rhs: ast.NewIdent("rhs")}} + if err := PrepareLocalVariables(prog, nil, pkg, info, nil); err == nil || !strings.Contains(err.Error(), "without syntax files") { + t.Fatalf("PrepareLocalVariables without files error = %v", err) + } + (&context{}).initializeLocalGuards(nil) +} + +func TestPrepareLocalVariablesRejectsInvalidMetadata(t *testing.T) { + t.Run("missing object", func(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := types.NewPackage("example.com/missing", "missing") + prog.SetLocalityInfo(llssa.FullName(pkg, "Value"), llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + if err := PrepareLocalVariables(prog, nil, pkg, &types.Info{}, nil); err == nil || !strings.Contains(err.Error(), "has no variable") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } + }) + t.Run("linkname cycle", func(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := types.NewPackage("example.com/cycle", "cycle") + first := llssa.FullName(pkg, "First") + second := llssa.FullName(pkg, "Second") + pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, "First", types.Typ[types.Int])) + prog.SetLocalityInfo(first, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLinkname(first, second) + prog.SetLinkname(second, first) + if err := PrepareLocalVariables(prog, nil, pkg, &types.Info{}, nil); err == nil || !strings.Contains(err.Error(), "linkname cycle") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } + }) +} + +func TestPlanLocalPackageDiagnostics(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + if plan, err := planLocalPackage(prog, nil); err != nil || len(plan.Variables) != 0 { + t.Fatalf("nil package plan = %+v, %v", plan, err) + } + + pkg := types.NewPackage("example.com/plan", "plan") + ordinary := llssa.FullName(pkg, "Ordinary") + pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, "Ordinary", types.Typ[types.Int])) + prog.SetLocalStorage(ordinary, llssa.LocalStorageNativeTLS) + if plan, err := planLocalPackage(prog, pkg); err != nil || len(plan.Variables) != 0 { + t.Fatalf("non-local metadata plan = %+v, %v", plan, err) + } + + missing := llssa.FullName(pkg, "Missing") + prog.SetLocalityInfo(missing, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + if _, err := planLocalPackage(prog, pkg); err == nil || !strings.Contains(err.Error(), "has no variable") { + t.Fatalf("missing object plan error = %v", err) + } +} + +func TestLocalInitializerNameCollision(t *testing.T) { + prog, _ := compileLocalitySource(t, `package locality +func __llgo_local_init_0() {} +//llgo:tls +var value = 1 +`) + info, ok := prog.VariableLocality("example.com/locality.value") + if !ok || !strings.HasSuffix(info.InitFunc, ".__llgo_local_init_1") || info.InitOrder != 1 { + t.Fatalf("value metadata = %+v, %v", info, ok) + } +} + +func TestNamedPointerLocalUsesPackageStorage(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +type Handle struct { Pointer *int } +func makeHandle() Handle { return Handle{} } +//llgo:tls +var Value = makeHandle() +func get() Handle { return Value } +`) + info, ok := prog.VariableLocality("example.com/locality.Value") + if !ok || info.LocalStorage != llssa.LocalStoragePackage { + t.Fatalf("named pointer metadata = %+v, %v", info, ok) + } + if !strings.Contains(ir, `call ptr @"example.com/locality.__llgo_local_block"()`) { + t.Fatalf("named pointer did not use package storage:\n%s", ir) + } +} diff --git a/cl/static_init.go b/cl/static_init.go index bbbe94cfb4..fd0f238151 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -85,6 +85,11 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { if _, rewritten := p.rewriteValue(globalName); rewritten { continue } + if info, ok := p.resolveLocality(llssa.FullName(global.Pkg.Pkg, global.Name())); ok && info.Locality != llssa.LocalityNone { + // Local initializers must remain executable so they can populate the + // current context rather than a process-wide LLVM initializer. + continue + } globals[global] = none{} } } diff --git a/internal/build/build.go b/internal/build/build.go index 2de1f96536..fced4cb4b9 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -394,11 +394,27 @@ func Do(args []string, conf *Config) ([]Package, error) { return prog.TypeSizes(sizes) } dedup := packages.NewDeduper() + var syntaxErr error + var syntaxErrMu sync.Mutex + recordSyntaxErr := func(err error) { + syntaxErrMu.Lock() + defer syntaxErrMu.Unlock() + if syntaxErr == nil { + syntaxErr = err + } + } + loadSyntaxErr := func() error { + syntaxErrMu.Lock() + defer syntaxErrMu.Unlock() + return syntaxErr + } dedup.SetPreload(func(pkg *types.Package, files []*ast.File) { if llruntime.SkipToBuild(pkg.Path()) { return } - cl.ParsePkgSyntax(prog, pkg, files) + if err := cl.ParsePkgSyntax(prog, cfg.Fset, pkg, files); err != nil { + recordSyntaxErr(err) + } }) if patterns == nil { @@ -421,6 +437,9 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + if err := loadSyntaxErr(); err != nil { + return nil, err + } if conf.AllowNoBody { allowMissingFunctionBodies(initial) } @@ -455,6 +474,9 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + if err := loadSyntaxErr(); err != nil { + return nil, err + } prog.SetRuntime(func() *types.Package { return altPkgs[0].Types @@ -462,7 +484,9 @@ func Do(args []string, conf *Config) ([]Package, error) { prog.SetPython(func() *types.Package { return dedup.Check(llssa.PkgPython).Types }) - preCollectRuntimeLinknames(prog, altPkgs) + if err := prepareLocalVariables(prog, initial, altPkgs); err != nil { + return nil, err + } buildMode := ssaBuildMode cabiOptimize := true @@ -1872,13 +1896,22 @@ func altPkgs(initial []*packages.Package, conf *Config, alts ...string) []string return alts } -func preCollectRuntimeLinknames(prog llssa.Program, pkgs []*packages.Package) { - for _, pkg := range pkgs { - if pkg != nil && pkg.PkgPath == llssa.PkgRuntime && len(pkg.Syntax) != 0 { - cl.PreCollectLinknames(prog, pkg.PkgPath, pkg.Syntax) - return +func prepareLocalVariables(prog llssa.Program, groups ...[]*packages.Package) error { + seen := make(map[*types.Package]bool) + var firstErr error + for _, roots := range groups { + packages.Visit(roots, nil, func(p *packages.Package) { + if firstErr != nil || p.Types == nil || p.IllTyped || seen[p.Types] { + return + } + seen[p.Types] = true + firstErr = cl.PrepareLocalVariables(prog, p.Fset, p.Types, p.TypesInfo, p.Syntax) + }) + if firstErr != nil { + return firstErr } } + return nil } func altSSAPkgs(prog *ssa.Program, patches cl.Patches, alts []*packages.Package, conf *Config, verbose bool) { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index c26241cc20..3ac9b5d371 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -10,6 +10,7 @@ import ( "go/ast" "go/parser" "go/token" + "go/types" "io" "os" "os/exec" @@ -19,6 +20,7 @@ import ( "strings" "testing" + "github.com/goplus/llgo/cl" "github.com/goplus/llgo/internal/buildenv" "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/lto" @@ -532,7 +534,7 @@ func TestCmpTestNonexistentPatternReturnsError(t *testing.T) { } } -func TestPreCollectRuntimeLinknames(t *testing.T) { +func TestParsePkgSyntaxCollectsRuntimeLinknames(t *testing.T) { prog := llssa.NewProgram(nil) fset := token.NewFileSet() file, err := parser.ParseFile(fset, "runtime.go", `package runtime @@ -543,15 +545,68 @@ func Sigsetjmp() if err != nil { t.Fatalf("ParseFile failed: %v", err) } - preCollectRuntimeLinknames(prog, []*packages.Package{{ - PkgPath: llssa.PkgRuntime, - Syntax: []*ast.File{file}, - }}) + pkg := types.NewPackage(llssa.PkgRuntime, "runtime") + if err := cl.ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } if got, ok := prog.Linkname(llssa.PkgRuntime + ".Sigsetjmp"); !ok || got != "C.sigsetjmp" { t.Fatalf("pre-collected runtime linkname = (%q,%v), want (%q,%v)", got, ok, "C.sigsetjmp", true) } } +func TestPrepareLocalVariables(t *testing.T) { + newLocalPackage := func(path string, withSyntax bool) (*packages.Package, *ast.File) { + pkg := types.NewPackage(path, "local") + value := types.NewVar(token.NoPos, pkg, "value", types.Typ[types.Int]) + pkg.Scope().Insert(value) + info := &types.Info{ + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + InitOrder: []*types.Initializer{{Lhs: []*types.Var{value}, Rhs: ast.NewIdent("rhs")}}, + } + loaded := &packages.Package{Types: pkg, TypesInfo: info} + var file *ast.File + if withSyntax { + file = &ast.File{Name: ast.NewIdent("local")} + loaded.Syntax = []*ast.File{file} + } + return loaded, file + } + + t.Run("filters and deduplicates packages", func(t *testing.T) { + prog := llssa.NewProgram(nil) + loaded, file := newLocalPackage("example.com/local", true) + prog.SetLocalityInfo("example.com/local.value", llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + duplicate := *loaded + + err := prepareLocalVariables(prog, + []*packages.Package{{}, {Types: types.NewPackage("example.com/bad", "bad"), IllTyped: true}, loaded}, + []*packages.Package{&duplicate}, + ) + if err != nil { + t.Fatal(err) + } + if got := len(file.Decls); got != 1 { + t.Fatalf("generated initializer declarations = %d, want 1", got) + } + }) + + t.Run("returns dependency error", func(t *testing.T) { + prog := llssa.NewProgram(nil) + dependency, _ := newLocalPackage("example.com/dependency", false) + prog.SetLocalityInfo("example.com/dependency.value", llssa.LocalityInfo{Locality: llssa.GoroutineLocal, HasInitializer: true}) + root := &packages.Package{ + Types: types.NewPackage("example.com/root", "root"), + Imports: map[string]*packages.Package{"example.com/dependency": dependency}, + } + + err := prepareLocalVariables(prog, []*packages.Package{root}) + if err == nil || !strings.Contains(err.Error(), "without syntax files") { + t.Fatalf("prepareLocalVariables error = %v", err) + } + }) +} + func TestLTOEnabledDefault(t *testing.T) { host := &Config{Target: ""} if host.ltoEnabled() { @@ -879,6 +934,70 @@ func F() {} pkgs[0].LPkg.Prog.Dispose() } +func TestDoReportsLocalityDirectiveError(t *testing.T) { + file := filepath.Join(t.TempDir(), "invalid_locality.go") + if err := os.WriteFile(file, []byte(`package invalidlocality + +//llgo:tls +func Invalid() {} +`), 0o644); err != nil { + t.Fatal(err) + } + conf := NewDefaultConf(ModeGen) + if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "applies only to package-level var declarations") { + t.Fatalf("Do error = %v, want locality directive diagnostic", err) + } +} + +func TestDoReportsLocalityAliasInitializer(t *testing.T) { + file := filepath.Join(t.TempDir(), "invalid_locality_alias.go") + if err := os.WriteFile(file, []byte(`package invalidlocalityalias + +import _ "unsafe" + +//llgo:tls +var Target int + +//go:linkname Alias example.com/target.Value +//llgo:tls +var Alias = 1 +`), 0o644); err != nil { + t.Fatal(err) + } + conf := NewDefaultConf(ModeGen) + if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "linkname alias") { + t.Fatalf("Do error = %v, want locality alias initializer diagnostic", err) + } +} + +func TestDoReportsAltPackageLocalityDirectiveError(t *testing.T) { + root := t.TempDir() + runtimeDir := filepath.Join(root, "runtime") + runtimePkgDir := filepath.Join(runtimeDir, "internal", "runtime") + if err := os.MkdirAll(runtimePkgDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(runtimeDir, "go.mod"), []byte("module github.com/goplus/llgo/runtime\n\ngo 1.24.0\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(runtimePkgDir, "runtime.go"), []byte(`package runtime + +//llgo:gls +func Invalid() {} +`), 0o644); err != nil { + t.Fatal(err) + } + file := filepath.Join(root, "main.go") + if err := os.WriteFile(file, []byte("package main\nfunc main() {}\n"), 0o644); err != nil { + t.Fatal(err) + } + t.Setenv("LLGO_ROOT", root) + conf := NewDefaultConf(ModeGen) + if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "applies only to package-level var declarations") { + t.Fatalf("Do error = %v, want alternate-package locality directive diagnostic", err) + } +} + func TestFormatPackageError(t *testing.T) { tests := []struct { name string diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 5dbf0ad81f..9dec932db2 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -225,6 +225,11 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa fnVal.SetUnnamedAddr(true) } b := fn.MakeBody(1) + var localCtx, previousLocalCtx llssa.Expr + hasLocalContext := prog.NeedsLocalContext() + if hasLocalContext { + localCtx, previousLocalCtx = b.EnterLocalContext() + } b.Store(argcVar.Expr, fn.Param(0)) b.Store(argvVar.Expr, fn.Param(1)) if IsStdioNobuf() { @@ -245,6 +250,9 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa if fns.pyFinalize != nil { b.Call(fns.pyFinalize.Expr) } + if hasLocalContext { + b.LeaveLocalContext(localCtx, previousLocalCtx) + } b.Return(prog.IntVal(0, prog.Int32())) return fn } diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index f803182482..c7bba6e9a7 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -4,6 +4,8 @@ package build import ( + "go/token" + "go/types" "strings" "testing" @@ -113,6 +115,44 @@ func TestGenMainModuleLibraryInitializesRuntime(t *testing.T) { } } +func TestGenMainModuleInstallsLocalContextWhenNeeded(t *testing.T) { + llvm.InitializeAllTargets() + t.Setenv(llgoStdioNobuf, "") + prog := llssa.NewProgram(nil) + runtimePkg := types.NewPackage(llssa.PkgRuntime, "runtime") + contextName := types.NewTypeName(token.NoPos, runtimePkg, "LocalContext", nil) + contextType := types.NewNamed(contextName, types.NewStruct(nil, nil), nil) + runtimePkg.Scope().Insert(contextName) + contextPointer := types.NewPointer(contextType) + enterParams := types.NewTuple(types.NewParam(token.NoPos, runtimePkg, "ctx", contextPointer)) + enterResults := types.NewTuple(types.NewParam(token.NoPos, runtimePkg, "previous", types.Typ[types.Uintptr])) + runtimePkg.Scope().Insert(types.NewFunc(token.NoPos, runtimePkg, "EnterLocalContext", types.NewSignatureType(nil, nil, nil, enterParams, enterResults, false))) + leaveParams := types.NewTuple( + types.NewParam(token.NoPos, runtimePkg, "ctx", contextPointer), + types.NewParam(token.NoPos, runtimePkg, "previous", types.Typ[types.Uintptr]), + ) + runtimePkg.Scope().Insert(types.NewFunc(token.NoPos, runtimePkg, "LeaveLocalContext", types.NewSignatureType(nil, nil, nil, leaveParams, nil, false))) + prog.SetRuntime(runtimePkg) + prog.SetLocalityInfo("example.com/state.Value", llssa.LocalityInfo{Locality: llssa.GoroutineLocal}) + prog.SetLocalStorage("example.com/state.Value", llssa.LocalStoragePackage) + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"} + ir := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{}).LPkg.String() + assertInOrder(t, ir, + "EnterLocalContext", + `call void @"example.com/foo.init"()`, + `call void @"example.com/foo.main"()`, + "LeaveLocalContext", + ) +} + func assertInOrder(t *testing.T, s string, wants ...string) { t.Helper() offset := 0 diff --git a/internal/locality/layout/layout.go b/internal/locality/layout/layout.go new file mode 100644 index 0000000000..0589c39f1f --- /dev/null +++ b/internal/locality/layout/layout.go @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package layout plans the internal storage layout for one package's local variables. It +// is intentionally independent of LLGo SSA, LLVM, the runtime, and the build +// cache so every integration layer consumes the same policy result. +package layout + +import ( + "fmt" + "go/types" + "sort" + + "github.com/goplus/llgo/internal/locality" +) + +// Storage identifies the physical addressing strategy selected for a variable. +type Storage uint8 + +const ( + StorageUnknown Storage = iota + StorageNativeTLS + StoragePackage +) + +// Declaration is the source information needed to plan one variable. +type Declaration struct { + Name string + Type types.Type + Info locality.Info +} + +// Variable is one planned local variable. Field is valid for package storage. +type Variable struct { + Declaration + Storage Storage + Field int +} + +// Initializer identifies one replay helper in Go package initialization order. +type Initializer struct { + Name string + Order int +} + +// Package is a deterministic storage plan. Block contains all pointer-bearing +// TLS and GLS variables in their shared physical package block. +type Package struct { + Path string + Variables []Variable + Block []Variable + Thread []Initializer + Goroutine []Initializer + + byName map[string]int +} + +// Plan creates a deterministic package layout. TLS and GLS remain logically +// distinct, while the current one-thread-per-goroutine backend may share their +// pointer-bearing package block. +func Plan(path string, declarations []Declaration) (Package, error) { + ret := Package{Path: path} + decls := append([]Declaration(nil), declarations...) + sort.Slice(decls, func(i, j int) bool { return decls[i].Name < decls[j].Name }) + ret.byName = make(map[string]int, len(decls)) + initializers := map[locality.Kind]map[int]string{ + locality.Thread: {}, + locality.Goroutine: {}, + } + for _, decl := range decls { + if decl.Info.Locality == locality.None { + continue + } + if decl.Name == "" || decl.Type == nil { + return Package{}, fmt.Errorf("locality layout: incomplete declaration %q", decl.Name) + } + if _, exists := ret.byName[decl.Name]; exists { + return Package{}, fmt.Errorf("locality layout: duplicate declaration %s", decl.Name) + } + if decl.Info.Locality != locality.Thread && decl.Info.Locality != locality.Goroutine { + return Package{}, fmt.Errorf("locality layout: invalid locality for %s", decl.Name) + } + prepared := decl.Info.InitFunc != "" && decl.Info.InitOrder != 0 + if decl.Info.HasInitializer != prepared { + return Package{}, fmt.Errorf("locality layout: inconsistent initializer metadata for %s", decl.Name) + } + variable := Variable{Declaration: decl, Field: -1, Storage: StorageForType(decl.Type)} + if variable.Storage == StoragePackage { + variable.Field = len(ret.Block) + ret.Block = append(ret.Block, variable) + } + ret.byName[decl.Name] = len(ret.Variables) + ret.Variables = append(ret.Variables, variable) + if decl.Info.InitFunc != "" { + byOrder := initializers[decl.Info.Locality] + if current, exists := byOrder[decl.Info.InitOrder]; exists && current != decl.Info.InitFunc { + return Package{}, fmt.Errorf("locality layout: initializer order %d names both %s and %s", decl.Info.InitOrder, current, decl.Info.InitFunc) + } + byOrder[decl.Info.InitOrder] = decl.Info.InitFunc + } + } + ret.Thread = orderedInitializers(initializers[locality.Thread]) + ret.Goroutine = orderedInitializers(initializers[locality.Goroutine]) + return ret, nil +} + +// StorageForType returns the physical storage class for a local variable type. +// Pointer-free values use native LLVM TLS; values visible to the GC share the +// package block rooted by LocalContext. +func StorageForType(typ types.Type) Storage { + if hasPointers(typ) { + return StoragePackage + } + return StorageNativeTLS +} + +func hasPointers(typ types.Type) bool { + typ = types.Unalias(typ) + switch typ := typ.(type) { + case *types.Basic: + return typ.Kind() == types.String || typ.Kind() == types.UnsafePointer + case *types.Pointer, *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface: + return true + case *types.Array: + return typ.Len() != 0 && hasPointers(typ.Elem()) + case *types.Struct: + for i := 0; i < typ.NumFields(); i++ { + if hasPointers(typ.Field(i).Type()) { + return true + } + } + return false + case *types.Named: + return hasPointers(typ.Underlying()) + case *types.TypeParam: + return true + default: + return false + } +} + +func orderedInitializers(byOrder map[int]string) []Initializer { + ret := make([]Initializer, 0, len(byOrder)) + for order, name := range byOrder { + ret = append(ret, Initializer{Name: name, Order: order}) + } + sort.Slice(ret, func(i, j int) bool { return ret[i].Order < ret[j].Order }) + return ret +} + +// Lookup returns a variable from the package plan. +func (p Package) Lookup(name string) (Variable, bool) { + index, ok := p.byName[name] + if !ok { + return Variable{}, false + } + return p.Variables[index], true +} + +// Initializers returns the ordered replay helpers for kind. +func (p Package) Initializers(kind locality.Kind) []Initializer { + if kind == locality.Thread { + return p.Thread + } + if kind == locality.Goroutine { + return p.Goroutine + } + return nil +} + +// BlockName returns the shared package-block accessor symbol. +func BlockName(path string) string { return qualify(path, "__llgo_local_block") } + +// BlockKeyName returns the shared package-block descriptor symbol. +func BlockKeyName(path string) string { return qualify(path, "__llgo_local_key") } + +// InitName returns the package/kind initializer dispatcher symbol. +func InitName(path string, kind locality.Kind) string { + return qualify(path, "__llgo_"+kind.String()+"_init") +} + +// EnsureName returns the package/kind first-use initializer symbol. +func EnsureName(path string, kind locality.Kind) string { return InitName(path, kind) + "$ensure" } + +// GuardName returns the package/kind native TLS state symbol. +func GuardName(path string, kind locality.Kind) string { return InitName(path, kind) + "$guard" } + +// FailureKeyName returns the package/kind initializer failure key symbol. +func FailureKeyName(path string, kind locality.Kind) string { return InitName(path, kind) + "$failure" } + +func qualify(path, name string) string { + if path == "" { + return name + } + return path + "." + name +} diff --git a/internal/locality/layout/layout_test.go b/internal/locality/layout/layout_test.go new file mode 100644 index 0000000000..3aabeb2b79 --- /dev/null +++ b/internal/locality/layout/layout_test.go @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package layout + +import ( + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/locality" +) + +func TestPlanSharesPointerStorageAndPreservesKinds(t *testing.T) { + plan, err := Plan("example.com/state", []Declaration{ + {Name: "example.com/state.Ignored", Type: types.Typ[types.Int]}, + {Name: "example.com/state.Z", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Goroutine}}, + {Name: "example.com/state.P", Type: types.NewPointer(types.Typ[types.Int]), Info: locality.Info{Locality: locality.Thread, HasInitializer: true, InitFunc: "p.init1", InitOrder: 2}}, + {Name: "example.com/state.A", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Goroutine, HasInitializer: true, InitFunc: "p.init0", InitOrder: 1}}, + {Name: "example.com/state.Q", Type: types.NewSlice(types.Typ[types.Byte]), Info: locality.Info{Locality: locality.Goroutine}}, + }) + if err != nil { + t.Fatal(err) + } + if len(plan.Variables) != 4 || len(plan.Block) != 2 { + t.Fatalf("plan sizes = %d/%d", len(plan.Variables), len(plan.Block)) + } + if got, _ := plan.Lookup("example.com/state.Z"); got.Storage != StorageNativeTLS || got.Info.Locality != locality.Goroutine { + t.Fatalf("scalar GLS plan = %+v", got) + } + if got, _ := plan.Lookup("example.com/state.P"); got.Storage != StoragePackage || got.Field != 0 { + t.Fatalf("pointer TLS plan = %+v", got) + } + if got, _ := plan.Lookup("example.com/state.Q"); got.Storage != StoragePackage || got.Field != 1 { + t.Fatalf("slice GLS plan = %+v", got) + } + if len(plan.Thread) != 1 || plan.Thread[0].Name != "p.init1" { + t.Fatalf("thread initializers = %+v", plan.Thread) + } + if len(plan.Goroutine) != 1 || plan.Goroutine[0].Name != "p.init0" { + t.Fatalf("goroutine initializers = %+v", plan.Goroutine) + } + if got := plan.Initializers(locality.Thread); len(got) != 1 || got[0].Name != "p.init1" { + t.Fatalf("Initializers(thread) = %+v", got) + } + if got := plan.Initializers(locality.Goroutine); len(got) != 1 || got[0].Name != "p.init0" { + t.Fatalf("Initializers(goroutine) = %+v", got) + } + if got := plan.Initializers(locality.None); got != nil { + t.Fatalf("Initializers(none) = %+v", got) + } + if _, ok := plan.Lookup("example.com/state.Missing"); ok { + t.Fatal("Lookup found a missing variable") + } +} + +func TestOrderedInitializers(t *testing.T) { + got := orderedInitializers(map[int]string{3: "p.third", 1: "p.first", 2: "p.second"}) + if len(got) != 3 || got[0].Name != "p.first" || got[1].Name != "p.second" || got[2].Name != "p.third" { + t.Fatalf("ordered initializers = %+v", got) + } +} + +func TestPlanRejectsInvalidDeclarations(t *testing.T) { + tests := []struct { + name string + in []Declaration + want string + }{ + {"missing type", []Declaration{{Name: "p.x", Info: locality.Info{Locality: locality.Thread}}}, "incomplete"}, + {"duplicate", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread}}, {Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread}}}, "duplicate"}, + {"invalid kind", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Kind(99)}}}, "invalid locality"}, + {"unprepared", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, HasInitializer: true}}}, "inconsistent initializer metadata"}, + {"unexpected helper", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, InitFunc: "p.a", InitOrder: 1}}}, "inconsistent initializer metadata"}, + {"order conflict", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, HasInitializer: true, InitFunc: "p.a", InitOrder: 1}}, {Name: "p.y", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, HasInitializer: true, InitFunc: "p.b", InitOrder: 1}}}, "names both"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := Plan("p", test.in); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("Plan error = %v, want %q", err, test.want) + } + }) + } +} + +func TestNames(t *testing.T) { + if got := BlockName("example.com/p"); got != "example.com/p.__llgo_local_block" { + t.Fatal(got) + } + if got := BlockKeyName("example.com/p"); got != "example.com/p.__llgo_local_key" { + t.Fatal(got) + } + if got := InitName("example.com/p", locality.Thread); got != "example.com/p.__llgo_tls_init" { + t.Fatal(got) + } + if got := EnsureName("example.com/p", locality.Goroutine); got != "example.com/p.__llgo_gls_init$ensure" { + t.Fatal(got) + } + if got := GuardName("", locality.Thread); got != "__llgo_tls_init$guard" { + t.Fatal(got) + } + if got := FailureKeyName("", locality.Goroutine); got != "__llgo_gls_init$failure" { + t.Fatal(got) + } +} + +func TestStorageForType(t *testing.T) { + if got := StorageForType(types.Typ[types.Uintptr]); got != StorageNativeTLS { + t.Fatalf("uintptr storage = %v", got) + } + if got := StorageForType(types.NewArray(types.NewPointer(types.Typ[types.Int]), 0)); got != StorageNativeTLS { + t.Fatalf("zero-length pointer array storage = %v", got) + } + if got := StorageForType(types.NewStruct([]*types.Var{ + types.NewField(0, nil, "value", types.NewPointer(types.Typ[types.Int]), false), + }, nil)); got != StoragePackage { + t.Fatalf("pointer struct storage = %v", got) + } +} + +func TestHasPointers(t *testing.T) { + pkg := types.NewPackage("example.com/types", "types") + namedInt := types.NewNamed(types.NewTypeName(token.NoPos, pkg, "Int", nil), types.Typ[types.Int], nil) + typeParam := types.NewTypeParam(types.NewTypeName(token.NoPos, pkg, "T", nil), types.NewInterfaceType(nil, nil).Complete()) + tests := []struct { + typ types.Type + want bool + }{ + {types.Typ[types.Int], false}, + {types.Typ[types.String], true}, + {types.Typ[types.UnsafePointer], true}, + {types.NewPointer(types.Typ[types.Int]), true}, + {types.NewSlice(types.Typ[types.Int]), true}, + {types.NewMap(types.Typ[types.Int], types.Typ[types.Int]), true}, + {types.NewChan(types.SendRecv, types.Typ[types.Int]), true}, + {types.NewSignatureType(nil, nil, nil, nil, nil, false), true}, + {types.NewInterfaceType(nil, nil).Complete(), true}, + {types.NewArray(types.Typ[types.Int], 1), false}, + {types.NewArray(types.NewPointer(types.Typ[types.Int]), 0), false}, + {types.NewArray(types.NewPointer(types.Typ[types.Int]), 1), true}, + {types.NewStruct([]*types.Var{types.NewVar(token.NoPos, pkg, "n", types.Typ[types.Int])}, nil), false}, + {types.NewStruct([]*types.Var{types.NewVar(token.NoPos, pkg, "p", types.NewPointer(types.Typ[types.Int]))}, nil), true}, + {namedInt, false}, + {typeParam, true}, + {types.NewTuple(), false}, + } + for _, test := range tests { + if got := hasPointers(test.typ); got != test.want { + t.Fatalf("hasPointers(%v) = %v, want %v", test.typ, got, test.want) + } + } +} diff --git a/ssa/decl.go b/ssa/decl.go index df70a66a0e..a575dd1bee 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -117,9 +117,25 @@ func (p Package) NewVarEx(name string, t Type) Global { return p.doNewVar(name, t) } +// NewThreadLocalVar creates a native TLS variable. Unlike NewVar, it keeps +// independent storage for zero-sized values instead of using the module-wide +// zero-sized allocation sentinel. +func (p Package) NewThreadLocalVar(name string, typ types.Type, bg Background) Global { + if v, ok := p.vars[name]; ok { + v.impl.SetThreadLocal(true) + return v + } + t := p.Prog.Type(typ, bg) + return p.doNewVarEx(name, t, true) +} + func (p Package) doNewVar(name string, t Type) Global { + return p.doNewVarEx(name, t, false) +} + +func (p Package) doNewVarEx(name string, t Type, threadLocal bool) Global { typ := p.Prog.Elem(t).ll - if p.Prog.td.TypeAllocSize(typ) == 0 { + if !threadLocal && p.Prog.td.TypeAllocSize(typ) == 0 { var rt *types.Package if p.Prog.rt != nil || p.Prog.rtget != nil { rt = p.Prog.runtime() @@ -141,6 +157,7 @@ func (p Package) doNewVar(name string, t Type) Global { } } gbl := llvm.AddGlobal(p.mod, typ, name) + gbl.SetThreadLocal(threadLocal) alignment := p.Prog.td.ABITypeAlignment(typ) gbl.SetAlignment(alignment) ret := &aGlobal{Expr{gbl, t}} diff --git a/ssa/goroutine.go b/ssa/goroutine.go index 1c9f5708c3..8f2c30a578 100644 --- a/ssa/goroutine.go +++ b/ssa/goroutine.go @@ -110,6 +110,11 @@ func (p Package) routine(t Type, fn Expr, buildCall func(Builder, Expr, ...Expr) prog := p.Prog routine := p.NewFunc(p.routineName(), prog.tyRoutine(), InC) b := routine.MakeBody(1) + var localCtx, previousLocalCtx Expr + hasLocalContext := prog.NeedsLocalContext() + if hasLocalContext { + localCtx, previousLocalCtx = b.EnterLocalContext() + } param := routine.Param(0) data := Expr{llvm.CreateLoad(b.impl, t.ll, param.impl), t} args := make([]Expr, n) @@ -125,6 +130,9 @@ func (p Package) routine(t Type, fn Expr, buildCall func(Builder, Expr, ...Expr) buildCall(b, fn, args...) lastInst := b.impl.GetInsertBlock().LastInstruction() if lastInst.IsNil() || lastInst.IsAUnreachableInst().IsNil() { + if hasLocalContext { + b.LeaveLocalContext(localCtx, previousLocalCtx) + } b.Return(prog.Nil(prog.VoidPtr())) } return routine.Expr diff --git a/ssa/goroutine_patch_test.go b/ssa/goroutine_patch_test.go index 16af8643c9..28f0b8bf8f 100644 --- a/ssa/goroutine_patch_test.go +++ b/ssa/goroutine_patch_test.go @@ -53,6 +53,29 @@ func TestGoClosureStartupUsesGCManagedMemory(t *testing.T) { if got := strings.Count(ir, `"github.com/goplus/llgo/runtime/internal/runtime.AllocU"`); got < 1 { t.Fatalf("expected closure ctx to use AllocU, got %d:\n%s", got, ir) } + if strings.Contains(ir, "EnterLocalContext") { + t.Fatalf("program without context-backed locals paid locality entry cost:\n%s", ir) + } +} + +func TestGoInstallsContextForContextBackedLocals(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + prog.SetLocalityInfo("example.com/state.Value", ssa.LocalityInfo{Locality: ssa.GoroutineLocal}) + prog.SetLocalStorage("example.com/state.Value", ssa.LocalStoragePackage) + pkg := prog.NewPackage("bar", "foo/bar") + outer := pkg.NewFunc("outer", ssa.NoArgsNoRet, ssa.InGo) + b := outer.MakeBody(1) + b.Go(ssa.Nil, func(b ssa.Builder, _ ssa.Expr, args ...ssa.Expr) ssa.Expr { + return ssa.Expr{} + }) + b.Return() + + ir := pkg.String() + for _, want := range []string{"LocalContext", "EnterLocalContext", "LeaveLocalContext"} { + if !strings.Contains(ir, want) { + t.Fatalf("goroutine wrapper missing %q:\n%s", want, ir) + } + } } func TestGoPanicRoutineDoesNotReturnAfterUnreachable(t *testing.T) { diff --git a/ssa/local_context.go b/ssa/local_context.go new file mode 100644 index 0000000000..9739ff19cc --- /dev/null +++ b/ssa/local_context.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import "go/types" + +// EnterLocalContext creates the stack root used by TLS/GLS locality blocks and +// installs it for the current outermost Go entry. previous is nonzero only for +// a nested entry that inherited an existing context. +func (b Builder) EnterLocalContext() (ctx, previous Expr) { + fn := b.Pkg.rtFunc("EnterLocalContext") + params := fn.raw.Type.(*types.Signature).Params() + ctxPtr := b.Prog.rawType(params.At(0).Type()) + ctxType := b.Prog.rawType(ctxPtr.RawType().(*types.Pointer).Elem()) + ctx = b.Alloc(ctxType, false) + previous = b.Call(fn, ctx) + return +} + +// LeaveLocalContext restores an inherited context or drops the stack roots +// installed by EnterLocalContext. +func (b Builder) LeaveLocalContext(ctx, previous Expr) { + b.Call(b.Pkg.rtFunc("LeaveLocalContext"), ctx, previous) +} diff --git a/ssa/locality.go b/ssa/locality.go new file mode 100644 index 0000000000..4cfbb68fa5 --- /dev/null +++ b/ssa/locality.go @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "fmt" + "go/types" + "sort" + "strings" + "sync" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" +) + +type Locality = locality.Kind + +const ( + LocalityNone = locality.None + ThreadLocal = locality.Thread + GoroutineLocal = locality.Goroutine +) + +type LocalityInfo = locality.Info +type LocalStorage = localitylayout.Storage + +const ( + LocalStorageUnknown = localitylayout.StorageUnknown + LocalStorageNativeTLS = localitylayout.StorageNativeTLS + LocalStoragePackage = localitylayout.StoragePackage +) + +// VariableLocality is the locality metadata attached to one package variable. +type VariableLocality struct { + LocalStorage LocalStorage + locality.Info +} + +type localityInfos struct { + mu sync.RWMutex + entries map[string]VariableLocality + parsedPackages map[*types.Package]struct{} +} + +func newLocalityInfos() *localityInfos { + return &localityInfos{ + entries: make(map[string]VariableLocality), + parsedPackages: make(map[*types.Package]struct{}), + } +} + +func (p *localityInfos) update(name string, update func(*VariableLocality)) { + p.mu.Lock() + info := p.entries[name] + update(&info) + p.entries[name] = info + p.mu.Unlock() +} + +func (p Program) SetLocalityInfo(name string, info LocalityInfo) { + p.localities.update(name, func(current *VariableLocality) { current.Info = info }) +} + +func (p Program) SetLocalStorage(name string, storage LocalStorage) { + p.localities.update(name, func(info *VariableLocality) { info.LocalStorage = storage }) +} + +func (p Program) VariableLocality(name string) (VariableLocality, bool) { + p.localities.mu.RLock() + info, ok := p.localities.entries[name] + p.localities.mu.RUnlock() + return info, ok +} + +// ResolveLocality follows linkname aliases and returns the canonical declaration +// name together with its merged locality metadata. +func (p Program) ResolveLocality(name string) (string, VariableLocality, bool, error) { + lookup := func(name string) (VariableLocality, bool) { + p.localities.mu.RLock() + info, ok := p.localities.entries[name] + p.localities.mu.RUnlock() + return info, ok + } + return resolveLocality(lookup, p.Linkname, name) +} + +func resolveLocality(lookup func(string) (VariableLocality, bool), linkname func(string) (string, bool), name string) (string, VariableLocality, bool, error) { + result, ok := lookup(name) + if !ok { + result = VariableLocality{} + } + seen := make(map[string]bool) + current := name + for { + if seen[current] { + return "", VariableLocality{}, false, fmt.Errorf("declaration linkname cycle involving %s", current) + } + seen[current] = true + target, hasLink := linkname(current) + target = strings.TrimPrefix(target, "go:") + if !hasLink || target == "" || target == current { + return current, result, ok, nil + } + targetInfo, exists := lookup(target) + if exists && targetInfo.Locality != locality.None { + switch { + case result.Locality == locality.None: + result = targetInfo + ok = true + case result.Locality != targetInfo.Locality: + return "", VariableLocality{}, false, fmt.Errorf("linkname alias %s uses %s but target %s uses %s", name, locality.Directive(result.Locality), target, locality.Directive(targetInfo.Locality)) + case hasInitialization(result.Info) && hasInitialization(targetInfo.Info) && result.Info != targetInfo.Info: + return "", VariableLocality{}, false, fmt.Errorf("linkname alias %s and target %s have incompatible local initializers", name, target) + case !hasInitialization(result.Info): + result.Info = targetInfo.Info + } + if result.LocalStorage == LocalStorageUnknown { + result.LocalStorage = targetInfo.LocalStorage + } else if targetInfo.LocalStorage != LocalStorageUnknown && result.LocalStorage != targetInfo.LocalStorage { + return "", VariableLocality{}, false, fmt.Errorf("linkname alias %s and target %s have incompatible local storage", name, target) + } + } + current = target + } +} + +func hasInitialization(info locality.Info) bool { + return info.HasInitializer || info.InitFunc != "" || info.InitOrder != 0 +} + +func (p Program) ValidateLocalities(pkgPath string) error { + prefix := pkgPath + "." + p.localities.mu.RLock() + names := make([]string, 0) + for name := range p.localities.entries { + if strings.HasPrefix(name, prefix) { + names = append(names, name) + } + } + p.localities.mu.RUnlock() + sort.Strings(names) + for _, name := range names { + if _, _, _, err := p.ResolveLocality(name); err != nil { + return err + } + } + return nil +} + +func (p Program) PackageSyntaxParsed(pkg *types.Package) bool { + p.localities.mu.RLock() + _, ok := p.localities.parsedPackages[pkg] + p.localities.mu.RUnlock() + return ok +} + +func (p Program) MarkPackageSyntaxParsed(pkg *types.Package) { + p.localities.mu.Lock() + p.localities.parsedPackages[pkg] = struct{}{} + p.localities.mu.Unlock() +} + +func (p Program) PackageLocalities(pkgPath string) map[string]VariableLocality { + prefix := pkgPath + "." + ret := make(map[string]VariableLocality) + p.localities.mu.RLock() + for name, info := range p.localities.entries { + if info.Locality != locality.None && strings.HasPrefix(name, prefix) { + ret[name] = info + } + } + p.localities.mu.RUnlock() + return ret +} + +func (p Program) NeedsLocalContext() bool { + p.localities.mu.RLock() + defer p.localities.mu.RUnlock() + for _, info := range p.localities.entries { + if info.Locality != locality.None && (info.LocalStorage != LocalStorageNativeTLS || hasInitialization(info.Info)) { + return true + } + } + return false +} diff --git a/ssa/locality_test.go b/ssa/locality_test.go new file mode 100644 index 0000000000..991c80808d --- /dev/null +++ b/ssa/locality_test.go @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "go/types" + "strings" + "testing" +) + +func TestLocalityInfos(t *testing.T) { + prog := NewProgram(nil) + pkg := types.NewPackage("example.com/p", "p") + if prog.PackageSyntaxParsed(pkg) { + t.Fatal("new package was already marked as parsed") + } + prog.MarkPackageSyntaxParsed(pkg) + if !prog.PackageSyntaxParsed(pkg) { + t.Fatal("package syntax parsed marker was not retained") + } + if prog.PackageSyntaxParsed(types.NewPackage("example.com/p", "p")) { + t.Fatal("syntax marker was shared by distinct package objects") + } + + name := "example.com/p.value" + prog.SetLocalityInfo(name, LocalityInfo{ + Locality: GoroutineLocal, + HasInitializer: true, + InitFunc: "example.com/p.initLocal", + InitOrder: 1, + }) + prog.SetLocalStorage(name, LocalStoragePackage) + + want, ok := prog.VariableLocality(name) + if !ok || want.Locality != GoroutineLocal || want.LocalStorage != LocalStoragePackage || !want.HasInitializer || want.InitFunc == "" || want.InitOrder != 1 { + t.Fatalf("VariableLocality(%q) = %+v, %v", name, want, ok) + } + + prog.SetLocalStorage("example.com/p.ordinary", LocalStorageNativeTLS) + decls := prog.PackageLocalities("example.com/p") + if len(decls) != 1 || decls[name] != want { + t.Fatalf("PackageLocalities = %+v", decls) + } + delete(decls, name) + if _, ok := prog.VariableLocality(name); !ok { + t.Fatal("mutating PackageLocalities changed program metadata") + } +} + +func TestNeedsLocalContext(t *testing.T) { + prog := NewProgram(nil) + if prog.NeedsLocalContext() { + t.Fatal("empty program needs a local context") + } + name := "example.com/p.value" + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) + if !prog.NeedsLocalContext() { + t.Fatal("unknown local storage did not conservatively require a context") + } + prog.SetLocalStorage(name, LocalStorageNativeTLS) + if prog.NeedsLocalContext() { + t.Fatal("native TLS required a local context") + } + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal, HasInitializer: true, InitFunc: "example.com/p.initValue", InitOrder: 1}) + if !prog.NeedsLocalContext() { + t.Fatal("native TLS initializer failure storage did not require a context") + } + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) + prog.SetLocalStorage(name, LocalStoragePackage) + if !prog.NeedsLocalContext() { + t.Fatal("context storage was not detected") + } +} + +func TestResolveLinknameLocality(t *testing.T) { + prog := NewProgram(nil) + target := "example.com/target.Value" + alias := "example.com/alias.Value" + prog.SetLocalityInfo(target, LocalityInfo{Locality: ThreadLocal, HasInitializer: true, InitFunc: "example.com/target.initValue", InitOrder: 1}) + prog.SetLocalStorage(target, LocalStoragePackage) + prog.SetLinkname(alias, target) + + _, got, ok, err := prog.ResolveLocality(alias) + if err != nil { + t.Fatal(err) + } + if !ok || got.Locality != ThreadLocal || got.LocalStorage != LocalStoragePackage || got.InitFunc != "example.com/target.initValue" || got.InitOrder != 1 { + t.Fatalf("ResolveLocality(%q) = %+v, %v", alias, got, ok) + } + if err := prog.ValidateLocalities("example.com/alias"); err != nil { + t.Fatal(err) + } + sameKind := "example.com/alias.SameKind" + prog.SetLinkname(sameKind, target) + prog.SetLocalityInfo(sameKind, LocalityInfo{Locality: ThreadLocal}) + if _, got, ok, err := prog.ResolveLocality(sameKind); err != nil || !ok || got.InitFunc != "example.com/target.initValue" { + t.Fatalf("same-kind ResolveLocality(%q) = %+v, %v", sameKind, got, ok) + } + + incompatible := "example.com/alias.Incompatible" + prog.SetLinkname(incompatible, target) + prog.SetLocalityInfo(incompatible, LocalityInfo{Locality: ThreadLocal, HasInitializer: true, InitFunc: "example.com/alias.initValue", InitOrder: 1}) + if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "incompatible local initializers") { + t.Fatalf("initializer mismatch error = %v", err) + } + targetDecl, _ := prog.VariableLocality(target) + prog.SetLocalityInfo(incompatible, targetDecl.Info) + + storageMismatch := "example.com/alias.StorageMismatch" + prog.SetLinkname(storageMismatch, target) + prog.SetLocalityInfo(storageMismatch, LocalityInfo{Locality: ThreadLocal}) + prog.SetLocalStorage(storageMismatch, LocalStorageNativeTLS) + if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "incompatible local storage") { + t.Fatalf("storage mismatch error = %v", err) + } + prog.SetLocalStorage(storageMismatch, LocalStoragePackage) + + prog.SetLocalityInfo(alias, LocalityInfo{Locality: GoroutineLocal}) + if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "uses //llgo:gls") { + t.Fatalf("locality mismatch error = %v", err) + } +} + +func TestValidateLocalityLinknameCycle(t *testing.T) { + prog := NewProgram(nil) + prog.SetLinkname("example.com/p.First", "example.com/p.Second") + prog.SetLinkname("example.com/p.Second", "example.com/p.First") + prog.SetLocalityInfo("example.com/p.First", LocalityInfo{Locality: ThreadLocal}) + if err := prog.ValidateLocalities("example.com/p"); err == nil || !strings.Contains(err.Error(), "linkname cycle") { + t.Fatalf("linkname cycle error = %v", err) + } +} + +func TestValidateLocalityAllowsSelfLinkname(t *testing.T) { + prog := NewProgram(nil) + name := "example.com/p.Value" + prog.SetLinkname(name, name) + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) + if err := prog.ValidateLocalities("example.com/p"); err != nil { + t.Fatal(err) + } + if _, got, ok, err := prog.ResolveLocality(name); err != nil || !ok || got.Locality != ThreadLocal { + t.Fatalf("ResolveLocality(%q) = %+v, %v", name, got, ok) + } +} diff --git a/ssa/package.go b/ssa/package.go index 8282a676d6..af98cf2c5f 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -23,6 +23,7 @@ import ( "log" "runtime" "strconv" + "sync" "unsafe" "github.com/goplus/llgo/internal/env" @@ -224,7 +225,9 @@ type aProgram struct { printfTy *types.Signature paramObjPtr_ *types.Var - linkname map[string]string // pkgPath.nameInPkg => linkname + linknameMu sync.RWMutex + linkname map[string]string // pkgPath.nameInPkg => linkname + localities *localityInfos noInterface map[string]none // pkgPath.T.method or pkgPath.(*T).method abiSymbol map[string]*AbiSymbol // abi symbol name => AbiSymbol @@ -313,7 +316,8 @@ func NewProgram(target *Target) Program { ctx: ctx, gocvt: newGoTypes(), target: target, td: td, tm: tm, is32Bits: is32Bits, ptrSize: td.PointerSize(), named: make(map[string]Type), fnnamed: make(map[string]int), - linkname: make(map[string]string), noInterface: make(map[string]none), abiSymbol: make(map[string]*AbiSymbol), + linkname: make(map[string]string), localities: newLocalityInfos(), + noInterface: make(map[string]none), abiSymbol: make(map[string]*AbiSymbol), debugInfoOptimized: target.effectiveOptLevel() != optlevel.O0, } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) @@ -395,11 +399,15 @@ func (p Program) SetTypeBackground(fullName string, bg Background) { } func (p Program) SetLinkname(name, link string) { + p.linknameMu.Lock() p.linkname[name] = link + p.linknameMu.Unlock() } func (p Program) Linkname(name string) (link string, ok bool) { + p.linknameMu.RLock() link, ok = p.linkname[name] + p.linknameMu.RUnlock() return } @@ -856,6 +864,11 @@ func (p Package) rtFunc(fnName string) Expr { return p.NewFunc(name, sig, InGo).Expr } +// RuntimeFunc returns a declaration for a function in LLGo's internal runtime. +func (p Package) RuntimeFunc(fnName string) Expr { + return p.rtFunc(fnName) +} + func (p Package) cFunc(fullName string, sig *types.Signature) Expr { return p.NewFunc(fullName, sig, InC).Expr } diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index c45e71035c..33d63e2ffd 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -1896,6 +1896,25 @@ source_filename = "foo/bar" `) } +func TestThreadLocalVar(t *testing.T) { + prog := NewProgram(nil) + pkg := prog.NewPackage("bar", "foo/bar") + a := pkg.NewThreadLocalVar("a", types.NewPointer(types.Typ[types.Int]), InGo) + if got := pkg.NewThreadLocalVar("a", types.NewPointer(types.Typ[types.Int]), InGo); got != a { + t.Fatal("NewThreadLocalVar(a) did not reuse the existing global") + } + a.InitNil() + empty := types.NewStruct(nil, nil) + z := pkg.NewThreadLocalVar("z", types.NewPointer(empty), InGo) + z.InitNil() + assertPkg(t, pkg, `; ModuleID = 'foo/bar' +source_filename = "foo/bar" + +@a = thread_local global i64 0, align 8 +@z = thread_local global {} zeroinitializer, align 1 +`) +} + func TestConst(t *testing.T) { prog := NewProgram(nil) pkg := prog.NewPackage("bar", "foo/bar") @@ -2675,7 +2694,7 @@ func TestRtFuncResolvesLinkname(t *testing.T) { return name }) - if got := pkg.rtFunc("Sigsetjmp").impl.Name(); got != "sigsetjmp" { + if got := pkg.RuntimeFunc("Sigsetjmp").impl.Name(); got != "sigsetjmp" { t.Fatalf("rtFunc linkname = %q, want %q", got, "sigsetjmp") } } diff --git a/ssa/type.go b/ssa/type.go index f90f8de380..89e309194c 100644 --- a/ssa/type.go +++ b/ssa/type.go @@ -194,6 +194,11 @@ func (p Program) SizeOf(typ Type, n ...int64) uint64 { return size } +// AlignOf returns the ABI alignment of typ for the current target. +func (p Program) AlignOf(typ Type) uint64 { + return uint64(p.td.ABITypeAlignment(typ.ll)) +} + // OffsetOf returns the offset of a field in a struct. func (p Program) OffsetOf(typ Type, i int) uint64 { return p.td.ElementOffset(typ.ll, i) From 08fca6f8429fba8a1d1b305af874962b1a6e19d1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jul 2026 19:09:13 +0800 Subject: [PATCH 04/20] build: key local context mode in package cache --- internal/build/collect.go | 1 + internal/build/collect_test.go | 73 ++++++++++++++++++++++++++++++++++ internal/build/fingerprint.go | 29 +++++++------- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/internal/build/collect.go b/internal/build/collect.go index f482762fb7..c01a813a26 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -111,6 +111,7 @@ func (c *context) collectCommonInputs(m *manifestBuilder) { m.common.GoGlobalDCE = c.buildConf.goGlobalDCEEnabled() m.common.EmitDWARF = shouldEmitDebugInfo(c.buildConf, &c.crossCompile) m.common.PCLNMode = effectivePCLNMode(c.buildConf).String() + m.common.LocalContext = c.prog != nil && c.prog.NeedsLocalContext() // Compiler configuration if c.crossCompile.CC != "" { diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index 752205d083..4103b1ec0d 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -30,6 +30,7 @@ import ( "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/lto" "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" gopackages "golang.org/x/tools/go/packages" ) @@ -359,6 +360,78 @@ func TestCollectFingerprintCanonicalizesPCLNEnvironment(t *testing.T) { } } +func TestCollectFingerprintLocalContextMode(t *testing.T) { + td := t.TempDir() + goFile := filepath.Join(td, "state.go") + if err := os.WriteFile(goFile, []byte("package state"), 0644); err != nil { + t.Fatal(err) + } + newPackage := func() *aPackage { + return &aPackage{Package: &packages.Package{ + ID: "example.com/state", + PkgPath: "example.com/state", + GoFiles: []string{goFile}, + }} + } + newContext := func(prog llssa.Program) *context { + return &context{ + conf: &packages.Config{}, + prog: prog, + buildConf: &Config{Goos: "linux", Goarch: "amd64"}, + crossCompile: crosscompile.Export{LLVMTarget: "x86_64-unknown-linux"}, + } + } + fingerprint := func(prog llssa.Program) (*aPackage, manifestData) { + pkg := newPackage() + if err := newContext(prog).collectFingerprint(pkg); err != nil { + t.Fatal(err) + } + data, err := decodeManifest(pkg.Manifest) + if err != nil { + t.Fatal(err) + } + return pkg, data + } + + plain, plainManifest := fingerprint(llssa.NewProgram(nil)) + nativeProg := llssa.NewProgram(nil) + nativeProg.SetLocalityInfo("example.com/state.value", llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + nativeProg.SetLocalStorage("example.com/state.value", llssa.LocalStorageNativeTLS) + native, nativeManifest := fingerprint(nativeProg) + contextProg := llssa.NewProgram(nil) + contextProg.SetLocalityInfo("example.com/state.value", llssa.LocalityInfo{Locality: llssa.GoroutineLocal}) + contextProg.SetLocalStorage("example.com/state.value", llssa.LocalStoragePackage) + withContext, contextManifest := fingerprint(contextProg) + initializedProg := llssa.NewProgram(nil) + initializedProg.SetLocalityInfo("example.com/state.value", llssa.LocalityInfo{ + Locality: llssa.ThreadLocal, + HasInitializer: true, + InitFunc: "example.com/state.__llgo_local_init_0", + InitOrder: 1, + }) + initializedProg.SetLocalStorage("example.com/state.value", llssa.LocalStorageNativeTLS) + initialized, initializedManifest := fingerprint(initializedProg) + + if plain.Fingerprint != native.Fingerprint { + t.Fatal("native TLS changed the package cache fingerprint") + } + if withContext.Fingerprint == plain.Fingerprint { + t.Fatal("local-context and plain builds shared a package cache fingerprint") + } + if initialized.Fingerprint == plain.Fingerprint { + t.Fatal("initialized native TLS and plain builds shared a package cache fingerprint") + } + if plainManifest.Common.LocalContext || nativeManifest.Common.LocalContext { + t.Fatal("plain or native-TLS manifest enabled the local context") + } + if !contextManifest.Common.LocalContext { + t.Fatal("context-backed locality was not recorded in the manifest") + } + if !initializedManifest.Common.LocalContext { + t.Fatal("native TLS initializer failure storage was not recorded in the manifest") + } +} + func TestDevLTOGlobalDCECollectFingerprint(t *testing.T) { td := t.TempDir() diff --git a/internal/build/fingerprint.go b/internal/build/fingerprint.go index aba29f23c1..82eeeda747 100644 --- a/internal/build/fingerprint.go +++ b/internal/build/fingerprint.go @@ -112,24 +112,25 @@ func (s *envSection) empty() bool { } type commonSection struct { - AbiMode string `yaml:"ABI_MODE,omitempty"` - BuildTags []string `yaml:"BUILD_TAGS,omitempty"` - Target string `yaml:"TARGET,omitempty"` - TargetABI string `yaml:"TARGET_ABI,omitempty"` - GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` - EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` - PCLNMode string `yaml:"PCLN_MODE,omitempty"` - CC string `yaml:"CC,omitempty"` - CCFlags []string `yaml:"CCFLAGS,omitempty"` - CFlags []string `yaml:"CFLAGS,omitempty"` - LDFlags []string `yaml:"LDFLAGS,omitempty"` - Linker string `yaml:"LINKER,omitempty"` - ExtraFiles []fileDigest `yaml:"EXTRA_FILES,omitempty"` + AbiMode string `yaml:"ABI_MODE,omitempty"` + BuildTags []string `yaml:"BUILD_TAGS,omitempty"` + Target string `yaml:"TARGET,omitempty"` + TargetABI string `yaml:"TARGET_ABI,omitempty"` + GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` + EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` + PCLNMode string `yaml:"PCLN_MODE,omitempty"` + LocalContext bool `yaml:"LOCAL_CONTEXT,omitempty"` + CC string `yaml:"CC,omitempty"` + CCFlags []string `yaml:"CCFLAGS,omitempty"` + CFlags []string `yaml:"CFLAGS,omitempty"` + LDFlags []string `yaml:"LDFLAGS,omitempty"` + Linker string `yaml:"LINKER,omitempty"` + ExtraFiles []fileDigest `yaml:"EXTRA_FILES,omitempty"` } func (s *commonSection) empty() bool { return s.AbiMode == "" && len(s.BuildTags) == 0 && s.Target == "" && s.TargetABI == "" && - !s.GoGlobalDCE && !s.EmitDWARF && s.PCLNMode == "" && s.CC == "" && len(s.CCFlags) == 0 && len(s.CFlags) == 0 && len(s.LDFlags) == 0 && + !s.GoGlobalDCE && !s.EmitDWARF && s.PCLNMode == "" && !s.LocalContext && s.CC == "" && len(s.CCFlags) == 0 && len(s.CFlags) == 0 && len(s.LDFlags) == 0 && s.Linker == "" && len(s.ExtraFiles) == 0 } From 9aded28c5b5ac7b595d8b41575a84c2da8a088e4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jul 2026 19:09:37 +0800 Subject: [PATCH 05/20] test: cover TLS and GLS runtime behavior --- cl/_testgo/localitycodegen/in.go | 74 +++ test/llgoext/locality_test.go | 544 ++++++++++++++++++ test/llgoext/testdata/localitybench/bench.go | 29 + .../testdata/localityfailure/failure.go | 57 ++ test/llgoext/testdata/localityscope/scope.go | 66 +++ 5 files changed, 770 insertions(+) create mode 100644 cl/_testgo/localitycodegen/in.go create mode 100644 test/llgoext/locality_test.go create mode 100644 test/llgoext/testdata/localitybench/bench.go create mode 100644 test/llgoext/testdata/localityfailure/failure.go create mode 100644 test/llgoext/testdata/localityscope/scope.go diff --git a/cl/_testgo/localitycodegen/in.go b/cl/_testgo/localitycodegen/in.go new file mode 100644 index 0000000000..8166bbfc50 --- /dev/null +++ b/cl/_testgo/localitycodegen/in.go @@ -0,0 +1,74 @@ +// LITTEST +package main + +// CHECK-DAG: @"{{.*}}localitycodegen.Scalar" = thread_local global i64 0 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_local_key" = global i8 0 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$guard" = thread_local global i8 0 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$failure" = global i8 0 +// CHECK-NOT: RegisterLocalRoot +// CHECK-NOT: localitycodegen.Pointer" = thread_local +// CHECK-NOT: localitycodegen.Initialized" = thread_local + +// CHECK-LABEL: define ptr @"{{.*}}localitycodegen.__llgo_local_block"() +// CHECK: call ptr @"{{.*}}runtime.LocalPackage"(ptr @"{{.*}}localitycodegen.__llgo_local_key", i64 16, i64 8) +// CHECK: ret ptr + +// CHECK-LABEL: define void @"{{.*}}localitycodegen.__llgo_tls_init"() +// CHECK: call void @"{{.*}}localitycodegen.__llgo_local_init_0"() + +// CHECK-LABEL: define void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() +// CHECK: load i8, ptr +// CHECK: call void @"{{.*}}runtime.EnsureLocalInitializer"(ptr @"{{.*}}localitycodegen.__llgo_tls_init$guard", ptr @"{{.*}}localitycodegen.__llgo_tls_init$failure" + +// CHECK-LABEL: define ptr @{{"?ExportedLocality"?}}() +// CHECK: call i64 @"{{.*}}EnterLocalContext" +// CHECK: call ptr @"{{.*}}localitycodegen.__llgo_local_block"() +// CHECK: call void @"{{.*}}LeaveLocalContext" +// CHECK: ret ptr + +// CHECK-LABEL: define void @"{{.*}}localitycodegen.init"() +// CHECK: store i8 2, ptr +// CHECK: call ptr @"{{.*}}localitycodegen.newPointer"() +// CHECK: call void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() +// CHECK: call ptr @"{{.*}}localitycodegen.__llgo_local_block"() + +// CHECK-LABEL: define { i64, ptr, ptr } @"{{.*}}localitycodegen.values"() +// CHECK: call void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() +// CHECK: load i64, ptr @"{{.*}}localitycodegen.Scalar" +// CHECK: call ptr @"{{.*}}localitycodegen.__llgo_local_block"() +// CHECK: load ptr, ptr +// CHECK: load ptr, ptr + +// CHECK-LABEL: define ptr @"{{.*}}localitycodegen._llgo_routine$1"(ptr %0) +// CHECK: alloca %"{{.*}}LocalContext", align 8 +// CHECK: call i64 @"{{.*}}EnterLocalContext" +// CHECK: call void @"{{.*}}LeaveLocalContext" + +var backing int + +func newPointer() *int { + return &backing +} + +//llgo:tls +var Scalar int + +//llgo:gls +var Pointer *int + +//llgo:tls +var Initialized = newPointer() + +func values() (int, *int, *int) { + return Scalar, Pointer, Initialized +} + +//export ExportedLocality +func ExportedLocality() *int { + return Pointer +} + +func main() { + _, _, _ = values() + go values() +} diff --git a/test/llgoext/locality_test.go b/test/llgoext/locality_test.go new file mode 100644 index 0000000000..9dff66521d --- /dev/null +++ b/test/llgoext/locality_test.go @@ -0,0 +1,544 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "runtime" + "sync/atomic" + "testing" + + "github.com/goplus/llgo/test/llgoext/testdata/localitybench" + "github.com/goplus/llgo/test/llgoext/testdata/localityfailure" + "github.com/goplus/llgo/test/llgoext/testdata/localityscope" +) + +var initializerSequence int + +func nextLocalValue(base int) int { + initializerSequence++ + return base + initializerSequence +} + +//llgo:tls +var tlsCounter int + +//llgo:gls +var glsCounter int + +//llgo:tls +var initializedTLS = nextLocalValue(100) + +//llgo:gls +var initializedGLS = nextLocalValue(200) + +type localitySnapshot struct { + tlsCounter int + glsCounter int + initializedTLS int + initializedGLS int +} + +func snapshotLocality() localitySnapshot { + return localitySnapshot{ + tlsCounter: tlsCounter, + glsCounter: glsCounter, + initializedTLS: initializedTLS, + initializedGLS: initializedGLS, + } +} + +func TestTLSAndGLSIsolation(t *testing.T) { + parentInitial := snapshotLocality() + if parentInitial.initializedTLS <= 100 || parentInitial.initializedGLS <= 200 { + t.Fatalf("parent initializers did not run: %+v", parentInitial) + } + + tlsCounter = 11 + glsCounter = 22 + parentSet := snapshotLocality() + + type childResult struct { + first localitySnapshot + again localitySnapshot + set localitySnapshot + } + done := make(chan childResult) + go func() { + first := snapshotLocality() + again := snapshotLocality() + tlsCounter = 31 + glsCounter = 32 + done <- childResult{first: first, again: again, set: snapshotLocality()} + }() + child := <-done + + if child.first.tlsCounter != 0 || child.first.glsCounter != 0 { + t.Fatalf("child inherited parent local values: %+v", child.first) + } + if child.first != child.again { + t.Fatalf("child initializer ran more than once: first=%+v again=%+v", child.first, child.again) + } + if child.first.initializedTLS == parentInitial.initializedTLS || child.first.initializedGLS == parentInitial.initializedGLS { + t.Fatalf("child reused parent initialization: parent=%+v child=%+v", parentInitial, child.first) + } + if child.set.tlsCounter != 31 || child.set.glsCounter != 32 { + t.Fatalf("child local writes were lost: %+v", child.set) + } + if got := snapshotLocality(); got != parentSet { + t.Fatalf("child changed parent local values: got=%+v want=%+v", got, parentSet) + } +} + +func TestPanickingInitializerIsSticky(t *testing.T) { + for attempt := 0; attempt < 2; attempt++ { + var recovered any + func() { + defer func() { recovered = recover() }() + _ = localityfailure.Value() + }() + if recovered == nil { + t.Fatalf("attempt %d did not re-panic", attempt) + } + if recovered != localityfailure.Failure { + t.Fatalf("attempt %d panic = %v, want %v", attempt, recovered, localityfailure.Failure) + } + } + if attempts := localityfailure.Attempts(); attempts != 2 { + t.Fatalf("initializer attempts = %d, want 2", attempts) + } +} + +func TestNilPanickingInitializerIsSticky(t *testing.T) { + for attempt := 0; attempt < 2; attempt++ { + var recovered any + func() { + defer func() { recovered = recover() }() + _ = localityfailure.NilValue() + }() + if recovered == nil { + t.Fatalf("attempt %d did not re-panic", attempt) + } + } + if attempts := localityfailure.NilAttempts(); attempts != 2 { + t.Fatalf("initializer attempts = %d, want 2", attempts) + } +} + +type recursiveInitializer interface { + value() int +} + +type recursiveSourceValue struct{} + +func (recursiveSourceValue) value() int { + return recursiveValue + 1 +} + +var recursiveSource recursiveInitializer = recursiveSourceValue{} + +//llgo:gls +var recursiveValue = recursiveSource.value() + +func TestRecursiveInitializerObservesPartialValue(t *testing.T) { + if recursiveValue != 1 { + t.Fatalf("recursive initializer value = %d, want 1", recursiveValue) + } + if recursiveValue != 1 { + t.Fatal("recursive initializer ran more than once") + } +} + +var lateInitializerAttempts int + +func nextLateValue() int { + lateInitializerAttempts++ + return 100 + lateInitializerAttempts +} + +// zLate sorts after the package init function in SSA member order. +// +//llgo:tls +var zLate = nextLateValue() + +func TestLateSortedInitializer(t *testing.T) { + value, attempts := zLate, lateInitializerAttempts + if value != 100+attempts { + t.Fatalf("late initializer value = %d, attempts = %d", value, attempts) + } + if again := zLate; again != value || lateInitializerAttempts != attempts { + t.Fatalf("late initializer repeated: value=%d/%d attempts=%d/%d", again, value, lateInitializerAttempts, attempts) + } +} + +type rootedValue struct { + value int + pad [256]byte +} + +func newRootedValue() *rootedValue { + return &rootedValue{value: 73} +} + +//llgo:gls +var rootedPointer = newRootedValue() + +//go:noinline +func touchRootedPointer() { + if rootedPointer == nil || rootedPointer.value != 73 { + panic("invalid rooted pointer") + } +} + +//go:noinline +func collectWithoutLocalPointer() { + for i := 0; i < 3; i++ { + _ = make([]byte, 1<<20) + runtime.GC() + } +} + +//go:noinline +func readRootedPointer() int { + return rootedPointer.value +} + +func TestLocalPointerIsGCRoot(t *testing.T) { + touchRootedPointer() + collectWithoutLocalPointer() + if got := readRootedPointer(); got != 73 { + t.Fatalf("rooted pointer value = %d, want 73", got) + } +} + +func TestLocalContextCleanupAfterThreadExit(t *testing.T) { + for i := 0; i < 16; i++ { + exited := make(chan struct{}) + go func() { + defer close(exited) + touchRootedPointer() + }() + <-exited + } + runtime.GC() + runtime.GC() +} + +//llgo:gls +var atomicGLS int64 + +func TestLocalAddressAndAtomicSemantics(t *testing.T) { + first := &atomicGLS + second := &atomicGLS + if first != second { + t.Fatalf("repeated GLS address changed: %p != %p", first, second) + } + atomic.StoreInt64(first, 7) + if got := atomic.AddInt64(&atomicGLS, 5); got != 12 { + t.Fatalf("atomic GLS value = %d, want 12", got) + } +} + +func localClosure() func() int { + glsCounter = 40 + return func() int { + glsCounter++ + return glsCounter + } +} + +func TestClosureUsesInvocationContext(t *testing.T) { + closure := localClosure() + done := make(chan int) + go func() { + done <- closure() + }() + if got := <-done; got != 1 { + t.Fatalf("closure used creator GLS value: got %d, want 1", got) + } + if glsCounter != 40 { + t.Fatalf("child closure changed parent GLS value: %d", glsCounter) + } +} + +type escapedBlockValue struct { + pointer *int + value int +} + +//llgo:gls +var escapedBlock escapedBlockValue + +func TestEscapedPackageBlockAddressSurvivesOwnerExit(t *testing.T) { + addresses := make(chan *escapedBlockValue) + exited := make(chan struct{}) + go func() { + value := 71 + escapedBlock = escapedBlockValue{pointer: &value, value: 71} + addresses <- &escapedBlock + close(exited) + }() + address := <-addresses + <-exited + runtime.GC() + runtime.GC() + if address.value != 71 || address.pointer == nil || *address.pointer != 71 { + t.Fatalf("escaped package block after owner exit = %+v", address) + } + done := make(chan bool) + go func(block *escapedBlockValue) { + block.value = 72 + *block.pointer = 72 + done <- true + }(address) + <-done + if address.value != 72 || *address.pointer != 72 { + t.Fatalf("escaped package block after cross-goroutine write = %+v", address) + } +} + +func TestEscapedPackageBlockAddressSurvivesGoexit(t *testing.T) { + addresses := make(chan *escapedBlockValue) + exited := make(chan struct{}) + go func() { + value := 81 + escapedBlock = escapedBlockValue{pointer: &value, value: 81} + address := &escapedBlock + defer close(exited) + defer func() { + addresses <- address + }() + runtime.Goexit() + }() + address := <-addresses + <-exited + runtime.GC() + if address.value != 81 || address.pointer == nil || *address.pointer != 81 { + t.Fatalf("escaped package block after Goexit = %+v", address) + } +} + +//llgo:gls +var zeroSizedGLS struct{} + +func TestZeroSizedNativeLocalAddressIsStable(t *testing.T) { + first := &zeroSizedGLS + second := &zeroSizedGLS + if first == nil || first != second { + t.Fatalf("zero-sized GLS address changed: %p != %p", first, second) + } +} + +func TestInitializerScopeRunsOncePerPackageKind(t *testing.T) { + firstBefore := localityscope.FirstCalls() + secondBefore := localityscope.SecondCalls() + type result struct { + firstValue int + firstCalls int + secondCalls int + firstCallsAgain int + secondValue int + secondCallsAfter int + } + done := make(chan result) + go func() { + firstValue := localityscope.First + firstCalls := localityscope.FirstCalls() + secondCalls := localityscope.SecondCalls() + _ = localityscope.First + firstCallsAgain := localityscope.FirstCalls() + secondValue := localityscope.Second + done <- result{ + firstValue: firstValue, + firstCalls: firstCalls, + secondCalls: secondCalls, + firstCallsAgain: firstCallsAgain, + secondValue: secondValue, + secondCallsAfter: localityscope.SecondCalls(), + } + }() + got := <-done + if got.firstValue == 0 || got.secondValue == 0 { + t.Fatalf("lazy initializer values = %+v", got) + } + if got.firstCalls != firstBefore+1 || got.firstCallsAgain != got.firstCalls { + t.Fatalf("first initializer calls = %+v, baseline %d", got, firstBefore) + } + if got.secondCalls != secondBefore+1 || got.secondCallsAfter != got.secondCalls { + t.Fatalf("package GLS initializers did not run together once: %+v, baseline %d", got, secondBefore) + } +} + +func TestMultiValueInitializerUsesOneGroup(t *testing.T) { + before := localityscope.PairCalls() + type result struct { + first, second int + afterFirst int + afterSecond int + } + done := make(chan result) + go func() { + first := localityscope.PairFirst + afterFirst := localityscope.PairCalls() + second := localityscope.PairSecond + done <- result{first, second, afterFirst, localityscope.PairCalls()} + }() + got := <-done + if got.first == 0 || got.second == 0 || got.afterFirst != before+1 || got.afterSecond != got.afterFirst { + t.Fatalf("multi-value initializer group = %+v, baseline %d", got, before) + } +} + +func TestCrossPackageMixedInitializerGroup(t *testing.T) { + before := localityscope.MixedCalls() + type result struct { + scalar int + pointer *int + addressStable bool + calls int + } + done := make(chan result) + go func() { + scalar := localityscope.MixedScalar + address := &localityscope.MixedScalar + done <- result{ + scalar: scalar, + pointer: localityscope.MixedPointer, + addressStable: address == &localityscope.MixedScalar, + calls: localityscope.MixedCalls(), + } + }() + got := <-done + if got.scalar == 0 || got.pointer == nil || !got.addressStable || got.calls != before+1 { + t.Fatalf("cross-package mixed initializer = %+v, baseline %d", got, before) + } +} + +var benchmarkOrdinary int + +//llgo:tls +var benchmarkTLS int + +//llgo:gls +var benchmarkGLS int + +var benchmarkSink int + +//go:noinline +func bumpOrdinaryGlobal() int { + benchmarkOrdinary++ + return benchmarkOrdinary +} + +//go:noinline +func bumpNativeTLS() int { + benchmarkTLS++ + return benchmarkTLS +} + +//go:noinline +func bumpNativeGLS() int { + benchmarkGLS++ + return benchmarkGLS +} + +type benchmarkPackageValue struct { + pointer *int + value int +} + +//llgo:tls +var benchmarkTLSPackage benchmarkPackageValue + +//llgo:gls +var benchmarkGLSPackage benchmarkPackageValue + +//go:noinline +func bumpTLSPackageBlock() int { + benchmarkTLSPackage.value++ + return benchmarkTLSPackage.value +} + +//go:noinline +func bumpGLSPackageBlock() int { + benchmarkGLSPackage.value++ + return benchmarkGLSPackage.value +} + +func BenchmarkOrdinaryGlobal(b *testing.B) { + value := 0 + for i := 0; i < b.N; i++ { + value += bumpOrdinaryGlobal() + } + benchmarkSink = value +} + +func BenchmarkNativeTLS(b *testing.B) { + value := 0 + for i := 0; i < b.N; i++ { + value += bumpNativeTLS() + } + benchmarkSink = value +} + +func BenchmarkNativeGLS(b *testing.B) { + value := 0 + for i := 0; i < b.N; i++ { + value += bumpNativeGLS() + } + benchmarkSink = value +} + +func BenchmarkTLSPackageBlock(b *testing.B) { + benchmarkTLSPackage.pointer = &benchmarkSink + b.ResetTimer() + value := 0 + for i := 0; i < b.N; i++ { + value += bumpTLSPackageBlock() + } + benchmarkSink = value +} + +func BenchmarkGLSPackageBlock(b *testing.B) { + benchmarkGLSPackage.pointer = &benchmarkSink + b.ResetTimer() + value := 0 + for i := 0; i < b.N; i++ { + value += bumpGLSPackageBlock() + } + benchmarkSink = value +} + +func BenchmarkGoroutineEntry(b *testing.B) { + for i := 0; i < b.N; i++ { + done := make(chan struct{}) + go func() { close(done) }() + <-done + } +} + +func BenchmarkGoroutinePackageBlockFirstTouch(b *testing.B) { + for i := 0; i < b.N; i++ { + done := make(chan struct{}) + go func() { + localitybench.Touch() + close(done) + }() + <-done + } +} diff --git a/test/llgoext/testdata/localitybench/bench.go b/test/llgoext/testdata/localitybench/bench.go new file mode 100644 index 0000000000..b9a1426252 --- /dev/null +++ b/test/llgoext/testdata/localitybench/bench.go @@ -0,0 +1,29 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localitybench + +var backing int + +//llgo:gls +var pointer *int + +//go:noinline +func Touch() { + pointer = &backing +} diff --git a/test/llgoext/testdata/localityfailure/failure.go b/test/llgoext/testdata/localityfailure/failure.go new file mode 100644 index 0000000000..c42990ebd8 --- /dev/null +++ b/test/llgoext/testdata/localityfailure/failure.go @@ -0,0 +1,57 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localityfailure + +const Failure = "locality initializer failed" + +var attempts int + +func initialize() int { + attempts++ + if attempts > 1 { + panic(Failure) + } + return attempts +} + +//llgo:tls +var value = initialize() + +//go:noinline +func Value() int { return value } + +func Attempts() int { return attempts } + +var nilAttempts int + +func initializeNil() int { + nilAttempts++ + if nilAttempts > 1 { + panic(nil) + } + return nilAttempts +} + +//llgo:gls +var nilValue = initializeNil() + +//go:noinline +func NilValue() int { return nilValue } + +func NilAttempts() int { return nilAttempts } diff --git a/test/llgoext/testdata/localityscope/scope.go b/test/llgoext/testdata/localityscope/scope.go new file mode 100644 index 0000000000..dce9828dca --- /dev/null +++ b/test/llgoext/testdata/localityscope/scope.go @@ -0,0 +1,66 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localityscope + +var firstCalls int +var secondCalls int + +func initFirst() int { + firstCalls++ + return 100 + firstCalls +} + +func initSecond() int { + secondCalls++ + return 200 + secondCalls +} + +//llgo:gls +var First = initFirst() + +//llgo:gls +var Second = initSecond() + +func FirstCalls() int { return firstCalls } +func SecondCalls() int { return secondCalls } + +var pairCalls int + +func initPair() (int, int) { + pairCalls++ + return 300 + pairCalls, 400 + pairCalls +} + +//llgo:gls +var PairFirst, PairSecond = initPair() + +func PairCalls() int { return pairCalls } + +var mixedCalls int +var mixedBacking = 500 + +func initMixed() (int, *int) { + mixedCalls++ + return 600 + mixedCalls, &mixedBacking +} + +//llgo:tls +var MixedScalar, MixedPointer = initMixed() + +func MixedCalls() int { return mixedCalls } From ebac89175eda985b205bcf3cabfcd9fd7ccc8869 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 00:53:54 +0800 Subject: [PATCH 06/20] runtime: split local package fast path --- runtime/internal/runtime/local_context.go | 14 +++++-- test/llgoext/runtime_g_bench_test.go | 45 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 test/llgoext/runtime_g_bench_test.go diff --git a/runtime/internal/runtime/local_context.go b/runtime/internal/runtime/local_context.go index 95f6226f8f..54ae9e408c 100644 --- a/runtime/internal/runtime/local_context.go +++ b/runtime/internal/runtime/local_context.go @@ -83,6 +83,17 @@ func releaseLocalBlocks(ctx *LocalContext) { // head fast path; other accesses move the matching block to the front. func LocalPackage(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) + if ctx != nil { + first := ctx.blocks + if first != nil && first.key == key && align != 0 && align&(align-1) == 0 { + return localBlockData(first, align) + } + } + return localPackageSlow(ctx, key, size, align) +} + +//go:noinline +func localPackageSlow(ctx *LocalContext, key unsafe.Pointer, size, align uintptr) unsafe.Pointer { if ctx == nil { panic("runtime: local variable accessed outside a Go entry context") } @@ -93,9 +104,6 @@ func LocalPackage(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { panic("runtime: invalid local package alignment") } first := ctx.blocks - if first != nil && first.key == key { - return localBlockData(first, align) - } var previous *localBlock for block := first; block != nil; block = block.next { if block.key == key { diff --git a/test/llgoext/runtime_g_bench_test.go b/test/llgoext/runtime_g_bench_test.go new file mode 100644 index 0000000000..927a75d3b9 --- /dev/null +++ b/test/llgoext/runtime_g_bench_test.go @@ -0,0 +1,45 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "testing" + "unsafe" +) + +var runtimeDeferSink unsafe.Pointer + +//go:linkname runtimeGetThreadDefer github.com/goplus/llgo/runtime/internal/runtime.GetThreadDefer +func runtimeGetThreadDefer() unsafe.Pointer + +func BenchmarkRuntimeGetThreadDefer(b *testing.B) { + for i := 0; i < b.N; i++ { + runtimeDeferSink = runtimeGetThreadDefer() + } +} + +func BenchmarkRuntimeGoroutineEntryWithDefer(b *testing.B) { + for i := 0; i < b.N; i++ { + done := make(chan struct{}) + go func() { + defer close(done) + }() + <-done + } +} From bbfea02bc5961d586758167129cac7917fd05705 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 04:17:40 +0800 Subject: [PATCH 07/20] runtime: return local package data from hot block --- runtime/internal/runtime/local_context.go | 60 +++++++++++++---------- test/llgoext/locality_test.go | 18 +++++++ 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/runtime/internal/runtime/local_context.go b/runtime/internal/runtime/local_context.go index 54ae9e408c..9ce43dcdac 100644 --- a/runtime/internal/runtime/local_context.go +++ b/runtime/internal/runtime/local_context.go @@ -24,11 +24,15 @@ import "unsafe" // one-thread-per-goroutine backend maps both logical locality kinds to this one // physical package store. type LocalContext struct { - blocks *localBlock + // blocks points at the payload of the most recently used package block. + // Keeping the aligned payload at the head makes the common lookup return it + // directly; the block header is stored immediately before the payload. + blocks unsafe.Pointer } type localBlock struct { - next *localBlock + // next points at the next block's payload, not its header. + next unsafe.Pointer key unsafe.Pointer } @@ -67,14 +71,15 @@ func leaveCurrentLocalContext() { } func releaseLocalBlocks(ctx *LocalContext) { - block := ctx.blocks + data := ctx.blocks ctx.blocks = nil - for block != nil { + for data != nil { + block := localBlockHeader(data) next := block.next // Do not free block here: an address of a local variable may outlive its // owner. Breaking the links lets the GC retain only escaped blocks. block.next = nil - block = next + data = next } } @@ -84,9 +89,9 @@ func releaseLocalBlocks(ctx *LocalContext) { func LocalPackage(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) if ctx != nil { - first := ctx.blocks - if first != nil && first.key == key && align != 0 && align&(align-1) == 0 { - return localBlockData(first, align) + firstData := ctx.blocks + if firstData != nil && localBlockHeader(firstData).key == key { + return firstData } } return localPackageSlow(ctx, key, size, align) @@ -103,24 +108,27 @@ func localPackageSlow(ctx *LocalContext, key unsafe.Pointer, size, align uintptr if align == 0 || align&(align-1) != 0 { panic("runtime: invalid local package alignment") } - first := ctx.blocks + firstData := ctx.blocks var previous *localBlock - for block := first; block != nil; block = block.next { + for data := firstData; data != nil; { + block := localBlockHeader(data) + next := block.next if block.key == key { - previous.next = block.next - block.next = first - ctx.blocks = block - return localBlockData(block, align) + previous.next = next + block.next = firstData + ctx.blocks = data + return data } previous = block + data = next } - block := newLocalBlock(key, size, align) - block.next = first - ctx.blocks = block - return localBlockData(block, align) + data := newLocalBlock(key, size, align) + localBlockHeader(data).next = firstData + ctx.blocks = data + return data } -func newLocalBlock(key unsafe.Pointer, size, align uintptr) *localBlock { +func newLocalBlock(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { header := unsafe.Sizeof(localBlock{}) padding := align - 1 if size == 0 { @@ -129,16 +137,16 @@ func newLocalBlock(key unsafe.Pointer, size, align uintptr) *localBlock { if header > ^uintptr(0)-padding || header+padding > ^uintptr(0)-size { panic("runtime: local package size overflow") } - block := (*localBlock)(AllocZ(header + padding + size)) - if block == nil { + allocation := AllocZ(header + padding + size) + if allocation == nil { panic("runtime: failed to allocate local package") } + data := unsafe.Pointer((uintptr(allocation) + header + padding) &^ padding) + block := localBlockHeader(data) block.key = key - return block + return data } -func localBlockData(block *localBlock, align uintptr) unsafe.Pointer { - padding := align - 1 - data := (uintptr(unsafe.Pointer(block)) + unsafe.Sizeof(localBlock{}) + padding) &^ padding - return unsafe.Pointer(data) +func localBlockHeader(data unsafe.Pointer) *localBlock { + return (*localBlock)(unsafe.Pointer(uintptr(data) - unsafe.Sizeof(localBlock{}))) } diff --git a/test/llgoext/locality_test.go b/test/llgoext/locality_test.go index 9dff66521d..3d49367531 100644 --- a/test/llgoext/locality_test.go +++ b/test/llgoext/locality_test.go @@ -105,6 +105,24 @@ func TestTLSAndGLSIsolation(t *testing.T) { } } +func TestLocalPackageMoveToFront(t *testing.T) { + type result struct { + local int + imported int + } + done := make(chan result) + go func() { + glsCounter = 41 + localityscope.First = 51 + glsCounter++ + localityscope.First++ + done <- result{local: glsCounter, imported: localityscope.First} + }() + if got := <-done; got != (result{local: 42, imported: 52}) { + t.Fatalf("local package values after move-to-front = %+v", got) + } +} + func TestPanickingInitializerIsSticky(t *testing.T) { for attempt := 0; attempt < 2; attempt++ { var recovered any From 99f32a6662a59fd3fee852655c567f7b30870e7c Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 04:51:58 +0800 Subject: [PATCH 08/20] test: cover locality debug metadata lowering --- cl/locality_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cl/locality_test.go b/cl/locality_test.go index 081e8b35e7..05fcad5b81 100644 --- a/cl/locality_test.go +++ b/cl/locality_test.go @@ -181,6 +181,36 @@ func values() (int, *int, int, *int) { } } +func TestLocalityDebugInfoOnlyUsesFixedGlobals(t *testing.T) { + EnableDebug(true) + EnableDbgSyms(true) + defer EnableDebug(false) + defer EnableDbgSyms(false) + _, ir := compileLocalitySource(t, `package locality + +//llgo:tls +var Direct int + +//llgo:gls +var Pointer *int + +func values() (int, *int) { return Direct, Pointer } +`) + + direct := `@"example.com/locality.Direct" = thread_local global i64` + start := strings.Index(ir, direct) + if start < 0 { + t.Fatalf("native TLS global not found:\n%s", ir) + } + end := strings.IndexByte(ir[start:], '\n') + if end < 0 || !strings.Contains(ir[start:start+end], "!dbg") { + t.Fatalf("native TLS global has no debug metadata:\n%s", ir[start:]) + } + if strings.Contains(ir, `@"example.com/locality.Pointer" =`) { + t.Fatalf("package-local pointer was emitted as a fixed debug global:\n%s", ir) + } +} + func TestLocalityInitializersPreserveGoOrderPerKind(t *testing.T) { _, ir := compileLocalitySource(t, `package locality From 809726ce659cfb757da99b7c6a0333876595544f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 13:33:30 +0800 Subject: [PATCH 09/20] compiler: inline local package fast path --- cl/_testgo/localitycodegen/in.go | 9 ++ cl/locality_lower.go | 8 +- cl/locality_test.go | 54 +++++++++- ssa/local_context.go | 73 +++++++++++++- ssa/local_context_test.go | 100 +++++++++++++++++++ test/llgoext/locality_test.go | 41 ++++++++ test/llgoext/testdata/localitybench/bench.go | 28 ++++++ 7 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 ssa/local_context_test.go diff --git a/cl/_testgo/localitycodegen/in.go b/cl/_testgo/localitycodegen/in.go index 8166bbfc50..25927d2b47 100644 --- a/cl/_testgo/localitycodegen/in.go +++ b/cl/_testgo/localitycodegen/in.go @@ -5,11 +5,20 @@ package main // CHECK-DAG: @"{{.*}}localitycodegen.__llgo_local_key" = global i8 0 // CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$guard" = thread_local global i8 0 // CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$failure" = global i8 0 +// CHECK-DAG: @"{{.*}}runtime.currentLocalContext" = external thread_local global i64 // CHECK-NOT: RegisterLocalRoot // CHECK-NOT: localitycodegen.Pointer" = thread_local // CHECK-NOT: localitycodegen.Initialized" = thread_local // CHECK-LABEL: define ptr @"{{.*}}localitycodegen.__llgo_local_block"() +// CHECK: load i64, ptr @"{{.*}}runtime.currentLocalContext" +// CHECK: icmp ne i64 +// CHECK: load ptr, ptr +// CHECK: icmp ne ptr +// CHECK: sub i64 +// CHECK: load ptr, ptr +// CHECK: icmp eq ptr +// CHECK: ret ptr // CHECK: call ptr @"{{.*}}runtime.LocalPackage"(ptr @"{{.*}}localitycodegen.__llgo_local_key", i64 16, i64 8) // CHECK: ret ptr diff --git a/cl/locality_lower.go b/cl/locality_lower.go index 10b6cd4cee..7dbea7a612 100644 --- a/cl/locality_lower.go +++ b/cl/locality_lower.go @@ -261,15 +261,11 @@ func (p *context) buildLocalPackage(pkg llssa.Package, owner *localPackage, defi owner.blockFunc = pkg.NewFunc(localitylayout.BlockName(owner.plan.Path), noArgResultSignature(result), llssa.InGo) owner.blockFunc.Inline(llssa.AlwaysInline) if define && !owner.blockFunc.HasBody() { - b := owner.blockFunc.MakeBody(1) - raw := b.Call( - pkg.RuntimeFunc("LocalPackage"), - b.Convert(p.prog.VoidPtr(), key.Expr), + owner.blockFunc.BuildLocalPackageAccessor( + key.Expr, p.prog.IntVal(p.prog.SizeOf(owner.typ), p.prog.Uintptr()), p.prog.IntVal(p.prog.AlignOf(owner.typ), p.prog.Uintptr()), ) - b.Return(b.Convert(p.prog.Pointer(owner.typ), raw)) - b.EndBuild() } } for _, kind := range []locality.Kind{locality.Thread, locality.Goroutine} { diff --git a/cl/locality_test.go b/cl/locality_test.go index 05fcad5b81..694058bd90 100644 --- a/cl/locality_test.go +++ b/cl/locality_test.go @@ -16,6 +16,10 @@ import ( ) func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { + return compileLocalitySourceWithNativeAnchor(t, src, true) +} + +func compileLocalitySourceWithNativeAnchor(t *testing.T, src string, nativeAnchor bool) (llssa.Program, string) { t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "locality.go", src, parser.ParseComments) @@ -32,6 +36,11 @@ func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { prog := ssatest.NewProgramEx(t, nil, imp) prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) prog.SetRuntime(localityRuntimePackage()) + if nativeAnchor { + anchor := llssa.PkgRuntime + ".currentLocalContext" + prog.SetLocalityInfo(anchor, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLocalStorage(anchor, llssa.LocalStorageNativeTLS) + } if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { t.Fatal(err) } @@ -62,9 +71,17 @@ func newLocalityTypeInfo() *types.Info { func localityRuntimePackage() *types.Package { pkg := types.NewPackage(llssa.PkgRuntime, "runtime") + unsafePointer := types.Typ[types.UnsafePointer] + blocks := types.NewField(token.NoPos, pkg, "blocks", unsafePointer, false) localContextName := types.NewTypeName(token.NoPos, pkg, "LocalContext", nil) - localContext := types.NewNamed(localContextName, types.NewStruct(nil, nil), nil) + localContext := types.NewNamed(localContextName, types.NewStruct([]*types.Var{blocks}, nil), nil) pkg.Scope().Insert(localContextName) + next := types.NewField(token.NoPos, pkg, "next", unsafePointer, false) + key := types.NewField(token.NoPos, pkg, "key", unsafePointer, false) + localBlockName := types.NewTypeName(token.NoPos, pkg, "localBlock", nil) + types.NewNamed(localBlockName, types.NewStruct([]*types.Var{next, key}, nil), nil) + pkg.Scope().Insert(localBlockName) + pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, "currentLocalContext", types.Typ[types.Uintptr])) localPackageParams := types.NewTuple( types.NewParam(token.NoPos, pkg, "key", types.Typ[types.UnsafePointer]), @@ -94,6 +111,41 @@ func localityRuntimePackage() *types.Package { return pkg } +func TestLocalPackageAccessorUsesRuntimeAnchorStorage(t *testing.T) { + const src = `package locality + +//llgo:gls +var Pointer *int + +func value() *int { return Pointer } +` + for _, test := range []struct { + name string + nativeAnchor bool + declaration string + }{ + {"native TLS", true, "external thread_local global i64"}, + {"ordinary global", false, "external global i64"}, + } { + t.Run(test.name, func(t *testing.T) { + _, ir := compileLocalitySourceWithNativeAnchor(t, src, test.nativeAnchor) + anchor := `@"` + llssa.PkgRuntime + `.currentLocalContext" = ` + test.declaration + if !strings.Contains(ir, anchor) { + t.Fatalf("runtime context anchor declaration not found: %s\n%s", anchor, ir) + } + accessor := llvmFunction(t, ir, "example.com/locality.__llgo_local_block") + load := `load i64, ptr @"` + llssa.PkgRuntime + `.currentLocalContext"` + slow := `call ptr @"` + llssa.PkgRuntime + `.LocalPackage"` + if loadAt, slowAt := strings.Index(accessor, load), strings.Index(accessor, slow); loadAt < 0 || slowAt < 0 || loadAt >= slowAt { + t.Fatalf("accessor does not load the runtime anchor before its slow path:\n%s", accessor) + } + if got := strings.Count(accessor, "icmp "); got != 3 { + t.Fatalf("accessor comparisons = %d, want context/head/key checks:\n%s", got, accessor) + } + }) + } +} + func llvmFunction(t *testing.T, ir, name string) string { t.Helper() markerAt := strings.Index(ir, `@"`+name+`"(`) diff --git a/ssa/local_context.go b/ssa/local_context.go index 9739ff19cc..cec3e2878a 100644 --- a/ssa/local_context.go +++ b/ssa/local_context.go @@ -16,7 +16,16 @@ package ssa -import "go/types" +import ( + "go/token" + "go/types" +) + +const ( + runtimeCurrentLocalContext = "currentLocalContext" + runtimeLocalContext = "LocalContext" + runtimeLocalBlock = "localBlock" +) // EnterLocalContext creates the stack root used by TLS/GLS locality blocks and // installs it for the current outermost Go entry. previous is nonzero only for @@ -36,3 +45,65 @@ func (b Builder) EnterLocalContext() (ctx, previous Expr) { func (b Builder) LeaveLocalContext(ctx, previous Expr) { b.Call(b.Pkg.rtFunc("LeaveLocalContext"), ctx, previous) } + +// BuildLocalPackageAccessor builds an always-hot package block lookup around +// LocalPackage. Runtime Go types and locality metadata are the ABI: they supply +// the context/header layouts and whether the context anchor is native TLS or an +// ordinary global. The generated code therefore does not duplicate target +// selection or byte offsets. +func (p Function) BuildLocalPackageAccessor(key, size, align Expr) { + prog := p.Prog + runtimePkg := prog.runtime() + contextType := runtimePkg.Scope().Lookup(runtimeLocalContext).Type() + blockType := runtimePkg.Scope().Lookup(runtimeLocalBlock).Type() + _, contextBlocks, _ := types.LookupFieldOrMethod(contextType, true, runtimePkg, "blocks") + _, blockKey, _ := types.LookupFieldOrMethod(blockType, true, runtimePkg, "key") + + b := p.MakeBody(5) + checkHead := p.Block(1) + checkKey := p.Block(2) + hit := p.Block(3) + slow := p.Block(4) + key = b.Convert(prog.VoidPtr(), key) + + current := b.Load(p.Pkg.runtimeGlobal(runtimeCurrentLocalContext).Expr) + hasContext := b.BinOp(token.NEQ, current, prog.IntVal(0, prog.Uintptr())) + b.If(hasContext, checkHead, slow) + + b.SetBlock(checkHead) + context := b.Convert(prog.Pointer(prog.Type(contextType, InGo)), current) + head := b.Load(b.FieldAddr(context, contextBlocks[0])) + hasHead := b.BinOp(token.NEQ, head, prog.Nil(head.Type)) + b.If(hasHead, checkKey, slow) + + b.SetBlock(checkKey) + headerAddress := b.BinOp( + token.SUB, + b.Convert(prog.Uintptr(), head), + prog.IntVal(prog.SizeOf(prog.Type(blockType, InGo)), prog.Uintptr()), + ) + header := b.Convert(prog.Pointer(prog.Type(blockType, InGo)), headerAddress) + foundKey := b.Load(b.FieldAddr(header, blockKey[0])) + b.If(b.BinOp(token.EQL, foundKey, key), hit, slow) + + b.SetBlock(hit) + result := p.raw.Type.(*types.Signature).Results().At(0).Type() + b.Return(b.Convert(prog.rawType(result), head)) + + b.SetBlock(slow) + raw := b.Call(p.Pkg.rtFunc("LocalPackage"), key, size, align) + b.Return(b.Convert(prog.rawType(result), raw)) + b.EndBuild() +} + +func (p Package) runtimeGlobal(name string) Global { + p.NeedRuntime = true + runtimePkg := p.Prog.runtime() + variable := runtimePkg.Scope().Lookup(name).(*types.Var) + fullName := FullName(runtimePkg, name) + typ := types.NewPointer(variable.Type()) + if locality, ok := p.Prog.VariableLocality(fullName); ok && locality.LocalStorage == LocalStorageNativeTLS { + return p.NewThreadLocalVar(fullName, typ, InGo) + } + return p.NewVar(fullName, typ, InGo) +} diff --git a/ssa/local_context_test.go b/ssa/local_context_test.go new file mode 100644 index 0000000000..9517b13a9f --- /dev/null +++ b/ssa/local_context_test.go @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "go/token" + "go/types" + "strings" + "testing" +) + +func TestBuildLocalPackageAccessor(t *testing.T) { + for _, test := range []struct { + name string + nativeAnchor bool + declaration string + }{ + {"native TLS anchor", true, "external thread_local global i64"}, + {"ordinary anchor", false, "external global i64"}, + } { + t.Run(test.name, func(t *testing.T) { + prog := NewProgram(nil) + prog.SetRuntime(localContextTestRuntime()) + anchorName := PkgRuntime + "." + runtimeCurrentLocalContext + if test.nativeAnchor { + prog.SetLocalityInfo(anchorName, LocalityInfo{Locality: ThreadLocal}) + prog.SetLocalStorage(anchorName, LocalStorageNativeTLS) + } + + pkg := prog.NewPackage("accessor", "example.com/accessor") + key := pkg.NewVar("example.com/accessor.key", types.NewPointer(types.Typ[types.Uint8]), InGo) + key.InitNil() + field := types.NewField(token.NoPos, nil, "pointer", types.NewPointer(types.Typ[types.Int]), false) + block := types.NewStruct([]*types.Var{field}, nil) + blockType := prog.Type(block, InGo) + result := types.NewPointer(block) + results := types.NewTuple(types.NewVar(token.NoPos, nil, "", result)) + accessor := pkg.NewFunc("example.com/accessor.block", types.NewSignatureType(nil, nil, nil, nil, results, false), InGo) + accessor.BuildLocalPackageAccessor( + key.Expr, + prog.IntVal(prog.SizeOf(blockType), prog.Uintptr()), + prog.IntVal(prog.AlignOf(blockType), prog.Uintptr()), + ) + + ir := pkg.String() + anchor := `@"` + anchorName + `" = ` + test.declaration + if !strings.Contains(ir, anchor) { + t.Fatalf("context anchor declaration not found: %s\n%s", anchor, ir) + } + if got := strings.Count(ir, "icmp "); got != 3 { + t.Fatalf("accessor comparisons = %d, want context/head/key checks:\n%s", got, ir) + } + if !strings.Contains(ir, `call ptr @"`+PkgRuntime+`.LocalPackage"`) { + t.Fatalf("accessor has no LocalPackage slow path:\n%s", ir) + } + }) + } +} + +func localContextTestRuntime() *types.Package { + pkg := types.NewPackage(PkgRuntime, "runtime") + unsafePointer := types.Typ[types.UnsafePointer] + + contextName := types.NewTypeName(token.NoPos, pkg, runtimeLocalContext, nil) + contextFields := []*types.Var{types.NewField(token.NoPos, pkg, "blocks", unsafePointer, false)} + types.NewNamed(contextName, types.NewStruct(contextFields, nil), nil) + pkg.Scope().Insert(contextName) + + blockName := types.NewTypeName(token.NoPos, pkg, runtimeLocalBlock, nil) + blockFields := []*types.Var{ + types.NewField(token.NoPos, pkg, "next", unsafePointer, false), + types.NewField(token.NoPos, pkg, "key", unsafePointer, false), + } + types.NewNamed(blockName, types.NewStruct(blockFields, nil), nil) + pkg.Scope().Insert(blockName) + pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, runtimeCurrentLocalContext, types.Typ[types.Uintptr])) + + params := types.NewTuple( + types.NewVar(token.NoPos, pkg, "key", unsafePointer), + types.NewVar(token.NoPos, pkg, "size", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, pkg, "align", types.Typ[types.Uintptr]), + ) + results := types.NewTuple(types.NewVar(token.NoPos, pkg, "", unsafePointer)) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "LocalPackage", types.NewSignatureType(nil, nil, nil, params, results, false))) + return pkg +} diff --git a/test/llgoext/locality_test.go b/test/llgoext/locality_test.go index 3d49367531..796f320d47 100644 --- a/test/llgoext/locality_test.go +++ b/test/llgoext/locality_test.go @@ -456,6 +456,7 @@ var benchmarkTLS int var benchmarkGLS int var benchmarkSink int +var benchmarkReadSink uintptr //go:noinline func bumpOrdinaryGlobal() int { @@ -542,6 +543,46 @@ func BenchmarkGLSPackageBlock(b *testing.B) { benchmarkSink = value } +func TestComparableLocalityReads(t *testing.T) { + localitybench.PrepareReads() + ordinary := localitybench.ReadOrdinaryGlobal() + native := localitybench.ReadNativeTLS() + local := localitybench.ReadGLSPackage() + if ordinary == 0 || native != ordinary || local != ordinary { + t.Fatalf("comparable locality reads = ordinary:%#x native:%#x GLS:%#x", ordinary, native, local) + } +} + +func BenchmarkComparableOrdinaryGlobalRead(b *testing.B) { + localitybench.PrepareReads() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localitybench.ReadOrdinaryGlobal() + } + benchmarkReadSink = value +} + +func BenchmarkComparableNativeTLSRead(b *testing.B) { + localitybench.PrepareReads() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localitybench.ReadNativeTLS() + } + benchmarkReadSink = value +} + +func BenchmarkComparableGLSPackageRead(b *testing.B) { + localitybench.PrepareReads() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localitybench.ReadGLSPackage() + } + benchmarkReadSink = value +} + func BenchmarkGoroutineEntry(b *testing.B) { for i := 0; i < b.N; i++ { done := make(chan struct{}) diff --git a/test/llgoext/testdata/localitybench/bench.go b/test/llgoext/testdata/localitybench/bench.go index b9a1426252..2514a55b33 100644 --- a/test/llgoext/testdata/localitybench/bench.go +++ b/test/llgoext/testdata/localitybench/bench.go @@ -18,8 +18,15 @@ package localitybench +import "unsafe" + var backing int +var ordinaryPointer *int + +//llgo:tls +var nativePointerBits uintptr + //llgo:gls var pointer *int @@ -27,3 +34,24 @@ var pointer *int func Touch() { pointer = &backing } + +func PrepareReads() { + ordinaryPointer = &backing + nativePointerBits = uintptr(unsafe.Pointer(&backing)) + pointer = &backing +} + +//go:noinline +func ReadOrdinaryGlobal() uintptr { + return uintptr(unsafe.Pointer(ordinaryPointer)) +} + +//go:noinline +func ReadNativeTLS() uintptr { + return nativePointerBits +} + +//go:noinline +func ReadGLSPackage() uintptr { + return uintptr(unsafe.Pointer(pointer)) +} From 97ad68e69d8fa9b0efbff1afc866ec2c74960393 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 14:51:15 +0800 Subject: [PATCH 10/20] runtime: publish FuncForPC cache entries atomically Store each immutable Func as one atomic pointer and validate the queried PC from the Func itself. This prevents concurrent cache replacement from combining a PC and function from different writers. Apply the same publication rule to the prebuilt function cache and stress multiple PCs during concurrent first use. --- .../lib/runtime/pprof_runtime_stub_llgo.go | 49 ++++++++++++------- runtime/internal/lib/runtime/symtab.go | 12 ++--- test/go/runtime_lineinfo_stack_test.go | 48 ++++++++++++++++-- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index c74b6d1421..8d4987fea2 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -5,6 +5,7 @@ package runtime import ( "unsafe" + latomic "github.com/goplus/llgo/runtime/internal/lib/sync/atomic" llrt "github.com/goplus/llgo/runtime/internal/runtime" ) @@ -91,26 +92,36 @@ func SetCPUProfileRate(hz int) {} const funcForPCCacheSets = 1024 const funcForPCCacheWays = 4 -type funcForPCCacheEntry struct { - pc uintptr - fn *Func -} +// Cache immutable Func pointers as single atomic words. The queried pc lives +// in Func itself, so concurrent replacement cannot combine fields from two +// different cache entries. +var funcForPCCache [funcForPCCacheSets][funcForPCCacheWays]unsafe.Pointer +var funcForPCCacheNext [funcForPCCacheSets]uint32 +var funcForPCLast unsafe.Pointer -var funcForPCCache [funcForPCCacheSets][funcForPCCacheWays]funcForPCCacheEntry -var funcForPCCacheNext [funcForPCCacheSets]uint8 -var funcForPCLast funcForPCCacheEntry +func cachedFuncForPC(entry *unsafe.Pointer, pc uintptr) *Func { + p := latomic.LoadPointer(entry) + if p == nil { + return nil + } + fn := (*Func)(p) + if fn.pc != pc { + return nil + } + return fn +} func FuncForPC(pc uintptr) *Func { // External metadata must be installed before consulting either cache: // caching a pre-load miss would otherwise survive a successful load. ensureRuntimePCLN() - if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + if fn := cachedFuncForPC(&funcForPCLast, pc); fn != nil { return fn } set := &funcForPCCache[funcForPCCacheIndex(pc)] for i := 0; i < funcForPCCacheWays; i++ { - if fn := set[i].fn; fn != nil && set[i].pc == pc { - funcForPCLast = funcForPCCacheEntry{pc: pc, fn: fn} + if fn := cachedFuncForPC(&set[i], pc); fn != nil { + latomic.StorePointer(&funcForPCLast, unsafe.Pointer(fn)) return fn } } @@ -226,12 +237,12 @@ func newFuncForPC(pc uintptr, sym pcSymbol) *Func { // symbolized, going through the FuncForPC cache so repeated CallersFrames // walks over the same PCs stop allocating a Func per frame. func frameFuncForPC(pc uintptr, sym pcSymbol, name string) *Func { - if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + if fn := cachedFuncForPC(&funcForPCLast, pc); fn != nil { return fn } set := &funcForPCCache[funcForPCCacheIndex(pc)] for i := 0; i < funcForPCCacheWays; i++ { - if fn := set[i].fn; fn != nil && set[i].pc == pc { + if fn := cachedFuncForPC(&set[i], pc); fn != nil { return fn } } @@ -255,16 +266,16 @@ func cacheFuncForPC(pc uintptr, fn *Func) { setIndex := funcForPCCacheIndex(pc) set := &funcForPCCache[setIndex] for i := 0; i < funcForPCCacheWays; i++ { - if set[i].fn == nil || set[i].pc == pc { - set[i] = funcForPCCacheEntry{pc: pc, fn: fn} - funcForPCLast = set[i] + p := latomic.LoadPointer(&set[i]) + if p == nil || (*Func)(p).pc == pc { + latomic.StorePointer(&set[i], unsafe.Pointer(fn)) + latomic.StorePointer(&funcForPCLast, unsafe.Pointer(fn)) return } } - way := funcForPCCacheNext[setIndex] & (funcForPCCacheWays - 1) - funcForPCCacheNext[setIndex] = way + 1 - set[way] = funcForPCCacheEntry{pc: pc, fn: fn} - funcForPCLast = set[way] + way := (latomic.AddUint32(&funcForPCCacheNext[setIndex], 1) - 1) & (funcForPCCacheWays - 1) + latomic.StorePointer(&set[way], unsafe.Pointer(fn)) + latomic.StorePointer(&funcForPCLast, unsafe.Pointer(fn)) } func funcForPCCacheIndex(pc uintptr) uintptr { diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index b4db828e5c..883b8df45e 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -808,22 +808,22 @@ var runtimePrebuiltEntriesOnce uint32 // lookups. The set-associative pc cache in FuncForPC thrashes once the live // pc population outgrows it (thousands of distinct functions queried in a // loop); this cache is keyed by table row, so batch workloads stay O(search) -// after the first pass regardless of scale. Same benign-race model as the -// pc cache: word-sized pointer stores of identical values. +// after the first pass regardless of scale. Atomic pointer publication keeps +// concurrently created Func values fully initialized before readers use them. var runtimePrebuiltFuncs []unsafe.Pointer func prebuiltFuncCacheLoad(idx int) unsafe.Pointer { if idx < 0 || idx >= len(runtimePrebuiltFuncs) { return nil } - return runtimePrebuiltFuncs[idx] + return latomic.LoadPointer(&runtimePrebuiltFuncs[idx]) } func prebuiltFuncCacheStore(idx int, fn unsafe.Pointer) { if idx < 0 || idx >= len(runtimePrebuiltFuncs) { return } - runtimePrebuiltFuncs[idx] = fn + latomic.StorePointer(&runtimePrebuiltFuncs[idx], fn) } // prebuiltFrameIndexForEntry returns the ftab row whose entry is exactly pc, @@ -1576,9 +1576,9 @@ func init() { // Write-warm the FuncForPC cache: its first stores otherwise take // zero-fill write faults, one per page, on the first few lookups. for i := 0; i < funcForPCCacheSets; i += 4096 / int(unsafe.Sizeof(funcForPCCache[0])) { - funcForPCCache[i][0].pc = 0 + latomic.StorePointer(&funcForPCCache[i][0], nil) } - funcForPCCache[funcForPCCacheSets-1][0].pc = 0 + latomic.StorePointer(&funcForPCCache[funcForPCCacheSets-1][0], nil) } func coldFuncInfoEntryLookup(pc uintptr) (pcSymbol, bool) { diff --git a/test/go/runtime_lineinfo_stack_test.go b/test/go/runtime_lineinfo_stack_test.go index 52fffcabef..673276bcd7 100644 --- a/test/go/runtime_lineinfo_stack_test.go +++ b/test/go/runtime_lineinfo_stack_test.go @@ -224,6 +224,7 @@ func TestRuntimeLineInfoAndStack(t *testing.T) { const runtimeFuncInfoConcurrentFirstUseProbe = `package main import ( + "reflect" "runtime" "strconv" "strings" @@ -232,16 +233,33 @@ import ( func main() { const n = 32 + const rounds = 1000 start := make(chan struct{}) errc := make(chan string, n) var wg sync.WaitGroup for i := 0; i < n; i++ { + target := concurrentTargets[i%len(concurrentTargets)] + pc := reflect.ValueOf(target.fn).Pointer() wg.Add(1) - go func() { + go func(target concurrentTarget, pc uintptr) { defer wg.Done() <-start - errc <- checkRuntimeInfo() - }() + for j := 0; j < rounds; j++ { + fn := runtime.FuncForPC(pc) + if fn == nil || fn.Name() != target.name { + name := "" + if fn != nil { + name = fn.Name() + } + errc <- "bad target func: " + name + return + } + if err := checkRuntimeInfo(); err != "" { + errc <- err + return + } + } + }(target, pc) } close(start) wg.Wait() @@ -253,6 +271,30 @@ func main() { } } +type concurrentTarget struct { + fn func() + name string +} + +var concurrentTargets = []concurrentTarget{ + {concurrentTarget0, "main.concurrentTarget0"}, + {concurrentTarget1, "main.concurrentTarget1"}, + {concurrentTarget2, "main.concurrentTarget2"}, + {concurrentTarget3, "main.concurrentTarget3"}, +} + +//go:noinline +func concurrentTarget0() {} + +//go:noinline +func concurrentTarget1() {} + +//go:noinline +func concurrentTarget2() {} + +//go:noinline +func concurrentTarget3() {} + //go:noinline func checkRuntimeInfo() string { pc, file, line, ok := runtime.Caller(0) // CONCURRENT_CALLER_MARK From e0d25420acb647a0fc0b5a58244f5f67a440921b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 21:21:48 +0800 Subject: [PATCH 11/20] compiler/runtime: use direct package locality caches Give each package-backed locality block an owner-local TLS cache so hot accesses are O(1) regardless of the number of touched packages. Keep the block list only for GC reachability and teardown. Require locality variables to be unexported and reject go:linkname aliases. Add codegen, lifecycle, and 2/4/8-package performance coverage. --- cl/_testgo/localitycodegen/in.go | 34 +-- cl/locality_lower.go | 26 +- cl/locality_lower_test.go | 4 +- cl/locality_test.go | 252 +++++++----------- internal/build/build_test.go | 12 +- internal/locality/layout/layout.go | 10 +- internal/locality/layout/layout_test.go | 4 +- internal/locality/locality_test.go | 22 +- internal/locality/scan.go | 6 + runtime/internal/runtime/local_context.go | 62 ++--- .../internal/runtime/local_context_stub.go | 11 +- runtime/internal/runtime/local_initializer.go | 10 +- ssa/local_context.go | 69 +---- ssa/local_context_test.go | 84 ++---- ssa/locality.go | 69 +++-- ssa/locality_test.go | 78 ++---- test/llgoext/locality_test.go | 46 +++- test/llgoext/localitymulti/locality_test.go | 115 ++++++++ .../testdata/localityblocks/p0/block.go | 31 +++ .../testdata/localityblocks/p1/block.go | 31 +++ .../testdata/localityblocks/p2/block.go | 31 +++ .../testdata/localityblocks/p3/block.go | 31 +++ .../testdata/localityblocks/p4/block.go | 31 +++ .../testdata/localityblocks/p5/block.go | 31 +++ .../testdata/localityblocks/p6/block.go | 31 +++ .../testdata/localityblocks/p7/block.go | 31 +++ test/llgoext/testdata/localityscope/scope.go | 25 +- 27 files changed, 729 insertions(+), 458 deletions(-) create mode 100644 test/llgoext/localitymulti/locality_test.go create mode 100644 test/llgoext/testdata/localityblocks/p0/block.go create mode 100644 test/llgoext/testdata/localityblocks/p1/block.go create mode 100644 test/llgoext/testdata/localityblocks/p2/block.go create mode 100644 test/llgoext/testdata/localityblocks/p3/block.go create mode 100644 test/llgoext/testdata/localityblocks/p4/block.go create mode 100644 test/llgoext/testdata/localityblocks/p5/block.go create mode 100644 test/llgoext/testdata/localityblocks/p6/block.go create mode 100644 test/llgoext/testdata/localityblocks/p7/block.go diff --git a/cl/_testgo/localitycodegen/in.go b/cl/_testgo/localitycodegen/in.go index 25927d2b47..b200a72584 100644 --- a/cl/_testgo/localitycodegen/in.go +++ b/cl/_testgo/localitycodegen/in.go @@ -1,25 +1,19 @@ // LITTEST package main -// CHECK-DAG: @"{{.*}}localitycodegen.Scalar" = thread_local global i64 0 -// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_local_key" = global i8 0 +// CHECK-DAG: @"{{.*}}localitycodegen.scalar" = thread_local global i64 0 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_local_cache" = thread_local global i64 0 // CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$guard" = thread_local global i8 0 -// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$failure" = global i8 0 -// CHECK-DAG: @"{{.*}}runtime.currentLocalContext" = external thread_local global i64 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$failure_cache" = thread_local global i64 0 // CHECK-NOT: RegisterLocalRoot -// CHECK-NOT: localitycodegen.Pointer" = thread_local -// CHECK-NOT: localitycodegen.Initialized" = thread_local +// CHECK-NOT: localitycodegen.pointer" = thread_local +// CHECK-NOT: localitycodegen.initialized" = thread_local // CHECK-LABEL: define ptr @"{{.*}}localitycodegen.__llgo_local_block"() -// CHECK: load i64, ptr @"{{.*}}runtime.currentLocalContext" +// CHECK: load i64, ptr @"{{.*}}localitycodegen.__llgo_local_cache" // CHECK: icmp ne i64 -// CHECK: load ptr, ptr -// CHECK: icmp ne ptr -// CHECK: sub i64 -// CHECK: load ptr, ptr -// CHECK: icmp eq ptr // CHECK: ret ptr -// CHECK: call ptr @"{{.*}}runtime.LocalPackage"(ptr @"{{.*}}localitycodegen.__llgo_local_key", i64 16, i64 8) +// CHECK: call ptr @"{{.*}}runtime.LocalPackage"(ptr @"{{.*}}localitycodegen.__llgo_local_cache", i64 16, i64 8) // CHECK: ret ptr // CHECK-LABEL: define void @"{{.*}}localitycodegen.__llgo_tls_init"() @@ -27,7 +21,7 @@ package main // CHECK-LABEL: define void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() // CHECK: load i8, ptr -// CHECK: call void @"{{.*}}runtime.EnsureLocalInitializer"(ptr @"{{.*}}localitycodegen.__llgo_tls_init$guard", ptr @"{{.*}}localitycodegen.__llgo_tls_init$failure" +// CHECK: call void @"{{.*}}runtime.EnsureLocalInitializer"(ptr @"{{.*}}localitycodegen.__llgo_tls_init$guard", ptr @"{{.*}}localitycodegen.__llgo_tls_init$failure_cache" // CHECK-LABEL: define ptr @{{"?ExportedLocality"?}}() // CHECK: call i64 @"{{.*}}EnterLocalContext" @@ -43,7 +37,7 @@ package main // CHECK-LABEL: define { i64, ptr, ptr } @"{{.*}}localitycodegen.values"() // CHECK: call void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() -// CHECK: load i64, ptr @"{{.*}}localitycodegen.Scalar" +// CHECK: load i64, ptr @"{{.*}}localitycodegen.scalar" // CHECK: call ptr @"{{.*}}localitycodegen.__llgo_local_block"() // CHECK: load ptr, ptr // CHECK: load ptr, ptr @@ -60,21 +54,21 @@ func newPointer() *int { } //llgo:tls -var Scalar int +var scalar int //llgo:gls -var Pointer *int +var pointer *int //llgo:tls -var Initialized = newPointer() +var initialized = newPointer() func values() (int, *int, *int) { - return Scalar, Pointer, Initialized + return scalar, pointer, initialized } //export ExportedLocality func ExportedLocality() *int { - return Pointer + return pointer } func main() { diff --git a/cl/locality_lower.go b/cl/locality_lower.go index 7dbea7a612..b06dc2e432 100644 --- a/cl/locality_lower.go +++ b/cl/locality_lower.go @@ -44,10 +44,10 @@ type localPackage struct { } type localInitializer struct { - guard llssa.Global - failureKey llssa.Global - dispatch llssa.Function - ensure llssa.Function + guard llssa.Global + failureCache llssa.Global + dispatch llssa.Function + ensure llssa.Function } type localBaseCacheKey struct { @@ -131,9 +131,9 @@ func (p *context) prepareLocalVariables(pkg llssa.Package, globals []*ssa.Global } } -// localVariableFor resolves one Go SSA global to the canonical package plan. -// Both local definitions and imported references use this path so linkname and -// layout validation cannot diverge between the two lowering cases. +// localVariableFor validates one Go SSA global and finds its declaring package +// plan. Both definitions and references use this path so forbidden linkname +// aliases and layout validation cannot diverge between lowering cases. func (p *context) localVariableFor(pkg llssa.Package, global *ssa.Global, defineCurrent bool) (*localVariable, bool, error) { fullName := llssa.FullName(global.Pkg.Pkg, global.Name()) canonical, info, ok, err := p.prog.ResolveLocality(fullName) @@ -253,16 +253,16 @@ func (p *context) buildLocalPackage(pkg llssa.Package, owner *localPackage, defi } structType := types.NewStruct(fields, nil) owner.typ = p.prog.Type(structType, llssa.InGo) - key := pkg.NewVar(localitylayout.BlockKeyName(owner.plan.Path), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) + cache := pkg.NewThreadLocalVar(localitylayout.BlockCacheName(owner.plan.Path), types.NewPointer(types.Typ[types.Uintptr]), llssa.InGo) if define { - key.InitNil() + cache.InitNil() } result := types.NewPointer(structType) owner.blockFunc = pkg.NewFunc(localitylayout.BlockName(owner.plan.Path), noArgResultSignature(result), llssa.InGo) owner.blockFunc.Inline(llssa.AlwaysInline) if define && !owner.blockFunc.HasBody() { owner.blockFunc.BuildLocalPackageAccessor( - key.Expr, + cache.Expr, p.prog.IntVal(p.prog.SizeOf(owner.typ), p.prog.Uintptr()), p.prog.IntVal(p.prog.AlignOf(owner.typ), p.prog.Uintptr()), ) @@ -280,7 +280,7 @@ func (p *context) buildLocalPackage(pkg llssa.Package, owner *localPackage, defi func (p *context) buildLocalInitializer(pkg llssa.Package, owner *localPackage, kind locality.Kind, initializers []localitylayout.Initializer, define bool) *localInitializer { ret := &localInitializer{} ret.guard = pkg.NewThreadLocalVar(localitylayout.GuardName(owner.plan.Path, kind), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) - ret.failureKey = pkg.NewVar(localitylayout.FailureKeyName(owner.plan.Path, kind), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) + ret.failureCache = pkg.NewThreadLocalVar(localitylayout.FailureCacheName(owner.plan.Path, kind), types.NewPointer(types.Typ[types.Uintptr]), llssa.InGo) ret.dispatch = pkg.NewFunc(localitylayout.InitName(owner.plan.Path, kind), llssa.NoArgsNoRet, llssa.InGo) ret.ensure = pkg.NewFunc(localitylayout.EnsureName(owner.plan.Path, kind), llssa.NoArgsNoRet, llssa.InGo) ret.ensure.Inline(llssa.AlwaysInline) @@ -288,7 +288,7 @@ func (p *context) buildLocalInitializer(pkg llssa.Package, owner *localPackage, return ret } ret.guard.InitNil() - ret.failureKey.InitNil() + ret.failureCache.InitNil() if !ret.dispatch.HasBody() { b := ret.dispatch.MakeBody(1) for _, initializer := range initializers { @@ -307,7 +307,7 @@ func (p *context) buildLocalInitializer(pkg llssa.Package, owner *localPackage, b.Call( pkg.RuntimeFunc("EnsureLocalInitializer"), ret.guard.Expr, - b.Convert(p.prog.VoidPtr(), ret.failureKey.Expr), + ret.failureCache.Expr, closure, ) b.Jump(ret.ensure.Block(2)) diff --git a/cl/locality_lower_test.go b/cl/locality_lower_test.go index 7610d70c13..8d16ae9e26 100644 --- a/cl/locality_lower_test.go +++ b/cl/locality_lower_test.go @@ -124,13 +124,13 @@ func TestLocalityLoweringDiagnostics(t *testing.T) { } }) - t.Run("linkname cycle", func(t *testing.T) { + t.Run("local linkname", func(t *testing.T) { prog := newProgram() other := typesPkg.Path() + ".Other" prog.SetLinkname(name, other) prog.SetLinkname(other, name) ctx := &context{prog: prog, goTyps: typesPkg} - if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "linkname cycle") { + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { t.Fatalf("localVariableFor error = %v", err) } assertLocalityPanic(t, "resolveLocality", func() { ctx.resolveLocality(name) }) diff --git a/cl/locality_test.go b/cl/locality_test.go index 694058bd90..6c0c6ea515 100644 --- a/cl/locality_test.go +++ b/cl/locality_test.go @@ -16,10 +16,6 @@ import ( ) func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { - return compileLocalitySourceWithNativeAnchor(t, src, true) -} - -func compileLocalitySourceWithNativeAnchor(t *testing.T, src string, nativeAnchor bool) (llssa.Program, string) { t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "locality.go", src, parser.ParseComments) @@ -36,11 +32,6 @@ func compileLocalitySourceWithNativeAnchor(t *testing.T, src string, nativeAncho prog := ssatest.NewProgramEx(t, nil, imp) prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) prog.SetRuntime(localityRuntimePackage()) - if nativeAnchor { - anchor := llssa.PkgRuntime + ".currentLocalContext" - prog.SetLocalityInfo(anchor, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) - prog.SetLocalStorage(anchor, llssa.LocalStorageNativeTLS) - } if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { t.Fatal(err) } @@ -76,15 +67,9 @@ func localityRuntimePackage() *types.Package { localContextName := types.NewTypeName(token.NoPos, pkg, "LocalContext", nil) localContext := types.NewNamed(localContextName, types.NewStruct([]*types.Var{blocks}, nil), nil) pkg.Scope().Insert(localContextName) - next := types.NewField(token.NoPos, pkg, "next", unsafePointer, false) - key := types.NewField(token.NoPos, pkg, "key", unsafePointer, false) - localBlockName := types.NewTypeName(token.NoPos, pkg, "localBlock", nil) - types.NewNamed(localBlockName, types.NewStruct([]*types.Var{next, key}, nil), nil) - pkg.Scope().Insert(localBlockName) - pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, "currentLocalContext", types.Typ[types.Uintptr])) localPackageParams := types.NewTuple( - types.NewParam(token.NoPos, pkg, "key", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, pkg, "cache", types.NewPointer(types.Typ[types.Uintptr])), types.NewParam(token.NoPos, pkg, "size", types.Typ[types.Uintptr]), types.NewParam(token.NoPos, pkg, "align", types.Typ[types.Uintptr]), ) @@ -94,7 +79,7 @@ func localityRuntimePackage() *types.Package { callback := types.NewSignatureType(nil, nil, nil, nil, nil, false) ensureParams := types.NewTuple( types.NewParam(token.NoPos, pkg, "state", types.NewPointer(types.Typ[types.Uint8])), - types.NewParam(token.NoPos, pkg, "failureKey", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, pkg, "failureCache", types.NewPointer(types.Typ[types.Uintptr])), types.NewParam(token.NoPos, pkg, "initialize", callback), ) pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "EnsureLocalInitializer", types.NewSignatureType(nil, nil, nil, ensureParams, nil, false))) @@ -111,38 +96,29 @@ func localityRuntimePackage() *types.Package { return pkg } -func TestLocalPackageAccessorUsesRuntimeAnchorStorage(t *testing.T) { +func TestLocalPackageAccessorUsesDirectTLSCache(t *testing.T) { const src = `package locality //llgo:gls -var Pointer *int +var pointer *int -func value() *int { return Pointer } +func value() *int { return pointer } ` - for _, test := range []struct { - name string - nativeAnchor bool - declaration string - }{ - {"native TLS", true, "external thread_local global i64"}, - {"ordinary global", false, "external global i64"}, - } { - t.Run(test.name, func(t *testing.T) { - _, ir := compileLocalitySourceWithNativeAnchor(t, src, test.nativeAnchor) - anchor := `@"` + llssa.PkgRuntime + `.currentLocalContext" = ` + test.declaration - if !strings.Contains(ir, anchor) { - t.Fatalf("runtime context anchor declaration not found: %s\n%s", anchor, ir) - } - accessor := llvmFunction(t, ir, "example.com/locality.__llgo_local_block") - load := `load i64, ptr @"` + llssa.PkgRuntime + `.currentLocalContext"` - slow := `call ptr @"` + llssa.PkgRuntime + `.LocalPackage"` - if loadAt, slowAt := strings.Index(accessor, load), strings.Index(accessor, slow); loadAt < 0 || slowAt < 0 || loadAt >= slowAt { - t.Fatalf("accessor does not load the runtime anchor before its slow path:\n%s", accessor) - } - if got := strings.Count(accessor, "icmp "); got != 3 { - t.Fatalf("accessor comparisons = %d, want context/head/key checks:\n%s", got, accessor) - } - }) + _, ir := compileLocalitySource(t, src) + if !strings.Contains(ir, `@"example.com/locality.__llgo_local_cache" = thread_local global i64 0`) { + t.Fatalf("package direct cache not found:\n%s", ir) + } + accessor := llvmFunction(t, ir, "example.com/locality.__llgo_local_block") + cacheLoad := `load i64, ptr @"example.com/locality.__llgo_local_cache"` + slow := `call ptr @"` + llssa.PkgRuntime + `.LocalPackage"(ptr @"example.com/locality.__llgo_local_cache"` + if loadAt, slowAt := strings.Index(accessor, cacheLoad), strings.Index(accessor, slow); loadAt < 0 || slowAt < 0 || loadAt >= slowAt { + t.Fatalf("accessor does not load its direct cache before the cold path:\n%s", accessor) + } + if got := strings.Count(accessor, "icmp "); got != 1 { + t.Fatalf("accessor comparisons = %d, want one cache check:\n%s", got, accessor) + } + if strings.Contains(accessor, "currentLocalContext") { + t.Fatalf("accessor still depends on runtime context layout:\n%s", accessor) } } @@ -174,16 +150,16 @@ func scalar() int { return 42 } func pointer() *int { return &backing } //llgo:tls -var TLSScalar = scalar() +var tlsScalar = scalar() //llgo:tls -var TLSPointer = pointer() +var tlsPointer = pointer() //llgo:gls -var GLSScalar = scalar() +var glsScalar = scalar() //llgo:gls -var GLSPointer = pointer() +var glsPointer = pointer() func values() (int, *int, int, *int) { - return TLSScalar, TLSPointer, GLSScalar, GLSPointer + return tlsScalar, tlsPointer, glsScalar, glsPointer } `) @@ -191,10 +167,10 @@ func values() (int, *int, int, *int) { kind llssa.Locality storage llssa.LocalStorage }{ - "TLSScalar": {llssa.ThreadLocal, llssa.LocalStorageNativeTLS}, - "TLSPointer": {llssa.ThreadLocal, llssa.LocalStoragePackage}, - "GLSScalar": {llssa.GoroutineLocal, llssa.LocalStorageNativeTLS}, - "GLSPointer": {llssa.GoroutineLocal, llssa.LocalStoragePackage}, + "tlsScalar": {llssa.ThreadLocal, llssa.LocalStorageNativeTLS}, + "tlsPointer": {llssa.ThreadLocal, llssa.LocalStoragePackage}, + "glsScalar": {llssa.GoroutineLocal, llssa.LocalStorageNativeTLS}, + "glsPointer": {llssa.GoroutineLocal, llssa.LocalStoragePackage}, } for name, want := range checks { got, ok := prog.VariableLocality("example.com/locality." + name) @@ -202,18 +178,18 @@ func values() (int, *int, int, *int) { t.Fatalf("%s metadata = %+v, %v", name, got, ok) } } - for _, name := range []string{"TLSScalar", "GLSScalar"} { + for _, name := range []string{"tlsScalar", "glsScalar"} { if !strings.Contains(ir, `@"example.com/locality.`+name+`" = thread_local global i64`) { t.Fatalf("%s is not native TLS:\n%s", name, ir) } } - for _, name := range []string{"TLSPointer", "GLSPointer"} { + for _, name := range []string{"tlsPointer", "glsPointer"} { if strings.Contains(ir, `@"example.com/locality.`+name+`" = thread_local`) { t.Fatalf("%s retained a pointer-bearing TLS global:\n%s", name, ir) } } - if got := strings.Count(ir, `@"example.com/locality.__llgo_local_key" =`); got != 1 { - t.Fatalf("package block keys = %d, want 1:\n%s", got, ir) + if got := strings.Count(ir, `@"example.com/locality.__llgo_local_cache" = thread_local global i64 0`); got != 1 { + t.Fatalf("package block caches = %d, want 1:\n%s", got, ir) } if got := strings.Count(ir, `call ptr @"github.com/goplus/llgo/runtime/internal/runtime.LocalPackage"`); got != 1 { t.Fatalf("LocalPackage calls = %d, want one accessor definition:\n%s", got, ir) @@ -241,15 +217,15 @@ func TestLocalityDebugInfoOnlyUsesFixedGlobals(t *testing.T) { _, ir := compileLocalitySource(t, `package locality //llgo:tls -var Direct int +var direct int //llgo:gls -var Pointer *int +var pointer *int -func values() (int, *int) { return Direct, Pointer } +func values() (int, *int) { return direct, pointer } `) - direct := `@"example.com/locality.Direct" = thread_local global i64` + direct := `@"example.com/locality.direct" = thread_local global i64` start := strings.Index(ir, direct) if start < 0 { t.Fatalf("native TLS global not found:\n%s", ir) @@ -258,7 +234,7 @@ func values() (int, *int) { return Direct, Pointer } if end < 0 || !strings.Contains(ir[start:start+end], "!dbg") { t.Fatalf("native TLS global has no debug metadata:\n%s", ir[start:]) } - if strings.Contains(ir, `@"example.com/locality.Pointer" =`) { + if strings.Contains(ir, `@"example.com/locality.pointer" =`) { t.Fatalf("package-local pointer was emitted as a fixed debug global:\n%s", ir) } } @@ -268,12 +244,12 @@ func TestLocalityInitializersPreserveGoOrderPerKind(t *testing.T) { func mark(value int) int { return value } //llgo:tls -var T0 = mark(0) +var t0 = mark(0) //llgo:gls -var G0 = mark(1) +var g0 = mark(1) //llgo:tls -var T1 = mark(2) -func values() (int, int, int) { return T0, T1, G0 } +var t1 = mark(2) +func values() (int, int, int) { return t0, t1, g0 } `) tls := llvmFunction(t, ir, "example.com/locality.__llgo_tls_init") gls := llvmFunction(t, ir, "example.com/locality.__llgo_gls_init") @@ -297,8 +273,8 @@ func TestDirectInitializerStillRequiresFailureContext(t *testing.T) { prog, ir := compileLocalitySource(t, `package locality func value() int { return 1 } //llgo:tls -var Value = value() -func get() int { return Value } +var localValue = value() +func get() int { return localValue } `) if !prog.NeedsLocalContext() { t.Fatal("initializer failure storage did not enable a local context") @@ -306,7 +282,7 @@ func get() int { return Value } if strings.Contains(ir, `__llgo_local_block`) { t.Fatalf("pointer-free package unexpectedly has a value block:\n%s", ir) } - if !strings.Contains(ir, `@"example.com/locality.Value" = thread_local global i64`) || !strings.Contains(ir, `EnsureLocalInitializer`) { + if !strings.Contains(ir, `@"example.com/locality.localValue" = thread_local global i64`) || !strings.Contains(ir, `EnsureLocalInitializer`) { t.Fatalf("direct initializer lowering is incomplete:\n%s", ir) } } @@ -314,10 +290,10 @@ func get() int { return Value } func TestZeroValueDirectLocalsNeedNoContext(t *testing.T) { prog, ir := compileLocalitySource(t, `package locality //llgo:tls -var T int +var threadValue int //llgo:gls -var G uintptr -func values() (int, uintptr) { return T, G } +var goroutineValue uintptr +func values() (int, uintptr) { return threadValue, goroutineValue } `) if prog.NeedsLocalContext() { t.Fatal("pointer-free zero-value locals enabled a local context") @@ -330,10 +306,10 @@ func values() (int, uintptr) { return T, G } func TestExportedFunctionInstallsLocalContext(t *testing.T) { _, ir := compileLocalitySource(t, `package locality //llgo:gls -var Pointer *int +var pointer *int //export Exported func Exported(useLocal bool) *int { - if useLocal { return Pointer } + if useLocal { return pointer } return nil } `) @@ -355,9 +331,9 @@ func Exported(useLocal bool) *int { func TestExportedNativeTLSNeedsNoLocalContext(t *testing.T) { prog, ir := compileLocalitySource(t, `package locality //llgo:tls -var Scalar int +var scalar int //export Exported -func Exported() int { return Scalar } +func Exported() int { return scalar } `) if prog.NeedsLocalContext() { t.Fatal("zero-value native TLS enabled a local context") @@ -380,44 +356,32 @@ func assertTextOrder(t *testing.T, text string, wants ...string) { } } -func TestLocalityLinknameAliasesReuseCanonicalStorage(t *testing.T) { - prog, ir := compileLocalitySource(t, `package locality - -//llgo:gls -var Pointer *int -//go:linkname PointerAlias example.com/locality.Pointer +func TestLocalityRejectsLinknameAlias(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality //llgo:gls -var PointerAlias *int - -//llgo:tls -var Scalar int -//go:linkname ScalarAlias example.com/locality.Scalar -//llgo:tls -var ScalarAlias int - -func values() (*int, *int, int, int) { return Pointer, PointerAlias, Scalar, ScalarAlias } -`) - if strings.Contains(ir, "PointerAlias") || strings.Contains(ir, "ScalarAlias") { - t.Fatalf("linkname aliases received independent LLVM storage:\n%s", ir) +var target *int +//go:linkname alias example.com/locality.target +var alias *int +`, parser.ParseComments) + if err != nil { + t.Fatal(err) } - if got := strings.Count(ir, `@"example.com/locality.Scalar" = thread_local global i64`); got != 1 { - t.Fatalf("canonical scalar globals = %d, want 1:\n%s", got, ir) + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, []*ast.File{file}, info) + if err != nil { + t.Fatal(err) } - values := llvmFunction(t, ir, "example.com/locality.values") - if got := strings.Count(values, `call ptr @"example.com/locality.__llgo_local_block"()`); got != 1 { - t.Fatalf("alias package-base calls = %d, want 1:\n%s", got, values) + prog := ssatest.NewProgram(t, nil) + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) } - for name, want := range map[string]llssa.LocalStorage{ - "example.com/locality.PointerAlias": llssa.LocalStoragePackage, - "example.com/locality.ScalarAlias": llssa.LocalStorageNativeTLS, - } { - if got, ok := prog.VariableLocality(name); !ok || got.LocalStorage != want { - t.Fatalf("alias metadata %s = %+v, %v; want storage %v", name, got, ok, want) - } + if err := prog.ValidateLocalities(pkg.Path()); err == nil || !strings.Contains(err.Error(), "cannot reference local variable") { + t.Fatalf("ValidateLocalities error = %v", err) } } -func TestLocalityCrossPackageAccessUsesDependencyStorage(t *testing.T) { +func TestLocalityCrossPackageAccessUsesFunctions(t *testing.T) { fset := token.NewFileSet() parse := func(name, source string) *ast.File { file, err := parser.ParseFile(fset, name, source, parser.ParseComments) @@ -429,13 +393,14 @@ func TestLocalityCrossPackageAccessUsesDependencyStorage(t *testing.T) { depFile := parse("dep.go", `package dep func initialScalar() int { return 1 } //llgo:tls -var Scalar = initialScalar() +var scalar = initialScalar() //llgo:gls -var Pointer *int +var pointer *int +func Values() (int, *int) { return scalar, pointer } `) rootFile := parse("root.go", `package root import "example.com/dep" -func Values() (int, *int) { return dep.Scalar, dep.Pointer } +func Values() (int, *int) { return dep.Values() } `) check := func(path string, files []*ast.File, imp types.Importer) (*types.Package, *types.Info) { info := newLocalityTypeInfo() @@ -484,44 +449,36 @@ func Values() (int, *int) { return dep.Scalar, dep.Pointer } t.Fatal(err) } ir := root.String() - if !strings.Contains(ir, `@"example.com/dep.Scalar" = external thread_local global i64`) { - t.Fatalf("root package did not reference dependency TLS storage:\n%s", ir) - } - if !strings.Contains(ir, `declare ptr @"example.com/dep.__llgo_local_block"()`) { - t.Fatalf("root package did not reference dependency block accessor:\n%s", ir) + if !strings.Contains(ir, `@"example.com/dep.Values"`) { + t.Fatalf("root package did not call the dependency function:\n%s", ir) } - if !strings.Contains(ir, `declare void @"example.com/dep.__llgo_tls_init$ensure"()`) { - t.Fatalf("root package did not reference dependency initializer guard:\n%s", ir) - } - if strings.Contains(ir, `define ptr @"example.com/dep.__llgo_local_block"()`) { - t.Fatalf("root package redefined dependency block accessor:\n%s", ir) + for _, symbol := range []string{"example.com/dep.scalar", "example.com/dep.pointer", "example.com/dep.__llgo_"} { + if strings.Contains(ir, symbol) { + t.Fatalf("root package referenced dependency locality storage %q:\n%s", symbol, ir) + } } } -func TestPrepareRejectsLocalAliasInitializer(t *testing.T) { +func TestParseRejectsLocalAliasInitializer(t *testing.T) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "locality.go", `package locality //llgo:tls -var Target int -//go:linkname Alias example.com/locality.Target +var target int +//go:linkname alias example.com/locality.target //llgo:tls -var Alias = 1 +var alias = 1 `, parser.ParseComments) if err != nil { t.Fatal(err) } files := []*ast.File{file} - info := newLocalityTypeInfo() - pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, newLocalityTypeInfo()) if err != nil { t.Fatal(err) } prog := llssa.NewProgram(nil) - if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { - t.Fatal(err) - } - if err := PrepareLocalVariables(prog, fset, pkg, info, files); err == nil || !strings.Contains(err.Error(), "linkname alias") { - t.Fatalf("PrepareLocalVariables error = %v", err) + if err := ParsePkgSyntax(prog, fset, pkg, files); err == nil || !strings.Contains(err.Error(), "cannot apply to a //go:linkname variable") { + t.Fatalf("ParsePkgSyntax error = %v", err) } } @@ -565,15 +522,15 @@ var value = initialValue() wantError: "inconsistent initializer metadata", }, { - name: "linkname locality mismatch", + name: "linkname locality", src: `package locality //llgo:tls -var Target int -//go:linkname Alias example.com/locality.Target +var target int +//go:linkname alias example.com/locality.target //llgo:gls -var Alias int +var alias int `, - wantError: "uses //llgo:gls", + wantError: "cannot apply to a //go:linkname variable", }, } @@ -606,10 +563,9 @@ var Alias int } } -func TestPrepareRejectsLocalAliasWithoutLocalTarget(t *testing.T) { +func TestParseRejectsExportedLocalVariable(t *testing.T) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "locality.go", `package locality -//go:linkname Value C.value //llgo:tls var Value int `, parser.ParseComments) @@ -617,17 +573,13 @@ var Value int t.Fatal(err) } files := []*ast.File{file} - info := newLocalityTypeInfo() - pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, newLocalityTypeInfo()) if err != nil { t.Fatal(err) } prog := ssatest.NewProgram(t, nil) - if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { - t.Fatal(err) - } - if err := PrepareLocalVariables(prog, fset, pkg, info, files); err == nil || !strings.Contains(err.Error(), "is not a local variable") { - t.Fatalf("PrepareLocalVariables error = %v", err) + if err := ParsePkgSyntax(prog, fset, pkg, files); err == nil || !strings.Contains(err.Error(), "requires an unexported package variable") { + t.Fatalf("ParsePkgSyntax error = %v", err) } } @@ -663,7 +615,7 @@ func TestPrepareLocalVariablesRejectsInvalidMetadata(t *testing.T) { t.Fatalf("PrepareLocalVariables error = %v", err) } }) - t.Run("linkname cycle", func(t *testing.T) { + t.Run("local linkname", func(t *testing.T) { prog := ssatest.NewProgram(t, nil) pkg := types.NewPackage("example.com/cycle", "cycle") first := llssa.FullName(pkg, "First") @@ -672,7 +624,7 @@ func TestPrepareLocalVariablesRejectsInvalidMetadata(t *testing.T) { prog.SetLocalityInfo(first, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) prog.SetLinkname(first, second) prog.SetLinkname(second, first) - if err := PrepareLocalVariables(prog, nil, pkg, &types.Info{}, nil); err == nil || !strings.Contains(err.Error(), "linkname cycle") { + if err := PrepareLocalVariables(prog, nil, pkg, &types.Info{}, nil); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { t.Fatalf("PrepareLocalVariables error = %v", err) } }) @@ -716,10 +668,10 @@ func TestNamedPointerLocalUsesPackageStorage(t *testing.T) { type Handle struct { Pointer *int } func makeHandle() Handle { return Handle{} } //llgo:tls -var Value = makeHandle() -func get() Handle { return Value } +var value = makeHandle() +func get() Handle { return value } `) - info, ok := prog.VariableLocality("example.com/locality.Value") + info, ok := prog.VariableLocality("example.com/locality.value") if !ok || info.LocalStorage != llssa.LocalStoragePackage { t.Fatalf("named pointer metadata = %+v, %v", info, ok) } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 3ac9b5d371..aba4518d47 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -949,24 +949,24 @@ func Invalid() {} } } -func TestDoReportsLocalityAliasInitializer(t *testing.T) { +func TestDoRejectsLocalityLinkname(t *testing.T) { file := filepath.Join(t.TempDir(), "invalid_locality_alias.go") if err := os.WriteFile(file, []byte(`package invalidlocalityalias import _ "unsafe" //llgo:tls -var Target int +var target int -//go:linkname Alias example.com/target.Value +//go:linkname alias example.com/target.value //llgo:tls -var Alias = 1 +var alias = 1 `), 0o644); err != nil { t.Fatal(err) } conf := NewDefaultConf(ModeGen) - if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "linkname alias") { - t.Fatalf("Do error = %v, want locality alias initializer diagnostic", err) + if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "cannot apply to a //go:linkname variable") { + t.Fatalf("Do error = %v, want locality linkname diagnostic", err) } } diff --git a/internal/locality/layout/layout.go b/internal/locality/layout/layout.go index 0589c39f1f..2c8b8fbdfb 100644 --- a/internal/locality/layout/layout.go +++ b/internal/locality/layout/layout.go @@ -184,8 +184,8 @@ func (p Package) Initializers(kind locality.Kind) []Initializer { // BlockName returns the shared package-block accessor symbol. func BlockName(path string) string { return qualify(path, "__llgo_local_block") } -// BlockKeyName returns the shared package-block descriptor symbol. -func BlockKeyName(path string) string { return qualify(path, "__llgo_local_key") } +// BlockCacheName returns the package block's owner-local direct-cache symbol. +func BlockCacheName(path string) string { return qualify(path, "__llgo_local_cache") } // InitName returns the package/kind initializer dispatcher symbol. func InitName(path string, kind locality.Kind) string { @@ -198,8 +198,10 @@ func EnsureName(path string, kind locality.Kind) string { return InitName(path, // GuardName returns the package/kind native TLS state symbol. func GuardName(path string, kind locality.Kind) string { return InitName(path, kind) + "$guard" } -// FailureKeyName returns the package/kind initializer failure key symbol. -func FailureKeyName(path string, kind locality.Kind) string { return InitName(path, kind) + "$failure" } +// FailureCacheName returns the package/kind initializer failure-cache symbol. +func FailureCacheName(path string, kind locality.Kind) string { + return InitName(path, kind) + "$failure_cache" +} func qualify(path, name string) string { if path == "" { diff --git a/internal/locality/layout/layout_test.go b/internal/locality/layout/layout_test.go index 3aabeb2b79..af2527e3af 100644 --- a/internal/locality/layout/layout_test.go +++ b/internal/locality/layout/layout_test.go @@ -101,7 +101,7 @@ func TestNames(t *testing.T) { if got := BlockName("example.com/p"); got != "example.com/p.__llgo_local_block" { t.Fatal(got) } - if got := BlockKeyName("example.com/p"); got != "example.com/p.__llgo_local_key" { + if got := BlockCacheName("example.com/p"); got != "example.com/p.__llgo_local_cache" { t.Fatal(got) } if got := InitName("example.com/p", locality.Thread); got != "example.com/p.__llgo_tls_init" { @@ -113,7 +113,7 @@ func TestNames(t *testing.T) { if got := GuardName("", locality.Thread); got != "__llgo_tls_init$guard" { t.Fatal(got) } - if got := FailureKeyName("", locality.Goroutine); got != "__llgo_gls_init$failure" { + if got := FailureCacheName("", locality.Goroutine); got != "__llgo_gls_init$failure_cache" { t.Fatal(got) } } diff --git a/internal/locality/locality_test.go b/internal/locality/locality_test.go index 4a8ac7d44b..f46bcccc2d 100644 --- a/internal/locality/locality_test.go +++ b/internal/locality/locality_test.go @@ -121,6 +121,16 @@ func TestScanPackageVarBranches(t *testing.T) { decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:tls"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("_")}}}}, want: "blank identifier", }, + { + name: "exported name", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:gls"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("Value")}}}}, + want: "requires an unexported package variable", + }, + { + name: "linkname", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//go:linkname value example.com/p.value\n//llgo:tls"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("value")}}}}, + want: "cannot apply to a //go:linkname variable", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -285,7 +295,7 @@ func TestPrepareIsIdempotentAcrossPrograms(t *testing.T) { fset, file := parseFile(t, `package p func makeValue() *int { value := 42; return &value } //llgo:tls -var Value = makeValue() +var value = makeValue() `) files := []*ast.File{file} info := newTypeInfo() @@ -308,9 +318,9 @@ var Value = makeValue() } declCount := len(file.Decls) scopeCount := len(pkg.Scope().Names()) - initName := prepared["Value"].InitFunc - if initName != "example.com/p.__llgo_local_init_0" || prepared["Value"].InitOrder != 1 { - t.Fatalf("prepared metadata = %+v", prepared["Value"]) + initName := prepared["value"].InitFunc + if initName != "example.com/p.__llgo_local_init_0" || prepared["value"].InitOrder != 1 { + t.Fatalf("prepared metadata = %+v", prepared["value"]) } again, err := Prepare(fset, pkg.Path(), pkg, info, files, prepared) @@ -324,8 +334,8 @@ var Value = makeValue() if len(file.Decls) != declCount || len(pkg.Scope().Names()) != scopeCount { t.Fatalf("repeated Prepare changed syntax/scope: decls=%d/%d scope=%d/%d", len(file.Decls), declCount, len(pkg.Scope().Names()), scopeCount) } - if again["Value"] != prepared["Value"] || reused["Value"] != prepared["Value"] { - t.Fatalf("repeated metadata = %+v, reused = %+v, want %+v", again["Value"], reused["Value"], prepared["Value"]) + if again["value"] != prepared["value"] || reused["value"] != prepared["value"] { + t.Fatalf("repeated metadata = %+v, reused = %+v, want %+v", again["value"], reused["value"], prepared["value"]) } } diff --git a/internal/locality/scan.go b/internal/locality/scan.go index b458a81795..5032133baa 100644 --- a/internal/locality/scan.go +++ b/internal/locality/scan.go @@ -62,10 +62,16 @@ func ScanPackageVar(fset *token.FileSet, decl *ast.GenDecl) ([]Variable, error) if hasDirective(decl.Doc, "go:embed") || hasDirective(spec.Doc, "go:embed") { return nil, errorAt(fset, spec.Pos(), "%s and //go:embed cannot apply to the same variable declaration", Directive(kind)) } + if hasDirective(decl.Doc, "go:linkname") || hasDirective(spec.Doc, "go:linkname") { + return nil, errorAt(fset, spec.Pos(), "%s cannot apply to a //go:linkname variable", Directive(kind)) + } for _, ident := range spec.Names { if ident.Name == "_" { return nil, errorAt(fset, ident.Pos(), "locality directive cannot apply to the blank identifier") } + if ast.IsExported(ident.Name) { + return nil, errorAt(fset, ident.Pos(), "%s requires an unexported package variable", Directive(kind)) + } ret = append(ret, Variable{ Name: ident.Name, Info: Info{Locality: kind, HasInitializer: len(spec.Values) != 0}, diff --git a/runtime/internal/runtime/local_context.go b/runtime/internal/runtime/local_context.go index 9ce43dcdac..5425984bb6 100644 --- a/runtime/internal/runtime/local_context.go +++ b/runtime/internal/runtime/local_context.go @@ -24,16 +24,15 @@ import "unsafe" // one-thread-per-goroutine backend maps both logical locality kinds to this one // physical package store. type LocalContext struct { - // blocks points at the payload of the most recently used package block. - // Keeping the aligned payload at the head makes the common lookup return it - // directly; the block header is stored immediately before the payload. + // blocks points at the payload of the most recently allocated local block. + // The list keeps every block reachable from the outer Go entry stack frame. blocks unsafe.Pointer } type localBlock struct { // next points at the next block's payload, not its header. - next unsafe.Pointer - key unsafe.Pointer + next unsafe.Pointer + cacheSlot *uintptr } func EnterLocalContext(ctx *LocalContext) uintptr { @@ -78,57 +77,40 @@ func releaseLocalBlocks(ctx *LocalContext) { next := block.next // Do not free block here: an address of a local variable may outlive its // owner. Breaking the links lets the GC retain only escaped blocks. + *block.cacheSlot = 0 block.next = nil + block.cacheSlot = nil data = next } } -// LocalPackage returns stable, zeroed storage for one package in the current -// physical owner. Repeated access to the most recently used package takes the -// head fast path; other accesses move the matching block to the front. -func LocalPackage(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { - ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) - if ctx != nil { - firstData := ctx.blocks - if firstData != nil && localBlockHeader(firstData).key == key { - return firstData - } - } - return localPackageSlow(ctx, key, size, align) -} - +// LocalPackage creates stable, zeroed storage for one generated cache slot in +// the current physical owner. Generated accessors load the slot directly after +// first touch; the block list is retained only as a GC root and teardown list. +// //go:noinline -func localPackageSlow(ctx *LocalContext, key unsafe.Pointer, size, align uintptr) unsafe.Pointer { +func LocalPackage(cacheSlot *uintptr, size, align uintptr) unsafe.Pointer { + ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) if ctx == nil { panic("runtime: local variable accessed outside a Go entry context") } - if key == nil { - panic("runtime: nil local package key") + if cacheSlot == nil { + panic("runtime: nil local cache slot") + } + if data := unsafe.Pointer(*cacheSlot); data != nil { + return data } if align == 0 || align&(align-1) != 0 { panic("runtime: invalid local package alignment") } - firstData := ctx.blocks - var previous *localBlock - for data := firstData; data != nil; { - block := localBlockHeader(data) - next := block.next - if block.key == key { - previous.next = next - block.next = firstData - ctx.blocks = data - return data - } - previous = block - data = next - } - data := newLocalBlock(key, size, align) - localBlockHeader(data).next = firstData + data := newLocalBlock(cacheSlot, size, align) + localBlockHeader(data).next = ctx.blocks ctx.blocks = data + *cacheSlot = uintptr(data) return data } -func newLocalBlock(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { +func newLocalBlock(cacheSlot *uintptr, size, align uintptr) unsafe.Pointer { header := unsafe.Sizeof(localBlock{}) padding := align - 1 if size == 0 { @@ -143,7 +125,7 @@ func newLocalBlock(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { } data := unsafe.Pointer((uintptr(allocation) + header + padding) &^ padding) block := localBlockHeader(data) - block.key = key + block.cacheSlot = cacheSlot return data } diff --git a/runtime/internal/runtime/local_context_stub.go b/runtime/internal/runtime/local_context_stub.go index ebd713e575..4dd029e4ef 100644 --- a/runtime/internal/runtime/local_context_stub.go +++ b/runtime/internal/runtime/local_context_stub.go @@ -30,6 +30,13 @@ func LeaveLocalContext(ctx *LocalContext, previous uintptr) {} func leaveCurrentLocalContext() {} -func LocalPackage(key unsafe.Pointer, size, align uintptr) unsafe.Pointer { - return AllocZ(size) +func LocalPackage(cacheSlot *uintptr, size, align uintptr) unsafe.Pointer { + if cacheSlot != nil && *cacheSlot != 0 { + return unsafe.Pointer(*cacheSlot) + } + data := AllocZ(size) + if cacheSlot != nil { + *cacheSlot = uintptr(data) + } + return data } diff --git a/runtime/internal/runtime/local_initializer.go b/runtime/internal/runtime/local_initializer.go index 58cb8a7fc0..71fa122eda 100644 --- a/runtime/internal/runtime/local_initializer.go +++ b/runtime/internal/runtime/local_initializer.go @@ -28,14 +28,14 @@ const ( // EnsureLocalInitializer executes one package/locality dispatcher at most once // in the current owner. Recursive access observes partial initialization; a // recovered failure remains failed and re-panics on every later access. -func EnsureLocalInitializer(state *uint8, failureKey unsafe.Pointer, initialize func()) { +func EnsureLocalInitializer(state *uint8, failureCache *uintptr, initialize func()) { switch *state { case localInitReady: return case localInitInitializing: return case localInitFailed: - panic(*localInitializerFailure(failureKey)) + panic(*localInitializerFailure(failureCache)) case localInitUninitialized: default: panic("runtime: invalid local initializer state") @@ -47,7 +47,7 @@ func EnsureLocalInitializer(state *uint8, failureKey unsafe.Pointer, initialize return } value := recover() - *localInitializerFailure(failureKey) = value + *localInitializerFailure(failureCache) = value *state = localInitFailed panic(value) }() @@ -56,7 +56,7 @@ func EnsureLocalInitializer(state *uint8, failureKey unsafe.Pointer, initialize *state = localInitReady } -func localInitializerFailure(key unsafe.Pointer) *any { +func localInitializerFailure(cache *uintptr) *any { var value any - return (*any)(LocalPackage(key, unsafe.Sizeof(value), unsafe.Alignof(value))) + return (*any)(LocalPackage(cache, unsafe.Sizeof(value), unsafe.Alignof(value))) } diff --git a/ssa/local_context.go b/ssa/local_context.go index cec3e2878a..184b2b5ec4 100644 --- a/ssa/local_context.go +++ b/ssa/local_context.go @@ -21,11 +21,7 @@ import ( "go/types" ) -const ( - runtimeCurrentLocalContext = "currentLocalContext" - runtimeLocalContext = "LocalContext" - runtimeLocalBlock = "localBlock" -) +const runtimeLocalContext = "LocalContext" // EnterLocalContext creates the stack root used by TLS/GLS locality blocks and // installs it for the current outermost Go entry. previous is nonzero only for @@ -46,64 +42,23 @@ func (b Builder) LeaveLocalContext(ctx, previous Expr) { b.Call(b.Pkg.rtFunc("LeaveLocalContext"), ctx, previous) } -// BuildLocalPackageAccessor builds an always-hot package block lookup around -// LocalPackage. Runtime Go types and locality metadata are the ABI: they supply -// the context/header layouts and whether the context anchor is native TLS or an -// ordinary global. The generated code therefore does not duplicate target -// selection or byte offsets. -func (p Function) BuildLocalPackageAccessor(key, size, align Expr) { +// BuildLocalPackageAccessor builds an always-hot package block lookup around a +// generated owner-local cache slot. The runtime owns allocation and rooting; +// the compiler needs no LocalContext or block-header layout knowledge. +func (p Function) BuildLocalPackageAccessor(cache, size, align Expr) { prog := p.Prog - runtimePkg := prog.runtime() - contextType := runtimePkg.Scope().Lookup(runtimeLocalContext).Type() - blockType := runtimePkg.Scope().Lookup(runtimeLocalBlock).Type() - _, contextBlocks, _ := types.LookupFieldOrMethod(contextType, true, runtimePkg, "blocks") - _, blockKey, _ := types.LookupFieldOrMethod(blockType, true, runtimePkg, "key") - - b := p.MakeBody(5) - checkHead := p.Block(1) - checkKey := p.Block(2) - hit := p.Block(3) - slow := p.Block(4) - key = b.Convert(prog.VoidPtr(), key) - - current := b.Load(p.Pkg.runtimeGlobal(runtimeCurrentLocalContext).Expr) - hasContext := b.BinOp(token.NEQ, current, prog.IntVal(0, prog.Uintptr())) - b.If(hasContext, checkHead, slow) - - b.SetBlock(checkHead) - context := b.Convert(prog.Pointer(prog.Type(contextType, InGo)), current) - head := b.Load(b.FieldAddr(context, contextBlocks[0])) - hasHead := b.BinOp(token.NEQ, head, prog.Nil(head.Type)) - b.If(hasHead, checkKey, slow) - - b.SetBlock(checkKey) - headerAddress := b.BinOp( - token.SUB, - b.Convert(prog.Uintptr(), head), - prog.IntVal(prog.SizeOf(prog.Type(blockType, InGo)), prog.Uintptr()), - ) - header := b.Convert(prog.Pointer(prog.Type(blockType, InGo)), headerAddress) - foundKey := b.Load(b.FieldAddr(header, blockKey[0])) - b.If(b.BinOp(token.EQL, foundKey, key), hit, slow) + b := p.MakeBody(3) + hit := p.Block(1) + slow := p.Block(2) + cached := b.Load(cache) + b.If(b.BinOp(token.NEQ, cached, prog.IntVal(0, prog.Uintptr())), hit, slow) b.SetBlock(hit) result := p.raw.Type.(*types.Signature).Results().At(0).Type() - b.Return(b.Convert(prog.rawType(result), head)) + b.Return(b.Convert(prog.rawType(result), cached)) b.SetBlock(slow) - raw := b.Call(p.Pkg.rtFunc("LocalPackage"), key, size, align) + raw := b.Call(p.Pkg.rtFunc("LocalPackage"), cache, size, align) b.Return(b.Convert(prog.rawType(result), raw)) b.EndBuild() } - -func (p Package) runtimeGlobal(name string) Global { - p.NeedRuntime = true - runtimePkg := p.Prog.runtime() - variable := runtimePkg.Scope().Lookup(name).(*types.Var) - fullName := FullName(runtimePkg, name) - typ := types.NewPointer(variable.Type()) - if locality, ok := p.Prog.VariableLocality(fullName); ok && locality.LocalStorage == LocalStorageNativeTLS { - return p.NewThreadLocalVar(fullName, typ, InGo) - } - return p.NewVar(fullName, typ, InGo) -} diff --git a/ssa/local_context_test.go b/ssa/local_context_test.go index 9517b13a9f..624696e635 100644 --- a/ssa/local_context_test.go +++ b/ssa/local_context_test.go @@ -24,50 +24,32 @@ import ( ) func TestBuildLocalPackageAccessor(t *testing.T) { - for _, test := range []struct { - name string - nativeAnchor bool - declaration string - }{ - {"native TLS anchor", true, "external thread_local global i64"}, - {"ordinary anchor", false, "external global i64"}, - } { - t.Run(test.name, func(t *testing.T) { - prog := NewProgram(nil) - prog.SetRuntime(localContextTestRuntime()) - anchorName := PkgRuntime + "." + runtimeCurrentLocalContext - if test.nativeAnchor { - prog.SetLocalityInfo(anchorName, LocalityInfo{Locality: ThreadLocal}) - prog.SetLocalStorage(anchorName, LocalStorageNativeTLS) - } - - pkg := prog.NewPackage("accessor", "example.com/accessor") - key := pkg.NewVar("example.com/accessor.key", types.NewPointer(types.Typ[types.Uint8]), InGo) - key.InitNil() - field := types.NewField(token.NoPos, nil, "pointer", types.NewPointer(types.Typ[types.Int]), false) - block := types.NewStruct([]*types.Var{field}, nil) - blockType := prog.Type(block, InGo) - result := types.NewPointer(block) - results := types.NewTuple(types.NewVar(token.NoPos, nil, "", result)) - accessor := pkg.NewFunc("example.com/accessor.block", types.NewSignatureType(nil, nil, nil, nil, results, false), InGo) - accessor.BuildLocalPackageAccessor( - key.Expr, - prog.IntVal(prog.SizeOf(blockType), prog.Uintptr()), - prog.IntVal(prog.AlignOf(blockType), prog.Uintptr()), - ) + prog := NewProgram(nil) + prog.SetRuntime(localContextTestRuntime()) + pkg := prog.NewPackage("accessor", "example.com/accessor") + cache := pkg.NewThreadLocalVar("example.com/accessor.cache", types.NewPointer(types.Typ[types.Uintptr]), InGo) + cache.InitNil() + field := types.NewField(token.NoPos, nil, "pointer", types.NewPointer(types.Typ[types.Int]), false) + block := types.NewStruct([]*types.Var{field}, nil) + blockType := prog.Type(block, InGo) + result := types.NewPointer(block) + results := types.NewTuple(types.NewVar(token.NoPos, nil, "", result)) + accessor := pkg.NewFunc("example.com/accessor.block", types.NewSignatureType(nil, nil, nil, nil, results, false), InGo) + accessor.BuildLocalPackageAccessor( + cache.Expr, + prog.IntVal(prog.SizeOf(blockType), prog.Uintptr()), + prog.IntVal(prog.AlignOf(blockType), prog.Uintptr()), + ) - ir := pkg.String() - anchor := `@"` + anchorName + `" = ` + test.declaration - if !strings.Contains(ir, anchor) { - t.Fatalf("context anchor declaration not found: %s\n%s", anchor, ir) - } - if got := strings.Count(ir, "icmp "); got != 3 { - t.Fatalf("accessor comparisons = %d, want context/head/key checks:\n%s", got, ir) - } - if !strings.Contains(ir, `call ptr @"`+PkgRuntime+`.LocalPackage"`) { - t.Fatalf("accessor has no LocalPackage slow path:\n%s", ir) - } - }) + ir := pkg.String() + if !strings.Contains(ir, `@"example.com/accessor.cache" = thread_local global i64 0`) { + t.Fatalf("direct cache definition not found:\n%s", ir) + } + if got := strings.Count(ir, "icmp "); got != 1 { + t.Fatalf("accessor comparisons = %d, want one cache check:\n%s", got, ir) + } + if !strings.Contains(ir, `call ptr @"`+PkgRuntime+`.LocalPackage"(ptr @"example.com/accessor.cache"`) { + t.Fatalf("accessor has no cache-backed LocalPackage slow path:\n%s", ir) } } @@ -75,22 +57,8 @@ func localContextTestRuntime() *types.Package { pkg := types.NewPackage(PkgRuntime, "runtime") unsafePointer := types.Typ[types.UnsafePointer] - contextName := types.NewTypeName(token.NoPos, pkg, runtimeLocalContext, nil) - contextFields := []*types.Var{types.NewField(token.NoPos, pkg, "blocks", unsafePointer, false)} - types.NewNamed(contextName, types.NewStruct(contextFields, nil), nil) - pkg.Scope().Insert(contextName) - - blockName := types.NewTypeName(token.NoPos, pkg, runtimeLocalBlock, nil) - blockFields := []*types.Var{ - types.NewField(token.NoPos, pkg, "next", unsafePointer, false), - types.NewField(token.NoPos, pkg, "key", unsafePointer, false), - } - types.NewNamed(blockName, types.NewStruct(blockFields, nil), nil) - pkg.Scope().Insert(blockName) - pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, runtimeCurrentLocalContext, types.Typ[types.Uintptr])) - params := types.NewTuple( - types.NewVar(token.NoPos, pkg, "key", unsafePointer), + types.NewVar(token.NoPos, pkg, "cache", types.NewPointer(types.Typ[types.Uintptr])), types.NewVar(token.NoPos, pkg, "size", types.Typ[types.Uintptr]), types.NewVar(token.NoPos, pkg, "align", types.Typ[types.Uintptr]), ) diff --git a/ssa/locality.go b/ssa/locality.go index 4cfbb68fa5..a8e9a62a7c 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -86,8 +86,8 @@ func (p Program) VariableLocality(name string) (VariableLocality, bool) { return info, ok } -// ResolveLocality follows linkname aliases and returns the canonical declaration -// name together with its merged locality metadata. +// ResolveLocality returns locality metadata for one declaration. Locality +// variables cannot participate in go:linkname alias chains. func (p Program) ResolveLocality(name string) (string, VariableLocality, bool, error) { lookup := func(name string) (VariableLocality, bool) { p.localities.mu.RLock() @@ -112,27 +112,17 @@ func resolveLocality(lookup func(string) (VariableLocality, bool), linkname func seen[current] = true target, hasLink := linkname(current) target = strings.TrimPrefix(target, "go:") - if !hasLink || target == "" || target == current { + if !hasLink || target == "" { return current, result, ok, nil } - targetInfo, exists := lookup(target) - if exists && targetInfo.Locality != locality.None { - switch { - case result.Locality == locality.None: - result = targetInfo - ok = true - case result.Locality != targetInfo.Locality: - return "", VariableLocality{}, false, fmt.Errorf("linkname alias %s uses %s but target %s uses %s", name, locality.Directive(result.Locality), target, locality.Directive(targetInfo.Locality)) - case hasInitialization(result.Info) && hasInitialization(targetInfo.Info) && result.Info != targetInfo.Info: - return "", VariableLocality{}, false, fmt.Errorf("linkname alias %s and target %s have incompatible local initializers", name, target) - case !hasInitialization(result.Info): - result.Info = targetInfo.Info - } - if result.LocalStorage == LocalStorageUnknown { - result.LocalStorage = targetInfo.LocalStorage - } else if targetInfo.LocalStorage != LocalStorageUnknown && result.LocalStorage != targetInfo.LocalStorage { - return "", VariableLocality{}, false, fmt.Errorf("linkname alias %s and target %s have incompatible local storage", name, target) - } + if currentInfo, exists := lookup(current); exists && currentInfo.Locality != locality.None { + return "", VariableLocality{}, false, fmt.Errorf("local variable %s cannot use go:linkname", current) + } + if targetInfo, exists := lookup(target); exists && targetInfo.Locality != locality.None { + return "", VariableLocality{}, false, fmt.Errorf("go:linkname alias %s cannot reference local variable %s", name, target) + } + if target == current { + return current, result, ok, nil } current = target } @@ -145,13 +135,32 @@ func hasInitialization(info locality.Info) bool { func (p Program) ValidateLocalities(pkgPath string) error { prefix := pkgPath + "." p.localities.mu.RLock() - names := make([]string, 0) - for name := range p.localities.entries { + nameSet := make(map[string]bool) + localNames := make(map[string]bool) + for name, info := range p.localities.entries { + if info.Locality != locality.None { + localNames[name] = true + } if strings.HasPrefix(name, prefix) { - names = append(names, name) + nameSet[name] = true } } p.localities.mu.RUnlock() + p.linknameMu.RLock() + links := make(map[string]string, len(p.linkname)) + for name, target := range p.linkname { + links[name] = strings.TrimPrefix(target, "go:") + } + p.linknameMu.RUnlock() + for name := range links { + if strings.HasPrefix(name, prefix) && linknameReachesLocal(name, links, localNames) { + nameSet[name] = true + } + } + names := make([]string, 0, len(nameSet)) + for name := range nameSet { + names = append(names, name) + } sort.Strings(names) for _, name := range names { if _, _, _, err := p.ResolveLocality(name); err != nil { @@ -161,6 +170,18 @@ func (p Program) ValidateLocalities(pkgPath string) error { return nil } +func linknameReachesLocal(name string, links map[string]string, localNames map[string]bool) bool { + seen := make(map[string]bool) + for name != "" && !seen[name] { + if localNames[name] { + return true + } + seen[name] = true + name = links[name] + } + return false +} + func (p Program) PackageSyntaxParsed(pkg *types.Package) bool { p.localities.mu.RLock() _, ok := p.localities.parsedPackages[pkg] diff --git a/ssa/locality_test.go b/ssa/locality_test.go index 991c80808d..e931797443 100644 --- a/ssa/locality_test.go +++ b/ssa/locality_test.go @@ -86,74 +86,50 @@ func TestNeedsLocalContext(t *testing.T) { } } -func TestResolveLinknameLocality(t *testing.T) { +func TestRejectsLinknameLocality(t *testing.T) { prog := NewProgram(nil) - target := "example.com/target.Value" - alias := "example.com/alias.Value" + target := "example.com/target.value" + alias := "example.com/alias.value" prog.SetLocalityInfo(target, LocalityInfo{Locality: ThreadLocal, HasInitializer: true, InitFunc: "example.com/target.initValue", InitOrder: 1}) prog.SetLocalStorage(target, LocalStoragePackage) - prog.SetLinkname(alias, target) - - _, got, ok, err := prog.ResolveLocality(alias) - if err != nil { - t.Fatal(err) - } - if !ok || got.Locality != ThreadLocal || got.LocalStorage != LocalStoragePackage || got.InitFunc != "example.com/target.initValue" || got.InitOrder != 1 { - t.Fatalf("ResolveLocality(%q) = %+v, %v", alias, got, ok) + if canonical, got, ok, err := prog.ResolveLocality(target); err != nil || canonical != target || !ok || got.LocalStorage != LocalStoragePackage { + t.Fatalf("direct ResolveLocality(%q) = %q, %+v, %v, %v", target, canonical, got, ok, err) } - if err := prog.ValidateLocalities("example.com/alias"); err != nil { - t.Fatal(err) - } - sameKind := "example.com/alias.SameKind" - prog.SetLinkname(sameKind, target) - prog.SetLocalityInfo(sameKind, LocalityInfo{Locality: ThreadLocal}) - if _, got, ok, err := prog.ResolveLocality(sameKind); err != nil || !ok || got.InitFunc != "example.com/target.initValue" { - t.Fatalf("same-kind ResolveLocality(%q) = %+v, %v", sameKind, got, ok) - } - - incompatible := "example.com/alias.Incompatible" - prog.SetLinkname(incompatible, target) - prog.SetLocalityInfo(incompatible, LocalityInfo{Locality: ThreadLocal, HasInitializer: true, InitFunc: "example.com/alias.initValue", InitOrder: 1}) - if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "incompatible local initializers") { - t.Fatalf("initializer mismatch error = %v", err) - } - targetDecl, _ := prog.VariableLocality(target) - prog.SetLocalityInfo(incompatible, targetDecl.Info) - storageMismatch := "example.com/alias.StorageMismatch" - prog.SetLinkname(storageMismatch, target) - prog.SetLocalityInfo(storageMismatch, LocalityInfo{Locality: ThreadLocal}) - prog.SetLocalStorage(storageMismatch, LocalStorageNativeTLS) - if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "incompatible local storage") { - t.Fatalf("storage mismatch error = %v", err) - } - prog.SetLocalStorage(storageMismatch, LocalStoragePackage) + prog.SetLinkname(alias, target) + if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "cannot reference local variable") { + t.Fatalf("alias-to-local error = %v", err) + } - prog.SetLocalityInfo(alias, LocalityInfo{Locality: GoroutineLocal}) - if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "uses //llgo:gls") { - t.Fatalf("locality mismatch error = %v", err) + localAlias := "example.com/alias.local" + prog.SetLocalityInfo(localAlias, LocalityInfo{Locality: GoroutineLocal}) + prog.SetLinkname(localAlias, "example.com/target.ordinary") + if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("local-alias error = %v", err) } } -func TestValidateLocalityLinknameCycle(t *testing.T) { +func TestValidateLocalitiesIgnoresOrdinaryLinknameCycle(t *testing.T) { prog := NewProgram(nil) - prog.SetLinkname("example.com/p.First", "example.com/p.Second") - prog.SetLinkname("example.com/p.Second", "example.com/p.First") - prog.SetLocalityInfo("example.com/p.First", LocalityInfo{Locality: ThreadLocal}) - if err := prog.ValidateLocalities("example.com/p"); err == nil || !strings.Contains(err.Error(), "linkname cycle") { - t.Fatalf("linkname cycle error = %v", err) + prog.SetLinkname("example.com/p.first", "example.com/p.second") + prog.SetLinkname("example.com/p.second", "example.com/p.first") + if err := prog.ValidateLocalities("example.com/p"); err != nil { + t.Fatalf("ordinary linkname cycle affected locality validation: %v", err) } } -func TestValidateLocalityAllowsSelfLinkname(t *testing.T) { +func TestValidateLocalitySelfLinkname(t *testing.T) { prog := NewProgram(nil) - name := "example.com/p.Value" + name := "example.com/p.value" prog.SetLinkname(name, name) - prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) if err := prog.ValidateLocalities("example.com/p"); err != nil { t.Fatal(err) } - if _, got, ok, err := prog.ResolveLocality(name); err != nil || !ok || got.Locality != ThreadLocal { - t.Fatalf("ResolveLocality(%q) = %+v, %v", name, got, ok) + if canonical, _, ok, err := prog.ResolveLocality(name); err != nil || canonical != name || ok { + t.Fatalf("ordinary self-link ResolveLocality(%q) = %q, %v, %v", name, canonical, ok, err) + } + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) + if err := prog.ValidateLocalities("example.com/p"); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("local self-linkname error = %v", err) } } diff --git a/test/llgoext/locality_test.go b/test/llgoext/locality_test.go index 796f320d47..3f839419ba 100644 --- a/test/llgoext/locality_test.go +++ b/test/llgoext/locality_test.go @@ -105,7 +105,7 @@ func TestTLSAndGLSIsolation(t *testing.T) { } } -func TestLocalPackageMoveToFront(t *testing.T) { +func TestLocalPackageDirectCaches(t *testing.T) { type result struct { local int imported int @@ -113,13 +113,13 @@ func TestLocalPackageMoveToFront(t *testing.T) { done := make(chan result) go func() { glsCounter = 41 - localityscope.First = 51 + localityscope.SetFirst(51) glsCounter++ - localityscope.First++ - done <- result{local: glsCounter, imported: localityscope.First} + imported := localityscope.IncrementFirst() + done <- result{local: glsCounter, imported: imported} }() if got := <-done; got != (result{local: 42, imported: 52}) { - t.Fatalf("local package values after move-to-front = %+v", got) + t.Fatalf("local package values through direct caches = %+v", got) } } @@ -375,12 +375,12 @@ func TestInitializerScopeRunsOncePerPackageKind(t *testing.T) { } done := make(chan result) go func() { - firstValue := localityscope.First + firstValue := localityscope.First() firstCalls := localityscope.FirstCalls() secondCalls := localityscope.SecondCalls() - _ = localityscope.First + _ = localityscope.First() firstCallsAgain := localityscope.FirstCalls() - secondValue := localityscope.Second + secondValue := localityscope.Second() done <- result{ firstValue: firstValue, firstCalls: firstCalls, @@ -411,9 +411,9 @@ func TestMultiValueInitializerUsesOneGroup(t *testing.T) { } done := make(chan result) go func() { - first := localityscope.PairFirst + first := localityscope.PairFirst() afterFirst := localityscope.PairCalls() - second := localityscope.PairSecond + second := localityscope.PairSecond() done <- result{first, second, afterFirst, localityscope.PairCalls()} }() got := <-done @@ -432,12 +432,12 @@ func TestCrossPackageMixedInitializerGroup(t *testing.T) { } done := make(chan result) go func() { - scalar := localityscope.MixedScalar - address := &localityscope.MixedScalar + scalar := localityscope.MixedScalar() + address := localityscope.MixedScalarAddress() done <- result{ scalar: scalar, - pointer: localityscope.MixedPointer, - addressStable: address == &localityscope.MixedScalar, + pointer: localityscope.MixedPointer(), + addressStable: address == localityscope.MixedScalarAddress(), calls: localityscope.MixedCalls(), } }() @@ -499,6 +499,11 @@ func bumpGLSPackageBlock() int { return benchmarkGLSPackage.value } +//go:noinline +func readGLSPackageBlock() uintptr { + return uintptr(benchmarkGLSPackage.value) +} + func BenchmarkOrdinaryGlobal(b *testing.B) { value := 0 for i := 0; i < b.N; i++ { @@ -583,6 +588,19 @@ func BenchmarkComparableGLSPackageRead(b *testing.B) { benchmarkReadSink = value } +func BenchmarkAlternatingGLSPackageRead(b *testing.B) { + localitybench.PrepareReads() + benchmarkGLSPackage.pointer = &benchmarkSink + benchmarkGLSPackage.value = 1 + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += readGLSPackageBlock() + value += localitybench.ReadGLSPackage() + } + benchmarkReadSink = value +} + func BenchmarkGoroutineEntry(b *testing.B) { for i := 0; i < b.N; i++ { done := make(chan struct{}) diff --git a/test/llgoext/localitymulti/locality_test.go b/test/llgoext/localitymulti/locality_test.go new file mode 100644 index 0000000000..de1d8afe3e --- /dev/null +++ b/test/llgoext/localitymulti/locality_test.go @@ -0,0 +1,115 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localitymulti + +import ( + "testing" + + localityblock0 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p0" + localityblock1 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p1" + localityblock2 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p2" + localityblock3 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p3" + localityblock4 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p4" + localityblock5 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p5" + localityblock6 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p6" + localityblock7 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p7" +) + +var benchmarkSink uintptr + +func TestGLSPackageWorkingSet(t *testing.T) { + prepare := []func(){ + localityblock0.Prepare, + localityblock1.Prepare, + localityblock2.Prepare, + localityblock3.Prepare, + localityblock4.Prepare, + localityblock5.Prepare, + localityblock6.Prepare, + localityblock7.Prepare, + } + read := []func() uintptr{ + localityblock0.Read, + localityblock1.Read, + localityblock2.Read, + localityblock3.Read, + localityblock4.Read, + localityblock5.Read, + localityblock6.Read, + localityblock7.Read, + } + for i := range prepare { + prepare[i]() + if got := read[i](); got == 0 { + t.Fatalf("package %d GLS pointer is nil", i) + } + } +} + +func BenchmarkGLSPackageWorkingSet2(b *testing.B) { + localityblock0.Prepare() + localityblock1.Prepare() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localityblock0.Read() + value += localityblock1.Read() + } + benchmarkSink = value +} + +func BenchmarkGLSPackageWorkingSet4(b *testing.B) { + localityblock0.Prepare() + localityblock1.Prepare() + localityblock2.Prepare() + localityblock3.Prepare() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localityblock0.Read() + value += localityblock1.Read() + value += localityblock2.Read() + value += localityblock3.Read() + } + benchmarkSink = value +} + +func BenchmarkGLSPackageWorkingSet8(b *testing.B) { + localityblock0.Prepare() + localityblock1.Prepare() + localityblock2.Prepare() + localityblock3.Prepare() + localityblock4.Prepare() + localityblock5.Prepare() + localityblock6.Prepare() + localityblock7.Prepare() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localityblock0.Read() + value += localityblock1.Read() + value += localityblock2.Read() + value += localityblock3.Read() + value += localityblock4.Read() + value += localityblock5.Read() + value += localityblock6.Read() + value += localityblock7.Read() + } + benchmarkSink = value +} diff --git a/test/llgoext/testdata/localityblocks/p0/block.go b/test/llgoext/testdata/localityblocks/p0/block.go new file mode 100644 index 0000000000..b4978171e8 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p0/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p0 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p1/block.go b/test/llgoext/testdata/localityblocks/p1/block.go new file mode 100644 index 0000000000..1c83a1dc19 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p1/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p1 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p2/block.go b/test/llgoext/testdata/localityblocks/p2/block.go new file mode 100644 index 0000000000..440411923c --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p2/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p2 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p3/block.go b/test/llgoext/testdata/localityblocks/p3/block.go new file mode 100644 index 0000000000..2da455b714 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p3/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p3 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p4/block.go b/test/llgoext/testdata/localityblocks/p4/block.go new file mode 100644 index 0000000000..82fa5ee80d --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p4/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p4 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p5/block.go b/test/llgoext/testdata/localityblocks/p5/block.go new file mode 100644 index 0000000000..8e5151e02d --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p5/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p5 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p6/block.go b/test/llgoext/testdata/localityblocks/p6/block.go new file mode 100644 index 0000000000..e11b82c5d2 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p6/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p6 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p7/block.go b/test/llgoext/testdata/localityblocks/p7/block.go new file mode 100644 index 0000000000..6ebff03a61 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p7/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p7 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityscope/scope.go b/test/llgoext/testdata/localityscope/scope.go index dce9828dca..e20bd0a86a 100644 --- a/test/llgoext/testdata/localityscope/scope.go +++ b/test/llgoext/testdata/localityscope/scope.go @@ -32,13 +32,22 @@ func initSecond() int { } //llgo:gls -var First = initFirst() +var first = initFirst() //llgo:gls -var Second = initSecond() +var second = initSecond() func FirstCalls() int { return firstCalls } func SecondCalls() int { return secondCalls } +func First() int { return first } +func Second() int { return second } + +func SetFirst(value int) { first = value } + +func IncrementFirst() int { + first++ + return first +} var pairCalls int @@ -48,10 +57,13 @@ func initPair() (int, int) { } //llgo:gls -var PairFirst, PairSecond = initPair() +var pairFirst, pairSecond = initPair() func PairCalls() int { return pairCalls } +func PairFirst() int { return pairFirst } +func PairSecond() int { return pairSecond } + var mixedCalls int var mixedBacking = 500 @@ -61,6 +73,11 @@ func initMixed() (int, *int) { } //llgo:tls -var MixedScalar, MixedPointer = initMixed() +var mixedScalar, mixedPointer = initMixed() func MixedCalls() int { return mixedCalls } + +func MixedScalar() int { return mixedScalar } +func MixedPointer() *int { return mixedPointer } + +func MixedScalarAddress() *int { return &mixedScalar } From 99eadfcacec69680a85ff41d70c565543ed293b2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 16 Jul 2026 02:51:12 +0800 Subject: [PATCH 12/20] cl: cover locality validation and import lowering --- cl/locality.go | 16 +++------------- cl/locality_lower_test.go | 31 +++++++++++++++++++++++++++++++ cl/locality_test.go | 27 +++++++++++++++++++++++++++ ssa/locality_test.go | 9 +++++++-- 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/cl/locality.go b/cl/locality.go index 0961685f30..d9c561ae39 100644 --- a/cl/locality.go +++ b/cl/locality.go @@ -43,19 +43,16 @@ func PrepareLocalVariables(prog llssa.Program, fset *token.FileSet, pkg *types.P for name, local := range prepared { prog.SetLocalityInfo(llssa.FullName(pkg, name), local) } - for fullName, local := range prog.PackageLocalities(path) { + for fullName := range prog.PackageLocalities(path) { name := strings.TrimPrefix(fullName, path+".") object, _ := pkg.Scope().Lookup(name).(*types.Var) if object == nil { return fmt.Errorf("locality layout: package %s has no variable %s", path, name) } - canonical, _, _, err := prog.ResolveLocality(fullName) + _, _, _, err := prog.ResolveLocality(fullName) if err != nil { return err } - if canonical != fullName && local.HasInitializer { - return fmt.Errorf("locality layout: linkname alias %s cannot have an initializer", fullName) - } prog.SetLocalStorage(fullName, localitylayout.StorageForType(object.Type())) } _, err = planLocalPackage(prog, pkg) @@ -84,17 +81,10 @@ func planLocalPackage(prog llssa.Program, pkg *types.Package) (localitylayout.Pa decls := prog.PackageLocalities(path) input := make([]localitylayout.Declaration, 0, len(decls)) for fullName := range decls { - canonical, info, _, err := prog.ResolveLocality(fullName) + _, info, _, err := prog.ResolveLocality(fullName) if err != nil { return localitylayout.Package{}, err } - if canonical != fullName { - target, targetOK := prog.VariableLocality(canonical) - if !targetOK || target.Locality == locality.None { - return localitylayout.Package{}, fmt.Errorf("locality layout: linkname target %s for %s is not a local variable", canonical, fullName) - } - continue - } name := strings.TrimPrefix(fullName, prefix) object, _ := pkg.Scope().Lookup(name).(*types.Var) if object == nil { diff --git a/cl/locality_lower_test.go b/cl/locality_lower_test.go index 8d16ae9e26..8bae3152f3 100644 --- a/cl/locality_lower_test.go +++ b/cl/locality_lower_test.go @@ -73,6 +73,9 @@ func TestLocalityLoweringResolution(t *testing.T) { if got := ctx.localTypesPackage("example.com/loaded.Value"); got != loaded { t.Fatalf("loaded localTypesPackage = %v, want %v", got, loaded) } + if got := (&context{goProg: global.Pkg.Prog}).localTypesPackage(name); got != typesPkg { + t.Fatalf("SSA-program localTypesPackage = %v, want %v", got, typesPkg) + } if got := (&context{}).localTypesPackage("example.com/missing.Value"); got != nil { t.Fatalf("missing localTypesPackage = %v", got) } @@ -85,6 +88,34 @@ func TestLocalityLoweringResolution(t *testing.T) { } } +func TestLocalityLoweringDeclarationOnlyState(t *testing.T) { + typesPkg, global := localitySSAGlobal(t, "example.com/declaration") + name := llssa.FullName(typesPkg, global.Name()) + prog := ssatest.NewProgram(t, nil) + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLocalStorage(name, llssa.LocalStorageNativeTLS) + llvmPkg := prog.NewPackage(typesPkg.Name(), typesPkg.Path()) + ctx := &context{ + prog: prog, + pkg: llvmPkg, + goTyps: typesPkg, + locality: localityLowering{ + variables: make(map[*ssa.Global]*localVariable), + }, + } + + addr := ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + if addr != ctx.locality.variables[global].owner.direct[name].Expr { + t.Fatal("localVariableAddr did not return declaration-only TLS storage") + } + + owner := &localPackage{plan: localitylayout.Package{Path: typesPkg.Path()}} + initializer := ctx.buildLocalInitializer(llvmPkg, owner, locality.Thread, []localitylayout.Initializer{{Name: typesPkg.Path() + ".initLocal", Order: 1}}, false) + if initializer.dispatch.HasBody() || initializer.ensure.HasBody() { + t.Fatal("declaration-only initializer unexpectedly defined a body") + } +} + func TestLocalityLoweringDiagnostics(t *testing.T) { typesPkg, global := localitySSAGlobal(t, "example.com/diagnostic") name := llssa.FullName(typesPkg, global.Name()) diff --git a/cl/locality_test.go b/cl/locality_test.go index 6c0c6ea515..877313838c 100644 --- a/cl/locality_test.go +++ b/cl/locality_test.go @@ -563,6 +563,33 @@ var alias int } } +func TestNewPackageValidatesPreloadedLocalityMetadata(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality +var value int +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, files, info, true) + ssaPkg.Build() + + prog := ssatest.NewProgram(t, nil) + name := llssa.FullName(pkg, "value") + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLinkname(name, name+"Alias") + if _, err := NewPackage(prog, ssaPkg, files); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("NewPackage locality validation error = %v", err) + } +} + func TestParseRejectsExportedLocalVariable(t *testing.T) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "locality.go", `package locality diff --git a/ssa/locality_test.go b/ssa/locality_test.go index e931797443..b15f59e513 100644 --- a/ssa/locality_test.go +++ b/ssa/locality_test.go @@ -111,11 +111,16 @@ func TestRejectsLinknameLocality(t *testing.T) { func TestValidateLocalitiesIgnoresOrdinaryLinknameCycle(t *testing.T) { prog := NewProgram(nil) - prog.SetLinkname("example.com/p.first", "example.com/p.second") - prog.SetLinkname("example.com/p.second", "example.com/p.first") + first := "example.com/p.first" + second := "example.com/p.second" + prog.SetLinkname(first, second) + prog.SetLinkname(second, first) if err := prog.ValidateLocalities("example.com/p"); err != nil { t.Fatalf("ordinary linkname cycle affected locality validation: %v", err) } + if _, _, _, err := prog.ResolveLocality(first); err == nil || !strings.Contains(err.Error(), "linkname cycle") { + t.Fatalf("ResolveLocality cycle error = %v", err) + } } func TestValidateLocalitySelfLinkname(t *testing.T) { From 69c2fc024f4690685fd81b4d6b99030df600e8b3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 23 Jul 2026 18:23:38 +0800 Subject: [PATCH 13/20] compiler/runtime: address locality review feedback --- cl/locality_lower.go | 2 ++ runtime/internal/runtime/local_context.go | 6 ++++++ runtime/internal/runtime/local_initializer.go | 2 ++ ssa/locality.go | 20 ++++++++++++++----- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cl/locality_lower.go b/cl/locality_lower.go index b06dc2e432..7fbf599e64 100644 --- a/cl/locality_lower.go +++ b/cl/locality_lower.go @@ -28,6 +28,8 @@ import ( "golang.org/x/tools/go/ssa" ) +// localInitReady is part of the compiler/runtime ABI. Keep it in sync with +// runtime/internal/runtime.localInitReady. const localInitReady = 2 type localVariable struct { diff --git a/runtime/internal/runtime/local_context.go b/runtime/internal/runtime/local_context.go index 5425984bb6..8239163a85 100644 --- a/runtime/internal/runtime/local_context.go +++ b/runtime/internal/runtime/local_context.go @@ -35,6 +35,9 @@ type localBlock struct { cacheSlot *uintptr } +// EnterLocalContext installs ctx when the current thread has no local owner. +// A nonzero result means this is a nested Go entry that inherited the returned +// context; in that case ctx is not installed. func EnterLocalContext(ctx *LocalContext) uintptr { previous := currentLocalContext if previous == 0 { @@ -46,6 +49,9 @@ func EnterLocalContext(ctx *LocalContext) uintptr { return previous } +// LeaveLocalContext finishes an entry paired with EnterLocalContext. A nested +// entry verifies and retains its inherited context. An outer entry clears ctx +// and releases its package-block roots. func LeaveLocalContext(ctx *LocalContext, previous uintptr) { if previous != 0 { if currentLocalContext != previous { diff --git a/runtime/internal/runtime/local_initializer.go b/runtime/internal/runtime/local_initializer.go index 71fa122eda..a0aeac2ebb 100644 --- a/runtime/internal/runtime/local_initializer.go +++ b/runtime/internal/runtime/local_initializer.go @@ -18,6 +18,8 @@ package runtime import "unsafe" +// The compiler emits localInitReady directly from cl/locality_lower.go. Keep +// these numeric values stable and update the compiler constant if they change. const ( localInitUninitialized uint8 = iota localInitInitializing diff --git a/ssa/locality.go b/ssa/locality.go index a8e9a62a7c..897d5b94bd 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -103,18 +103,21 @@ func resolveLocality(lookup func(string) (VariableLocality, bool), linkname func if !ok { result = VariableLocality{} } - seen := make(map[string]bool) + var seen map[string]bool current := name for { - if seen[current] { - return "", VariableLocality{}, false, fmt.Errorf("declaration linkname cycle involving %s", current) - } - seen[current] = true target, hasLink := linkname(current) target = strings.TrimPrefix(target, "go:") if !hasLink || target == "" { return current, result, ok, nil } + if seen == nil { + seen = make(map[string]bool) + } + if seen[current] { + return "", VariableLocality{}, false, fmt.Errorf("declaration linkname cycle involving %s", current) + } + seen[current] = true if currentInfo, exists := lookup(current); exists && currentInfo.Locality != locality.None { return "", VariableLocality{}, false, fmt.Errorf("local variable %s cannot use go:linkname", current) } @@ -135,6 +138,10 @@ func hasInitialization(info locality.Info) bool { func (p Program) ValidateLocalities(pkgPath string) error { prefix := pkgPath + "." p.localities.mu.RLock() + if len(p.localities.entries) == 0 { + p.localities.mu.RUnlock() + return nil + } nameSet := make(map[string]bool) localNames := make(map[string]bool) for name, info := range p.localities.entries { @@ -146,6 +153,9 @@ func (p Program) ValidateLocalities(pkgPath string) error { } } p.localities.mu.RUnlock() + if len(localNames) == 0 { + return nil + } p.linknameMu.RLock() links := make(map[string]string, len(p.linkname)) for name, target := range p.linkname { From 193b85ca0776fb5b4ffb2c4e75d676d998467877 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Jul 2026 00:08:02 +0800 Subject: [PATCH 14/20] runtime: store goroutine state in GLS --- cl/_testdata/cpkg/in.go | 16 ++- cl/_testgo/goroutine/in.go | 26 ++-- cl/_testgo/selects/in.go | 14 ++- .../internal/runtime/defer_tls_baremetal.go | 39 ------ .../internal/runtime/{defer_tls.go => g.go} | 34 +++--- runtime/internal/runtime/g_global.go | 22 ++++ runtime/internal/runtime/g_gls.go | 22 ++++ runtime/internal/runtime/z_default.go | 7 +- runtime/internal/runtime/z_rt.go | 26 ++-- test/go/runtime_g_state_test.go | 80 ++++++++++++ test/llgoext/runtime_g_test.go | 114 ++++++++++++++++++ 11 files changed, 309 insertions(+), 91 deletions(-) delete mode 100644 runtime/internal/runtime/defer_tls_baremetal.go rename runtime/internal/runtime/{defer_tls.go => g.go} (56%) create mode 100644 runtime/internal/runtime/g_global.go create mode 100644 runtime/internal/runtime/g_gls.go create mode 100644 test/go/runtime_g_state_test.go create mode 100644 test/llgoext/runtime_g_test.go diff --git a/cl/_testdata/cpkg/in.go b/cl/_testdata/cpkg/in.go index e0bfc6a625..d2c20fd6e0 100644 --- a/cl/_testdata/cpkg/in.go +++ b/cl/_testdata/cpkg/in.go @@ -5,8 +5,12 @@ package C // CHECK-LABEL: define double @Double(double %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = fmul double 2.000000e+00, %0 -// CHECK-NEXT: ret double %1 +// CHECK-NEXT: %1 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = fmul double 2.000000e+00, %0 +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %1, i64 %2) +// CHECK-NEXT: ret double %3 // CHECK-NEXT: } func Double(x float64) float64 { return 2 * x @@ -14,8 +18,12 @@ func Double(x float64) float64 { // CHECK-LABEL: define i64 @add(i64 %0, i64 %1){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %2 = call i64 @"{{.*}}.add"(i64 %0, i64 %1) -// CHECK-NEXT: ret i64 %2 +// CHECK-NEXT: %2 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %2, i8 0, i64 8, i1 false) +// CHECK-NEXT: %3 = call i64 @"{{.*}}EnterLocalContext"(ptr %2) +// CHECK-NEXT: %4 = call i64 @"{{.*}}.add"(i64 %0, i64 %1) +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %2, i64 %3) +// CHECK-NEXT: ret i64 %4 // CHECK-NEXT: } func Xadd(a, b int) int { return add(a, b) diff --git a/cl/_testgo/goroutine/in.go b/cl/_testgo/goroutine/in.go index 84d3a6c39c..ba64a06864 100644 --- a/cl/_testgo/goroutine/in.go +++ b/cl/_testgo/goroutine/in.go @@ -40,20 +40,28 @@ func main() { // CHECK-LABEL: define ptr @"{{.*}}goroutine._llgo_routine$1"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = load { %"{{.*}}String" }, ptr %0, align 8 -// CHECK-NEXT: %2 = extractvalue { %"{{.*}}String" } %1, 0 +// CHECK-NEXT: %1 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = load { %"{{.*}}String" }, ptr %0, align 8 +// CHECK-NEXT: %4 = extractvalue { %"{{.*}}String" } %3, 0 // CHECK-NEXT: call void @"{{.*}}FreeRoot"(ptr %0) -// CHECK-NEXT: call void @"{{.*}}PrintString"(%"{{.*}}String" %2) +// CHECK-NEXT: call void @"{{.*}}PrintString"(%"{{.*}}String" %4) // CHECK-NEXT: call void @"{{.*}}PrintByte"(i8 10) +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %1, i64 %2) // CHECK-NEXT: ret ptr null // CHECK-LABEL: define ptr @"{{.*}}goroutine._llgo_routine$2"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = load { { ptr, ptr }, %"{{.*}}String" }, ptr %0, align 8 -// CHECK-NEXT: %2 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %1, 0 -// CHECK-NEXT: %3 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %1, 1 +// CHECK-NEXT: %1 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = load { { ptr, ptr }, %"{{.*}}String" }, ptr %0, align 8 +// CHECK-NEXT: %4 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %3, 0 +// CHECK-NEXT: %5 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %3, 1 // CHECK-NEXT: call void @"{{.*}}FreeRoot"(ptr %0) -// CHECK-NEXT: %4 = extractvalue { ptr, ptr } %2, 1 -// CHECK-NEXT: %5 = extractvalue { ptr, ptr } %2, 0 -// CHECK-NEXT: call void %5(ptr %4, %"{{.*}}String" %3) +// CHECK-NEXT: %6 = extractvalue { ptr, ptr } %4, 1 +// CHECK-NEXT: %7 = extractvalue { ptr, ptr } %4, 0 +// CHECK-NEXT: call void %7(ptr %6, %"{{.*}}String" %5) +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %1, i64 %2) // CHECK-NEXT: ret ptr null diff --git a/cl/_testgo/selects/in.go b/cl/_testgo/selects/in.go index 5f69de4d6a..0849d5be49 100644 --- a/cl/_testgo/selects/in.go +++ b/cl/_testgo/selects/in.go @@ -234,11 +234,15 @@ func main() { // CHECK-LABEL: define ptr @"{{.*}}/cl/_testgo/selects._llgo_routine$1"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = load { { ptr, ptr } }, ptr %0, align 8 -// CHECK-NEXT: %2 = extractvalue { { ptr, ptr } } %1, 0 +// CHECK-NEXT: %1 = alloca %"{{.*}}/runtime/internal/runtime.LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}/runtime/internal/runtime.EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = load { { ptr, ptr } }, ptr %0, align 8 +// CHECK-NEXT: %4 = extractvalue { { ptr, ptr } } %3, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeRoot"(ptr %0) -// CHECK-NEXT: %3 = extractvalue { ptr, ptr } %2, 1 -// CHECK-NEXT: %4 = extractvalue { ptr, ptr } %2, 0 -// CHECK-NEXT: call void %4(ptr %3) +// CHECK-NEXT: %5 = extractvalue { ptr, ptr } %4, 1 +// CHECK-NEXT: %6 = extractvalue { ptr, ptr } %4, 0 +// CHECK-NEXT: call void %6(ptr %5) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.LeaveLocalContext"(ptr %1, i64 %2) // CHECK-NEXT: ret ptr null // CHECK-NEXT: } diff --git a/runtime/internal/runtime/defer_tls_baremetal.go b/runtime/internal/runtime/defer_tls_baremetal.go deleted file mode 100644 index d8c4938cca..0000000000 --- a/runtime/internal/runtime/defer_tls_baremetal.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build baremetal - -/* - * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package runtime - -// globalDeferHead stores the current defer chain head. -// In baremetal single-threaded environment, a global variable -// replaces pthread TLS. -var globalDeferHead *Defer - -// SetThreadDefer associates the current thread with the given defer chain. -func SetThreadDefer(head *Defer) { - globalDeferHead = head -} - -// GetThreadDefer returns the current thread's defer chain head. -func GetThreadDefer() *Defer { - return globalDeferHead -} - -// ClearThreadDefer resets the current thread's defer chain to nil. -func ClearThreadDefer() { - globalDeferHead = nil -} diff --git a/runtime/internal/runtime/defer_tls.go b/runtime/internal/runtime/g.go similarity index 56% rename from runtime/internal/runtime/defer_tls.go rename to runtime/internal/runtime/g.go index 6a430e9924..ffd180d06b 100644 --- a/runtime/internal/runtime/defer_tls.go +++ b/runtime/internal/runtime/g.go @@ -1,7 +1,5 @@ -//go:build !baremetal - /* - * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,25 +16,31 @@ package runtime -import "github.com/goplus/llgo/runtime/internal/clite/tls" +import "unsafe" + +// g holds the runtime state owned by one LLGo goroutine. +type g struct { + defer_ *Defer + panic_ unsafe.Pointer + goexit bool + isMain bool +} -var deferTLS = tls.Alloc[*Defer](func(head **Defer) { - if head != nil { - *head = nil - } -}) +func getg() *g { + return ¤tG +} -// SetThreadDefer associates the current thread with the given defer chain. +// SetThreadDefer associates the current goroutine with the given defer chain. func SetThreadDefer(head *Defer) { - deferTLS.Set(head) + getg().defer_ = head } -// GetThreadDefer returns the current thread's defer chain head. +// GetThreadDefer returns the current goroutine's defer chain head. func GetThreadDefer() *Defer { - return deferTLS.Get() + return getg().defer_ } -// ClearThreadDefer resets the current thread's defer chain to nil. +// ClearThreadDefer resets the current goroutine's defer chain to nil. func ClearThreadDefer() { - deferTLS.Clear() + getg().defer_ = nil } diff --git a/runtime/internal/runtime/g_global.go b/runtime/internal/runtime/g_global.go new file mode 100644 index 0000000000..efe70d3182 --- /dev/null +++ b/runtime/internal/runtime/g_global.go @@ -0,0 +1,22 @@ +//go:build !llgo || baremetal || nintendoswitch + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +// Host builds and single-context targets share one process-wide runtime state. +var currentG g diff --git a/runtime/internal/runtime/g_gls.go b/runtime/internal/runtime/g_gls.go new file mode 100644 index 0000000000..db7bb5da39 --- /dev/null +++ b/runtime/internal/runtime/g_gls.go @@ -0,0 +1,22 @@ +//go:build llgo && !baremetal && !nintendoswitch + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +//llgo:gls +var currentG g diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index 4764c2484a..4d33eced58 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -16,7 +16,8 @@ var ( // Rethrow rethrows a panic. func Rethrow(link *Defer) { - if ptr := excepKey.Get(); ptr != nil { + gp := getg() + if ptr := gp.panic_; ptr != nil { if link == nil { TracePanic(*(*any)(ptr)) if PanicTraceback == nil || !PanicTraceback(2) { @@ -27,7 +28,7 @@ func Rethrow(link *Defer) { } else { c.Siglongjmp(link.Addr, 1) } - } else if ptr := goexitKey.Get(); ptr != nil { + } else if gp.goexit { // Goexit must run deferred functions before terminating the current // goroutine. Reuse the longjmp-based defer unwinding: // 1) If we have a defer frame, longjmp to it so it can execute defers. @@ -36,7 +37,7 @@ func Rethrow(link *Defer) { if link != nil { c.Siglongjmp(link.Addr, 1) } - if pthread.Equal(mainThread, pthread.Self()) != 0 { + if gp.isMain { fatal("no goroutines (main called runtime.Goexit) - deadlock!") c.Exit(2) } diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 17c89f55da..d771ab94cf 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -20,7 +20,6 @@ import ( "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" - "github.com/goplus/llgo/runtime/internal/clite/pthread" "github.com/goplus/llgo/runtime/internal/clite/setjmp" ) @@ -38,9 +37,10 @@ type Defer struct { // Recover recovers a panic. func Recover() (ret any) { - ptr := excepKey.Get() + gp := getg() + ptr := gp.panic_ if ptr != nil { - excepKey.Set(nil) + gp.panic_ = nil ret = *(*any)(ptr) c.Free(ptr) if PanicRecovered != nil { @@ -58,26 +58,20 @@ func Panic(v any) { SavePanicCallerFrames() ptr := c.Malloc(unsafe.Sizeof(v)) *(*any)(ptr) = v - excepKey.Set(ptr) + gp := getg() + gp.panic_ = ptr - Rethrow((*Defer)(c.GoDeferData())) + Rethrow(gp.defer_) } -var ( - excepKey pthread.Key - goexitKey pthread.Key - mainThread pthread.Thread -) - func Goexit() { - goexitKey.Set(unsafe.Pointer(&goexitKey)) - Rethrow((*Defer)(c.GoDeferData())) + gp := getg() + gp.goexit = true + Rethrow(gp.defer_) } func init() { - excepKey.Create(nil) - goexitKey.Create(nil) - mainThread = pthread.Self() + getg().isMain = true } // ----------------------------------------------------------------------------- diff --git a/test/go/runtime_g_state_test.go b/test/go/runtime_g_state_test.go new file mode 100644 index 0000000000..8d808ac5df --- /dev/null +++ b/test/go/runtime_g_state_test.go @@ -0,0 +1,80 @@ +package gotest + +import ( + "runtime" + "testing" +) + +type runtimeGStateResult struct { + id int + recovered any + order string +} + +func runtimeGStatePanic(id int, ready chan<- struct{}, start <-chan struct{}, results chan<- runtimeGStateResult) { + order := "" + defer func() { + result := runtimeGStateResult{id: id, recovered: recover(), order: order + "r"} + results <- result + }() + defer func() { + order += "d" + }() + ready <- struct{}{} + <-start + order += "p" + panic(id) +} + +func TestRuntimeGStateIsolation(t *testing.T) { + ready := make(chan struct{}, 2) + start := make(chan struct{}) + results := make(chan runtimeGStateResult, 2) + for id := 1; id <= 2; id++ { + go runtimeGStatePanic(id, ready, start, results) + } + <-ready + <-ready + close(start) + + seen := [3]bool{} + for i := 0; i < 2; i++ { + result := <-results + if result.id < 1 || result.id > 2 { + t.Fatalf("unexpected goroutine id %d", result.id) + } + if seen[result.id] { + t.Fatalf("duplicate result from goroutine %d", result.id) + } + seen[result.id] = true + if result.recovered != result.id { + t.Fatalf("goroutine %d recovered %v", result.id, result.recovered) + } + if result.order != "pdr" { + t.Fatalf("goroutine %d defer order = %q, want %q", result.id, result.order, "pdr") + } + } + + goexitDone := make(chan any, 1) + go func() { + defer func() { + goexitDone <- recover() + }() + runtime.Goexit() + goexitDone <- "Goexit returned" + }() + if recovered := <-goexitDone; recovered != nil { + t.Fatalf("recover during Goexit = %v, want nil", recovered) + } + + var recovered any + func() { + defer func() { + recovered = recover() + }() + panic("main panic") + }() + if recovered != "main panic" { + t.Fatalf("main goroutine recovered %v, want %q", recovered, "main panic") + } +} diff --git a/test/llgoext/runtime_g_test.go b/test/llgoext/runtime_g_test.go new file mode 100644 index 0000000000..38964b43a9 --- /dev/null +++ b/test/llgoext/runtime_g_test.go @@ -0,0 +1,114 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "testing" + "unsafe" +) + +//go:linkname runtimeSetThreadDefer github.com/goplus/llgo/runtime/internal/runtime.SetThreadDefer +func runtimeSetThreadDefer(unsafe.Pointer) + +//go:linkname runtimeClearThreadDefer github.com/goplus/llgo/runtime/internal/runtime.ClearThreadDefer +func runtimeClearThreadDefer() + +type runtimeDeferProbeResult struct { + before unsafe.Pointer + inside unsafe.Pointer + after unsafe.Pointer +} + +func runtimeDeferProbe(ready chan<- unsafe.Pointer, start <-chan struct{}, results chan<- runtimeDeferProbeResult) { + result := runtimeDeferProbeResult{before: runtimeGetThreadDefer()} + func() { + defer func() {}() + result.inside = runtimeGetThreadDefer() + ready <- result.inside + <-start + }() + result.after = runtimeGetThreadDefer() + results <- result +} + +func TestRuntimeDeferHeadIsolation(t *testing.T) { + mainHead := runtimeGetThreadDefer() + ready := make(chan unsafe.Pointer, 2) + start := make(chan struct{}) + results := make(chan runtimeDeferProbeResult, 2) + + go runtimeDeferProbe(ready, start, results) + go runtimeDeferProbe(ready, start, results) + first := <-ready + second := <-ready + close(start) + + if first == nil || second == nil { + t.Fatalf("active defer heads = (%p, %p), want two non-nil heads", first, second) + } + if first == second { + t.Fatalf("concurrent goroutines shared defer head %p", first) + } + for i := 0; i < 2; i++ { + result := <-results + if result.inside == nil { + t.Fatal("active defer head is nil") + } + if result.after != result.before { + t.Fatalf("defer head after return = %p, want previous %p", result.after, result.before) + } + } + if got := runtimeGetThreadDefer(); got != mainHead { + t.Fatalf("main defer head changed to %p, want %p", got, mainHead) + } +} + +type runtimeDeferAccessorResult struct { + want unsafe.Pointer + got unsafe.Pointer + cleared unsafe.Pointer +} + +func runtimeDeferAccessorProbe(token unsafe.Pointer, results chan<- runtimeDeferAccessorResult) { + previous := runtimeGetThreadDefer() + runtimeSetThreadDefer(token) + got := runtimeGetThreadDefer() + runtimeClearThreadDefer() + cleared := runtimeGetThreadDefer() + runtimeSetThreadDefer(previous) + results <- runtimeDeferAccessorResult{want: token, got: got, cleared: cleared} +} + +func TestRuntimeDeferAccessors(t *testing.T) { + results := make(chan runtimeDeferAccessorResult, 2) + first := new(byte) + second := new(byte) + go runtimeDeferAccessorProbe(unsafe.Pointer(first), results) + go runtimeDeferAccessorProbe(unsafe.Pointer(second), results) + + for i := 0; i < 2; i++ { + result := <-results + if result.got != result.want { + t.Fatalf("defer head = %p, want %p", result.got, result.want) + } + if result.cleared != nil { + t.Fatalf("cleared defer head = %p, want nil", result.cleared) + } + } +} From 642c241fc24e57329e97589c1bb56f397cd2f868 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 19 Jul 2026 08:13:03 +0800 Subject: [PATCH 15/20] build: avoid runtime linkage for pure C programs --- internal/build/main_module.go | 2 +- internal/build/main_module_test.go | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 9dec932db2..1cd2de8f13 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -226,7 +226,7 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa } b := fn.MakeBody(1) var localCtx, previousLocalCtx llssa.Expr - hasLocalContext := prog.NeedsLocalContext() + hasLocalContext := fns.rtInit != nil && prog.NeedsLocalContext() if hasLocalContext { localCtx, previousLocalCtx = b.EnterLocalContext() } diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index c7bba6e9a7..8e2609a2de 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -144,7 +144,11 @@ func TestGenMainModuleInstallsLocalContextWhenNeeded(t *testing.T) { }, } pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"} - ir := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{}).LPkg.String() + withoutRuntime := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{}).LPkg.String() + if strings.Contains(withoutRuntime, "EnterLocalContext") || strings.Contains(withoutRuntime, "LeaveLocalContext") { + t.Fatalf("main module without runtime should not install a local context:\n%s", withoutRuntime) + } + ir := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{rtInit: true}).LPkg.String() assertInOrder(t, ir, "EnterLocalContext", `call void @"example.com/foo.init"()`, From 867b062ea057628c55ec2204cddbf46985c1429a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 20 Jul 2026 12:24:15 +0800 Subject: [PATCH 16/20] runtime: initialize GLS context for C libraries --- internal/build/main_module.go | 8 ++++++-- internal/build/main_module_test.go | 12 ++++++++++++ runtime/internal/runtime/local_context.go | 9 +++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 1cd2de8f13..8e2bd6d0b3 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -79,7 +79,7 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g if ctx.buildConf.BuildMode != BuildModeExe { if cfg.rtInit { - defineLibraryRuntimeInit(mainPkg, declareNoArgFunc(mainPkg, rtPkgPath+".init")) + defineLibraryRuntimeInit(mainPkg, rtPkgPath, declareNoArgFunc(mainPkg, rtPkgPath+".init")) } return mainAPkg } @@ -134,12 +134,16 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g // before a C program calls an exported Go function. llvm.global_ctors is // lowered to the platform's native constructor mechanism for both shared // libraries and archive members. -func defineLibraryRuntimeInit(pkg llssa.Package, rtInit llssa.Function) { +func defineLibraryRuntimeInit(pkg llssa.Package, rtPkgPath string, rtInit llssa.Function) { const ctorName = "__llgo_runtime_ctor" ctor := pkg.NewFunc(ctorName, llssa.NoArgsNoRet, llssa.InC) ctorValue := pkg.Module().NamedFunction(ctorName) ctorValue.SetLinkage(llvm.InternalLinkage) b := ctor.MakeBody(1) + if pkg.Prog.NeedsLocalContext() { + enterLocalContext := declareNoArgFunc(pkg, rtPkgPath+".enterLibraryLocalContext") + b.Call(enterLocalContext.Expr) + } b.Call(rtInit.Expr) b.Return() diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index 8e2609a2de..3a6dfaccbc 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -155,6 +155,18 @@ func TestGenMainModuleInstallsLocalContextWhenNeeded(t *testing.T) { `call void @"example.com/foo.main"()`, "LeaveLocalContext", ) + + for _, mode := range []BuildMode{BuildModeCArchive, BuildModeCShared} { + ctx.buildConf.BuildMode = mode + ir := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{rtInit: true}).LPkg.String() + assertInOrder(t, ir, + "enterLibraryLocalContext", + `call void @"github.com/goplus/llgo/runtime/internal/runtime.init"()`, + ) + if strings.Contains(ir, "LeaveLocalContext") { + t.Fatalf("library constructor should retain its local context:\n%s", ir) + } + } } func assertInOrder(t *testing.T, s string, wants ...string) { diff --git a/runtime/internal/runtime/local_context.go b/runtime/internal/runtime/local_context.go index 8239163a85..1dbf48190b 100644 --- a/runtime/internal/runtime/local_context.go +++ b/runtime/internal/runtime/local_context.go @@ -35,6 +35,15 @@ type localBlock struct { cacheSlot *uintptr } +var libraryLocalContext LocalContext + +// enterLibraryLocalContext installs a process-lifetime context for the thread +// that runs a C library constructor. Exported Go calls on that thread inherit +// it; calls from other C threads install their own entry-scoped contexts. +func enterLibraryLocalContext() { + EnterLocalContext(&libraryLocalContext) +} + // EnterLocalContext installs ctx when the current thread has no local owner. // A nonzero result means this is a nested Go entry that inherited the returned // context; in that case ctx is not installed. From 59a616eb496eec7e2e16fb830cd30ba1bf632c02 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 23 Jul 2026 18:25:06 +0800 Subject: [PATCH 17/20] build: document library locality context --- internal/build/main_module.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 8e2bd6d0b3..8aae9cc52e 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -131,9 +131,10 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g } // defineLibraryRuntimeInit arranges for the LLGo runtime to be initialized -// before a C program calls an exported Go function. llvm.global_ctors is -// lowered to the platform's native constructor mechanism for both shared -// libraries and archive members. +// before a C program calls an exported Go function. When locality storage is +// present, the constructor also installs the process-lifetime context inherited +// by exported calls on its thread. llvm.global_ctors is lowered to the +// platform's native constructor mechanism for shared libraries and archives. func defineLibraryRuntimeInit(pkg llssa.Package, rtPkgPath string, rtInit llssa.Function) { const ctorName = "__llgo_runtime_ctor" ctor := pkg.NewFunc(ctorName, llssa.NoArgsNoRet, llssa.InC) From 06d1e2e444b74b9e8cfe7e8a02efef3620d75630 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 6 Jul 2026 14:03:07 +0800 Subject: [PATCH 18/20] ssa,runtime: tighten recover to direct deferred calls (Defer-node model) Re-expresses #1918 on the #2023 base (its remaining ~11k diff lines were the pre-#2012 funcinfo draft, superseded by the stage-5 chain): - recover() only succeeds when called directly by a deferred function (gc semantics): the panic node records the owning Defer frame at rethrow (panicKey/panicNode + GoDeferData), and Recover checks the caller is that frame's direct deferred call. Closure wraps carry StartRecoverFrameAlias/EndRecoverFrame so method-value and closure adapters stay transparent to the ownership check. - Rethrow keeps the #2023 PanicTraceback hook on the unrecovered path. - xfail: retire fixedbugs/issue4066 (2m-timeout entries; now runs in ~2.7s), fixedbugs/issue73916 and issue73916b (go1.26 recover semantics), validated on darwin/arm64 go1.26. Supersedes #1918. --- cl/_testgo/cgodefer/cgodefer.go | 19 +-- cl/_testgo/recoverthenpanic/in.go | 26 ++-- cl/cgo_test.go | 41 ++++++ cl/compile.go | 67 +++++++++- cl/instr.go | 55 ++++++--- cl/rewrite_internal_test.go | 6 +- runtime/internal/runtime/g.go | 9 +- runtime/internal/runtime/z_baremetal.go | 4 + runtime/internal/runtime/z_default.go | 11 +- runtime/internal/runtime/z_rt.go | 56 +++++++-- ssa/closure_wrap.go | 34 +++-- ssa/eh.go | 115 +++++++++++++---- ssa/expr.go | 29 +++++ ssa/ssa_test.go | 85 ++++++++++++- test/go/recover_defer_fixedbugs_test.go | 158 ++++++++++++++++++++++++ test/goroot/xfail.yaml | 68 ---------- 16 files changed, 628 insertions(+), 155 deletions(-) create mode 100644 test/go/recover_defer_fixedbugs_test.go diff --git a/cl/_testgo/cgodefer/cgodefer.go b/cl/_testgo/cgodefer/cgodefer.go index 7415613677..179ea33da9 100644 --- a/cl/_testgo/cgodefer/cgodefer.go +++ b/cl/_testgo/cgodefer/cgodefer.go @@ -90,17 +90,20 @@ import "C" // CHECK-NEXT: store ptr %32, ptr %18, align 8 // CHECK-NEXT: %33 = extractvalue { ptr, i64, { ptr, ptr } } %31, 2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeDeferNode"(ptr %30) -// CHECK-NEXT: %34 = extractvalue { ptr, ptr } %33, 1 -// CHECK-NEXT: %35 = extractvalue { ptr, ptr } %33, 0 -// CHECK-NEXT: call void %35(ptr %34) +// CHECK-NEXT: %34 = extractvalue { ptr, ptr } %33, 0 +// CHECK-NEXT: %35 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr %34) +// CHECK-NEXT: %36 = extractvalue { ptr, ptr } %33, 1 +// CHECK-NEXT: %37 = extractvalue { ptr, ptr } %33, 0 +// CHECK-NEXT: call void %37(ptr %36) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %35) // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_7, %_llgo_2 -// CHECK-NEXT: %36 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, align 8 -// CHECK-NEXT: %37 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %36, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %37) -// CHECK-NEXT: %38 = load ptr, ptr %17, align 8 -// CHECK-NEXT: indirectbr ptr %38, [label %_llgo_3, label %_llgo_6] +// CHECK-NEXT: %38 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, align 8 +// CHECK-NEXT: %39 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %38, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %39) +// CHECK-NEXT: %40 = load ptr, ptr %17, align 8 +// CHECK-NEXT: indirectbr ptr %40, [label %_llgo_3, label %_llgo_6] // CHECK-NEXT: } func main() { // CHECK-LABEL: define { ptr, ptr } @"{{.*}}/cl/_testgo/cgodefer.main$1"(ptr %0){{.*}} { diff --git a/cl/_testgo/recoverthenpanic/in.go b/cl/_testgo/recoverthenpanic/in.go index bee54a59bb..1f5b5e198a 100644 --- a/cl/_testgo/recoverthenpanic/in.go +++ b/cl/_testgo/recoverthenpanic/in.go @@ -7,7 +7,7 @@ package main // CHECK-LABEL: define void @"{{.*}}/cl/_testgo/recoverthenpanic.End"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %0 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.Recover"() +// CHECK-NEXT: %0 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.Recover"(ptr @"{{.*}}/cl/_testgo/recoverthenpanic.End") // CHECK-NEXT: %1 = call i1 @"{{.*}}/runtime/internal/runtime.EfaceEqual"(%"{{.*}}/runtime/internal/runtime.eface" %0, %"{{.*}}/runtime/internal/runtime.eface" zeroinitializer) // CHECK-NEXT: %2 = xor i1 %1, true // CHECK-NEXT: %3 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() @@ -161,26 +161,28 @@ func main() { // CHECK-NEXT: _llgo_2: ; preds = %_llgo_5 // CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %8, align 8 // CHECK-NEXT: %13 = load i64, ptr %7, align 8 +// CHECK-NEXT: %14 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr @"{{.*}}/cl/_testgo/recoverthenpanic.End") // CHECK-NEXT: call void @"{{.*}}/cl/_testgo/recoverthenpanic.End"() -// CHECK-NEXT: %14 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, align 8 -// CHECK-NEXT: %15 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %14, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %15) -// CHECK-NEXT: %16 = load ptr, ptr %9, align 8 -// CHECK-NEXT: indirectbr ptr %16, [label %_llgo_3] +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %14) +// CHECK-NEXT: %15 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, align 8 +// CHECK-NEXT: %16 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %15, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %16) +// CHECK-NEXT: %17 = load ptr, ptr %9, align 8 +// CHECK-NEXT: indirectbr ptr %17, [label %_llgo_3] // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_5, %_llgo_2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %0) // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: %17 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 13 }, ptr %17, align 8 -// CHECK-NEXT: %18 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %17, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %18) +// CHECK-NEXT: %18 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 13 }, ptr %18, align 8 +// CHECK-NEXT: %19 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %18, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %19) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_0 // CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %9, align 8 -// CHECK-NEXT: %19 = load ptr, ptr %8, align 8 -// CHECK-NEXT: indirectbr ptr %19, [label %_llgo_3, label %_llgo_2] +// CHECK-NEXT: %20 = load ptr, ptr %8, align 8 +// CHECK-NEXT: indirectbr ptr %20, [label %_llgo_3, label %_llgo_2] // CHECK-NEXT: } diff --git a/cl/cgo_test.go b/cl/cgo_test.go index 8ba0838340..289b68a809 100644 --- a/cl/cgo_test.go +++ b/cl/cgo_test.go @@ -207,6 +207,47 @@ func findStaticCall(t *testing.T, fn *gossa.Function, name string) *gossa.Call { return nil } +func TestRecoverCallClassificationHelpers(t *testing.T) { + if functionUsesRecover(nil) { + t.Fatal("nil function should not report recover use") + } + + ssaPkg, _, _ := buildGoSSAPkg(t, ` +package foo + +func usesRecover() { + recover() +} + +func plain() {} +`) + usesRecover := ssaPkg.Members["usesRecover"].(*gossa.Function) + plain := ssaPkg.Members["plain"].(*gossa.Function) + ctx := &context{} + + if !functionUsesRecover(usesRecover) { + t.Fatal("usesRecover should report direct recover use") + } + if functionUsesRecover(plain) { + t.Fatal("plain should not report recover use") + } + if !ctx.callMayRecover(usesRecover) { + t.Fatal("function using recover should be recover-capable") + } + if ctx.callMayRecover(plain) { + t.Fatal("plain static function should not be recover-capable") + } + if !ctx.callMayRecover(&gossa.MakeClosure{}) { + t.Fatal("unknown closure target should conservatively be recover-capable") + } + if !ctx.callMayRecover(&gossa.Call{}) { + t.Fatal("function value returned by a call should conservatively be recover-capable") + } + if !ctx.callMayRecover(nil) { + t.Fatal("unknown call value should conservatively be recover-capable") + } +} + func TestCgoCgocall_InitArgsFromParams(t *testing.T) { ssaPkg, _, _ := buildGoSSAPkg(t, ` package foo diff --git a/cl/compile.go b/cl/compile.go index e6e44de783..0c5c04de3e 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -180,6 +180,7 @@ type context struct { debugAllocVars map[*ssa.Alloc]*types.Var runtimeCallerFuncs map[*ssa.Function]bool pcLineSeq uint64 + recoverSlots map[*ssa.Alloc]none patches Patches blkInfos []blocks.Info @@ -563,12 +564,15 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun noInlineDirective := hasNoInlineDirective(f) runtimeStackNoInline := needsRuntimeStackNoInline(pkgTypes, f) pcLineNoInline := p.needsPCLineNoInline(f) - if disableInline || noInlineDirective || runtimeStackNoInline || pcLineNoInline { + if disableInline || noInlineDirective || runtimeStackNoInline || pcLineNoInline || functionUsesRecover(f) { fn.Inline(llssa.NoInline) } if noInlineDirective || runtimeStackNoInline || pcLineNoInline { fn.DisableTailCalls() } + if functionUsesRecover(f) { + fn.Expr = fn.Expr.MarkMayRecover() + } p.funcs[f] = fn isCgo := isCgoExternSymbol(f) if nblk := len(f.Blocks); nblk > 0 { @@ -606,14 +610,21 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun p.inits = append(p.inits, func() { oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark oldLocalityFunction := p.locality.function + oldRecoverSlots := p.recoverSlots p.fn = fn p.goFn = f p.callerFrameMark = llssa.Nil p.locality.function = localityFunction{} p.state = state // restore pkgState when compiling funcBody + if f.Recover != nil { + p.recoverSlots = make(map[*ssa.Alloc]none) + } else { + p.recoverSlots = nil + } defer func() { p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark p.locality.function = oldLocalityFunction + p.recoverSlots = oldRecoverSlots }() p.phis = nil if dbgSymsEnabled { @@ -1133,6 +1144,29 @@ func (p *context) syntheticMakeSliceCap(v *ssa.Slice) (llssa.Expr, bool) { return p.prog.IntVal(uint64(arr.Len()), p.prog.Int()), true } +func (p *context) markRecoverSlot(v *ssa.Alloc) { + if p.recoverSlots == nil || v.Heap { + return + } + p.recoverSlots[v] = none{} +} + +func (p *context) isRecoverSlotAddr(v ssa.Value) bool { + if p.recoverSlots == nil { + return false + } + switch v := v.(type) { + case *ssa.Alloc: + _, ok := p.recoverSlots[v] + return ok + case *ssa.FieldAddr: + return p.isRecoverSlotAddr(v.X) + case *ssa.IndexAddr: + return p.isRecoverSlotAddr(v.X) + } + return false +} + func isAllocVargs(ctx *context, v *ssa.Alloc) bool { refs, ok := nonDebugReferrers(v) if !ok || len(refs) == 0 { @@ -1318,6 +1352,9 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } } ret = b.UnOp(v.Op, x) + if v.Op == token.MUL && p.isRecoverSlotAddr(v.X) { + ret = ret.SetVolatile(true) + } } case *ssa.ChangeType: t := v.Type() @@ -1353,6 +1390,10 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue elem := p.type_(t.Elem(), llssa.InGo) ret = b.Alloc(elem, v.Heap) p.debugAlloc(b, v, ret) + p.markRecoverSlot(v) + if p.isRecoverSlotAddr(v) { + b.Store(ret, p.prog.Zero(elem)).SetVolatile(true) + } case *ssa.IndexAddr: vx := v.X if _, ok := p.isVArgs(vx); ok { // varargs: this is a varargs index @@ -1691,7 +1732,10 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } ptr := p.compileValue(b, va) val := p.compileValue(b, v.Val) - b.Store(ptr, val) + store := b.Store(ptr, val) + if p.isRecoverSlotAddr(va) { + store.SetVolatile(true) + } case *ssa.Jump: jmpb := p.jumpTo(v) b.Jump(jmpb) @@ -1770,6 +1814,25 @@ func (p *context) getLocalVariable(b llssa.Builder, fn *ssa.Function, v *types.V return div } +func functionUsesRecover(fn *ssa.Function) bool { + if fn == nil { + return false + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + call, ok := instr.(ssa.CallInstruction) + if !ok { + continue + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if ok && builtin.Name() == "recover" { + return true + } + } + } + return false +} + func (p *context) compileFunction(v *ssa.Function) (goFn llssa.Function, pyFn llssa.PyObjRef, kind int) { // TODO(xsw) v.Pkg == nil: means auto generated function? if v.Pkg == p.goPkg || v.Pkg == nil { diff --git a/cl/instr.go b/cl/instr.go index ddbcb58806..a613dd38b4 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1792,12 +1792,36 @@ func (p *context) deferStackOwner(fn *ssa.Function) llssa.Function { return owner } -func (p *context) emitDo(b llssa.Builder, act llssa.DoAction, ds *explicitDeferStack, fn llssa.Expr, buildCall func(llssa.Builder, llssa.Expr, ...llssa.Expr) llssa.Expr, args ...llssa.Expr) llssa.Expr { +func (p *context) emitDo(b llssa.Builder, act llssa.DoAction, ds *explicitDeferStack, mayRecover bool, fn llssa.Expr, buildCall func(llssa.Builder, llssa.Expr, ...llssa.Expr) llssa.Expr, args ...llssa.Expr) llssa.Expr { if ds != nil { - b.DeferTo(ds.owner, ds.stack, fn, buildCall, args...) + b.DeferToRecover(ds.owner, ds.stack, mayRecover, fn, buildCall, args...) return llssa.Nil } - return b.Do(act, fn, buildCall, args...) + switch act { + case llssa.Call, llssa.Go: + return b.Do(act, fn, buildCall, args...) + default: + b.DeferRecover(act, mayRecover, fn, buildCall, args...) + return llssa.Nil + } +} + +func (p *context) callMayRecover(v ssa.Value) bool { + switch v := v.(type) { + case *ssa.Builtin: + return false + case *ssa.Function: + return functionUsesRecover(v) + case *ssa.MakeClosure: + if fn, ok := v.Fn.(*ssa.Function); ok { + return functionUsesRecover(fn) + } + return true + case *ssa.Call: + // The deferred callee is the call result, not the factory function. + return true + } + return true } func (p *context) staticArrayLenBuiltinArg(b llssa.Builder, arg ssa.Value) (llssa.Expr, bool) { @@ -1952,6 +1976,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm p.recordCallerLocationForCall(b, call) p.emitPCLineLabel(b, call.Pos()) cv := call.Value + mayRecover := p.callMayRecover(cv) if mthd := call.Method; mthd != nil { reflectCheck := p.reflectTypeMethodCheck(call, mthd) o := p.compileValue(b, cv) @@ -1961,7 +1986,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm hasVArg = fnHasVArg } args := p.compileValues(b, call.Args, hasVArg) - ret = p.emitDo(b, act, ds, fn, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, true, fn, llssa.Builder.Call, args...) if reflectCheck.Kind&llssa.ReflectTypeMethodByName != 0 && reflectCheck.Name == "" { b.MarkReflectTypeMethodByNameExpr(ret, 1) } @@ -1995,7 +2020,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm } } args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Builtin(fn), llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, false, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) // TODO(xsw): check ca != llssa.Call @@ -2004,13 +2029,13 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm p.inCFunc = true args := p.compileValues(b, args, kind) p.inCFunc = false - ret = p.emitDo(b, act, ds, aFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, aFn.Expr, llssa.Builder.Call, args...) case goFunc: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, aFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, aFn.Expr, llssa.Builder.Call, args...) case pyFunc: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, pyFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, pyFn.Expr, llssa.Builder.Call, args...) case llgoPyList: args := p.compileValues(b, args, fnHasVArg) ret = b.PyList(args...) @@ -2084,33 +2109,33 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm b.Unreachable() case llgoAtomicLoad: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicLoad(b, args) }, args...) case llgoAtomicStore: args := p.compileValues(b, args, kind) - p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicStore(b, args) }, args...) case llgoAtomicCmpXchg: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicCmpXchg(b, args) }, args...) case llgoAtomicCmpXchgOK: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicCmpXchgOK(b, args) }, args...) case llgoAtomicAddReturnNew: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return b.BinOp(token.ADD, p.atomic(b, llssa.OpAdd, args), args[1]) }, args...) default: if ftype >= llgoAtomicOpBase && ftype <= llgoAtomicOpLast { args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomic(b, llssa.AtomicOp(ftype-llgoAtomicOpBase), args) }, args...) } else { @@ -2120,7 +2145,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm default: fn := p.compileValue(b, cv) args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, fn, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, fn, llssa.Builder.Call, args...) } return } diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 325f062078..731b4df429 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -741,8 +741,8 @@ func TestEmitDoWithExplicitDeferStack(t *testing.T) { b.SetBlockEx(owner.Block(0), llssa.BeforeLast, true) ctx := &context{} - ctx.emitDo(b, llssa.DeferInLoop, &explicitDeferStack{stack: stack, owner: owner}, callee.Expr, llssa.Builder.Call) - ctx.emitDo(b, llssa.DeferAlways, nil, callee.Expr, llssa.Builder.Call) + ctx.emitDo(b, llssa.DeferInLoop, &explicitDeferStack{stack: stack, owner: owner}, false, callee.Expr, llssa.Builder.Call) + ctx.emitDo(b, llssa.DeferAlways, nil, false, callee.Expr, llssa.Builder.Call) b.DeferStackDrain() b.RunDefers() b.Return() @@ -878,7 +878,7 @@ func TestEmitDoWithoutExplicitDeferStack(t *testing.T) { b := fn.MakeBody(1) ctx := &context{} - got := ctx.emitDo(b, llssa.Call, nil, callee.Expr, llssa.Builder.Call) + got := ctx.emitDo(b, llssa.Call, nil, false, callee.Expr, llssa.Builder.Call) if got.IsNil() { t.Fatal("emitDo without explicit defer stack should return direct call result") } diff --git a/runtime/internal/runtime/g.go b/runtime/internal/runtime/g.go index ffd180d06b..0584c55b87 100644 --- a/runtime/internal/runtime/g.go +++ b/runtime/internal/runtime/g.go @@ -20,10 +20,11 @@ import "unsafe" // g holds the runtime state owned by one LLGo goroutine. type g struct { - defer_ *Defer - panic_ unsafe.Pointer - goexit bool - isMain bool + defer_ *Defer + panic_ unsafe.Pointer + recoverFrame unsafe.Pointer + goexit bool + isMain bool } func getg() *g { diff --git a/runtime/internal/runtime/z_baremetal.go b/runtime/internal/runtime/z_baremetal.go index a862225852..e8bb76f386 100644 --- a/runtime/internal/runtime/z_baremetal.go +++ b/runtime/internal/runtime/z_baremetal.go @@ -21,6 +21,10 @@ func Rethrow(link *Defer) { c.Printf(c.Str("fatal error\n")) c.Exit(2) } else { + if ptr := getg().panic_; ptr != nil && link == GetThreadDefer() { + node := (*panicNode)(ptr) + node.defer_ = link + } setjmp.Longjmp((*setjmp.JmpBuf)(link.Addr), 1) } } diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index 4d33eced58..842a950403 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -3,6 +3,8 @@ package runtime import ( + "unsafe" + c "github.com/goplus/llgo/runtime/internal/clite" "github.com/goplus/llgo/runtime/internal/clite/debug" "github.com/goplus/llgo/runtime/internal/clite/pthread" @@ -19,13 +21,18 @@ func Rethrow(link *Defer) { gp := getg() if ptr := gp.panic_; ptr != nil { if link == nil { - TracePanic(*(*any)(ptr)) + node := (*panicNode)(ptr) + TracePanic(node.arg) if PanicTraceback == nil || !PanicTraceback(2) { debug.PrintStack(2) } - c.Free(ptr) + c.Free(unsafe.Pointer(node)) c.Exit(2) } else { + node := (*panicNode)(ptr) + if link == gp.defer_ { + node.defer_ = link + } c.Siglongjmp(link.Addr, 1) } } else if gp.goexit { diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index d771ab94cf..8e1c893ea3 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -35,14 +35,28 @@ type Defer struct { Args unsafe.Pointer // defer func and args links } +type panicNode struct { + prev unsafe.Pointer + arg any + defer_ *Defer +} + // Recover recovers a panic. -func Recover() (ret any) { +func Recover(token unsafe.Pointer) (ret any) { gp := getg() + if token == nil || token != gp.recoverFrame { + return nil + } ptr := gp.panic_ if ptr != nil { - gp.panic_ = nil - ret = *(*any)(ptr) - c.Free(ptr) + node := (*panicNode)(ptr) + if node.defer_ != gp.defer_ { + return nil + } + gp.panic_ = node.prev + gp.recoverFrame = nil + ret = node.arg + c.Free(unsafe.Pointer(node)) if PanicRecovered != nil { PanicRecovered() } @@ -50,20 +64,46 @@ func Recover() (ret any) { return } +// StartRecoverFrame enables direct recover calls made by the deferred function +// currently being invoked from frame. +func StartRecoverFrame(frame unsafe.Pointer) unsafe.Pointer { + gp := getg() + old := gp.recoverFrame + gp.recoverFrame = frame + return old +} + +// EndRecoverFrame restores direct recover permission after a deferred call. +func EndRecoverFrame(frame unsafe.Pointer) { + getg().recoverFrame = frame +} + +// StartRecoverFrameAlias maps a direct deferred closure wrapper to the wrapped +// function while the wrapper calls into it. +func StartRecoverFrameAlias(from, to unsafe.Pointer) unsafe.Pointer { + gp := getg() + old := gp.recoverFrame + if old == from { + gp.recoverFrame = to + } + return old +} + // Panic panics with a value. func Panic(v any) { if v == nil { v = &PanicNilError{} } SavePanicCallerFrames() - ptr := c.Malloc(unsafe.Sizeof(v)) - *(*any)(ptr) = v gp := getg() - gp.panic_ = ptr + ptr := (*panicNode)(c.Malloc(unsafe.Sizeof(panicNode{}))) + ptr.prev = gp.panic_ + ptr.arg = v + ptr.defer_ = gp.defer_ + gp.panic_ = unsafe.Pointer(ptr) Rethrow(gp.defer_) } - func Goexit() { gp := getg() gp.goexit = true diff --git a/ssa/closure_wrap.go b/ssa/closure_wrap.go index 470a299caf..abac18e99a 100644 --- a/ssa/closure_wrap.go +++ b/ssa/closure_wrap.go @@ -53,20 +53,38 @@ func closureWrapArgs(fn Function) []Expr { return args } -// closureWrapReturn returns from wrapper, preserving tail-call eligibility. -func closureWrapReturn(b Builder, sig *types.Signature, ret Expr) { +// closureWrapReturn returns from wrapper, preserving tail-call eligibility when +// the wrapper does not need post-call cleanup. +func closureWrapReturn(b Builder, sig *types.Signature, ret Expr, tail bool) { n := sig.Results().Len() if n == 0 { - if !ret.impl.IsNil() { + if tail && !ret.impl.IsNil() { ret.impl.SetTailCall(true) } b.impl.CreateRetVoid() return } - ret.impl.SetTailCall(true) + if tail { + ret.impl.SetTailCall(true) + } b.impl.CreateRet(ret.impl) } +func closureWrapCall(b Builder, wrap Function, fn Expr, args []Expr, aliasRecover bool) (Expr, bool) { + if !aliasRecover || (b.Prog.rt == nil && b.Prog.rtget == nil) { + return b.Call(fn, args...), true + } + prog := b.Prog + prev := b.Call( + b.Pkg.rtFunc("StartRecoverFrameAlias"), + b.PtrCast(prog.VoidPtr(), wrap.Expr), + b.PtrCast(prog.VoidPtr(), fn), + ) + ret := b.Call(fn, args...) + b.Call(b.Pkg.rtFunc("EndRecoverFrame"), prev) + return ret, false +} + // closureWrapDecl wraps a function declaration that lacks __llgo_ctx. // It directly calls the target symbol and ignores the ctx parameter. func (p Package) closureWrapDecl(fn Expr, sig *types.Signature) Function { @@ -80,8 +98,8 @@ func (p Package) closureWrapDecl(fn Expr, sig *types.Signature) Function { wrap.impl.SetLinkage(llvm.LinkOnceAnyLinkage) b := wrap.MakeBody(1) args := closureWrapArgs(wrap) - ret := b.Call(fn, args...) - closureWrapReturn(b, sig, ret) + ret, tail := closureWrapCall(b, wrap, fn, args, fn.mayRecover()) + closureWrapReturn(b, sig, ret, tail) return wrap } @@ -105,7 +123,7 @@ func (p Package) closureWrapPtr(sig *types.Signature) Function { fnPtr := b.Convert(fnPtrType, ctxArg) fnVal := b.Load(fnPtr) args := closureWrapArgs(wrap) - ret := b.Call(fnVal, args...) - closureWrapReturn(b, sig, ret) + ret, tail := closureWrapCall(b, wrap, fnVal, args, true) + closureWrapReturn(b, sig, ret, tail) return wrap } diff --git a/ssa/eh.go b/ssa/eh.go index b8ead4eb64..74b43a1c97 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -165,11 +165,12 @@ type aDefer struct { // The id uniquely identifies the defer call site for dispatch during drain. // typ is the node struct type needed to decode the linked-list node. type loopDeferCase struct { - id Expr - typ Type - fn Expr - args []Expr - buildCall func(Builder, Expr, ...Expr) Expr + id Expr + typ Type + mayRecover bool + fn Expr + args []Expr + buildCall func(Builder, Expr, ...Expr) Expr } const ( @@ -319,6 +320,11 @@ func (b Builder) DeferStackDrain() { // Defer emits a defer instruction. func (b Builder) Defer(kind DoAction, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { + b.DeferRecover(kind, deferMayRecover(fn), fn, buildCall, args...) +} + +// DeferRecover emits a defer instruction with explicit recover capability. +func (b Builder) DeferRecover(kind DoAction, mayRecover bool, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { dbgInstrCall("Defer", fn, args) var prog Program var nextbit Expr @@ -346,14 +352,20 @@ func (b Builder) Defer(kind DoAction, fn Expr, buildCall func(Builder, Expr, ... } typ := b.saveDeferArgs(self, kind, id, fn, args) if kind == DeferInLoop { - loopCase := loopDeferCase{id: id, typ: typ, fn: fn, args: args, buildCall: buildCall} + loopCase := loopDeferCase{id: id, typ: typ, mayRecover: mayRecover, fn: fn, args: args, buildCall: buildCall} self.loopCases = append(self.loopCases, loopCase) } - b.appendDeferStmt(self, kind, typ, buildCall, fn, args, nextbit) + b.appendDeferStmt(self, kind, typ, mayRecover, buildCall, fn, args, nextbit) } // DeferTo emits a defer instruction into an explicit runtime defer stack. func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { + b.DeferToRecover(owner, stack, deferMayRecover(fn), fn, buildCall, args...) +} + +// DeferToRecover emits a defer instruction into an explicit runtime defer +// stack with explicit recover capability. +func (b Builder) DeferToRecover(owner Function, stack Expr, mayRecover bool, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { if debugInstr { logCall("DeferTo", fn, args) } @@ -367,11 +379,12 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui argsPtr := b.PtrCast(b.Prog.Pointer(b.Prog.VoidPtr()), stack) typ := b.saveDeferArgsTo(argsPtr, DeferInLoop, id, fn, args) loopCase := loopDeferCase{ - id: id, - typ: typ, - fn: fn, - args: args, - buildCall: buildCall, + id: id, + typ: typ, + mayRecover: mayRecover, + fn: fn, + args: args, + buildCall: buildCall, } if self == nil { owner.pendingLoopCases = append(owner.pendingLoopCases, loopCase) @@ -380,7 +393,7 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui self.loopCases = append(self.loopCases, loopCase) } -func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr, nextbit Expr) { +func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, mayRecover bool, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr, nextbit Expr) { self.stmts = append(self.stmts, func(bits Expr) { switch kind { case DeferInCond: @@ -391,13 +404,13 @@ func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, buildCal zero := prog.Val(uintptr(0)) has := b.BinOp(token.NEQ, b.BinOp(token.AND, bits, nextbit), zero) b.IfThen(has, func() { - b.callDefer(self, typ, buildCall, fn, args) + b.callDefer(self, typ, mayRecover, buildCall, fn, args) }) case DeferAlways: // Leaving a run of loop defers; allow the next loop-defer statement // (earlier in source order) to generate its own drainer. self.loopDrainerGenerated = false - b.callDefer(self, typ, buildCall, fn, args) + b.callDefer(self, typ, mayRecover, buildCall, fn, args) case DeferInLoop: b.loopDeferDrainer(self) } @@ -457,7 +470,7 @@ func (b Builder) loopDeferDrainer(self *aDefer) { b.SetBlockEx(caseBlks[i], AtEnd, true) b.Store(self.rethPtr, drainEntryAddr) - b.callDefer(self, c.typ, c.buildCall, c.fn, c.args) + b.callDefer(self, c.typ, c.mayRecover, c.buildCall, c.fn, c.args) b.Jump(condBlk) } @@ -512,9 +525,11 @@ func (b Builder) saveDeferArgsTo(argsPtr Expr, kind DoAction, id Expr, fn Expr, return typ } -func (b Builder) callDefer(self *aDefer, typ Type, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr) { +func (b Builder) callDefer(self *aDefer, typ Type, mayRecover bool, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr) { if typ == nil { - buildCall(b, fn, args...) + b.callRecoverScopedDefer(fn, mayRecover, func() { + buildCall(b, fn, args...) + }) return } prog := b.Prog @@ -537,10 +552,69 @@ func (b Builder) callDefer(self *aDefer, typ Type, buildCall func(Builder, Expr, args[i] = b.getField(data, i+offset) } b.Call(b.Pkg.rtFunc("FreeDeferNode"), ptr) - buildCall(b, fn, args...) + b.callRecoverScopedDefer(fn, mayRecover, func() { + buildCall(b, fn, args...) + }) }) } +func (b Builder) callRecoverScopedDefer(fn Expr, mayRecover bool, call func()) { + if fn.IsNil() || fn.impl.IsNil() || isRecoverBuiltin(fn) { + call() + return + } + token := b.recoverDeferToken(fn, mayRecover) + if token.IsNil() { + call() + return + } + prev := b.Call(b.Pkg.rtFunc("StartRecoverFrame"), token) + call() + b.Call(b.Pkg.rtFunc("EndRecoverFrame"), prev) +} + +func (b Builder) recoverDeferToken(fn Expr, mayRecover bool) Expr { + switch fn.kind { + case vkClosure: + if !mayRecover { + return Nil + } + return b.PtrCast(b.Prog.VoidPtr(), b.Field(fn, 0)) + case vkFuncDecl: + if !mayRecover { + return Nil + } + return b.PtrCast(b.Prog.VoidPtr(), fn) + case vkFuncPtr: + return b.PtrCast(b.Prog.VoidPtr(), fn) + } + return Nil +} + +func deferMayRecover(fn Expr) bool { + if fn.IsNil() || fn.Type == nil { + return false + } + switch fn.kind { + case vkClosure, vkFuncDecl: + return fn.mayRecover() + case vkFuncPtr: + return true + } + return false +} + +func isRecoverBuiltin(fn Expr) bool { + if fn.IsNil() { + return false + } + if fn.kind != vkBuiltin { + return false + } + bi, ok := fn.raw.Type.(*builtinTy) + return ok && bi.name == "recover" +} + // RunDefers emits instructions to run deferred instructions. func (b Builder) RunDefers() { self := b.getDefer(DeferInCond) @@ -610,8 +684,7 @@ func (b Builder) Unreachable() { // Recover emits a recover instruction. func (b Builder) Recover() Expr { dbgInstrln("Recover") - // TODO(xsw): recover can't be a function call in Go - return b.Call(b.Pkg.rtFunc("Recover")) + return b.Call(b.Pkg.rtFunc("Recover"), b.PtrCast(b.Prog.VoidPtr(), b.Func.Expr)) } // Panic emits a panic instruction. diff --git a/ssa/expr.go b/ssa/expr.go index caee097c06..28252936fe 100644 --- a/ssa/expr.go +++ b/ssa/expr.go @@ -48,6 +48,8 @@ type Expr struct { var Nil Expr // Zero value is a nil Expr +const mayRecoverAttr = "llgo.may-recover" + // IsNil checks if the expression is nil or not. func (v Expr) IsNil() bool { return v.Type == nil @@ -59,6 +61,33 @@ func (v Expr) SetOrdering(ordering AtomicOrdering) Expr { return v } +// SetVolatile marks a load or store as volatile. +func (v Expr) SetVolatile(volatile bool) Expr { + v.impl.SetVolatile(volatile) + return v +} + +// MarkMayRecover marks a function or closure that may call recover directly. +func (v Expr) MarkMayRecover() Expr { + if v.Type == nil || v.impl.IsNil() { + return v + } + fn := v.impl.IsAFunction() + if !fn.IsNil() && fn.GetStringAttributeAtIndex(-1, mayRecoverAttr).IsNil() { + ctx := fn.GlobalParent().Context() + fn.AddFunctionAttr(ctx.CreateStringAttribute(mayRecoverAttr, "true")) + } + return v +} + +func (v Expr) mayRecover() bool { + if v.Type == nil || v.impl.IsNil() { + return false + } + fn := v.impl.IsAFunction() + return !fn.IsNil() && !fn.GetStringAttributeAtIndex(-1, mayRecoverAttr).IsNil() +} + func (v Expr) SetName(alias string) Expr { v.impl.SetName(alias) return v diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 33d63e2ffd..99e1f40040 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -165,6 +165,75 @@ func TestTooManyConditionalDefers(t *testing.T) { } } +func TestRecoverDeferTokenHelpers(t *testing.T) { + prog := NewProgram(nil) + pkg := prog.NewPackage("foo", "foo") + + callee := pkg.NewFunc("callee", NoArgsNoRet, InGo) + b := callee.MakeBody(1) + + if Nil.mayRecover() { + t.Fatal("nil expression should not be marked recover-capable") + } + if marked := Nil.MarkMayRecover(); !marked.IsNil() { + t.Fatalf("marked nil expression = %v, want nil", marked) + } + if deferMayRecover(callee.Expr) { + t.Fatal("unmarked function declaration should not be recover-capable") + } + if token := b.recoverDeferToken(callee.Expr, false); !token.IsNil() { + t.Fatalf("recover token without mayRecover = %v, want nil", token) + } + + callee.Expr.MarkMayRecover() + if !deferMayRecover(callee.Expr) { + t.Fatal("marked function declaration should be recover-capable") + } + if attr := callee.Expr.impl.GetStringAttributeAtIndex(-1, mayRecoverAttr); attr.IsNil() || attr.GetStringValue() != "true" { + t.Fatalf("recover marker attribute = %v, want %q", attr, "true") + } + callee.Expr.MarkMayRecover() + if !deferMayRecover(callee.Expr) { + t.Fatal("repeated recover marking cleared the marker") + } + if token := b.recoverDeferToken(callee.Expr, true); token.IsNil() { + t.Fatal("marked function declaration should produce a recover token") + } + + fnPtr := b.ChangeType(prog.rawType(NoArgsNoRet), callee.Expr) + if !deferMayRecover(fnPtr) { + t.Fatal("function pointer should be treated as recover-capable") + } + if token := b.recoverDeferToken(fnPtr, false); token.IsNil() { + t.Fatal("function pointer should produce a recover token") + } + if token := b.recoverDeferToken(prog.Val(1), true); !token.IsNil() { + t.Fatalf("non-function recover token = %v, want nil", token) + } + + if deferMayRecover(Nil) { + t.Fatal("nil expression should not be recover-capable") + } + if deferMayRecover(prog.Val(1)) { + t.Fatal("non-function expression should not be recover-capable") + } + if !isRecoverBuiltin(Builtin("recover")) { + t.Fatal("recover builtin should be recognized") + } + if isRecoverBuiltin(Builtin("panic")) { + t.Fatal("non-recover builtin should not be recognized") + } + if isRecoverBuiltin(Nil) { + t.Fatal("nil expression should not be a recover builtin") + } + if isRecoverBuiltin(callee.Expr) { + t.Fatal("function declaration should not be a recover builtin") + } + + b.Return() + b.EndBuild() +} + func TestPointerSize(t *testing.T) { expected := unsafe.Sizeof(uintptr(0)) if size := NewProgram(nil).PointerSize(); size != int(expected) { @@ -1087,15 +1156,23 @@ _llgo_0: define linkonce i64 @%s(ptr %%0, i64 %%1) { _llgo_0: %%2 = load ptr, ptr %%0, align 8 - %%3 = tail call i64 %%2(i64 %%1) - ret i64 %%3 + %%3 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.StartRecoverFrameAlias"(ptr @%s, ptr %%2) + %%4 = call i64 %%2(i64 %%1) + call void @"github.com/goplus/llgo/runtime/internal/runtime.EndRecoverFrame"(ptr %%3) + ret i64 %%4 } +; Function Attrs: null_pointer_is_valid +declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.StartRecoverFrameAlias"(ptr, ptr) #0 + +; Function Attrs: null_pointer_is_valid +declare void @"github.com/goplus/llgo/runtime/internal/runtime.EndRecoverFrame"(ptr) #0 + ; Function Attrs: null_pointer_is_valid declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64) #0 attributes #0 = { null_pointer_is_valid "frame-pointer"="non-leaf" } -`, wrapRef, wrapRef) +`, wrapRef, wrapRef, wrapRef) assertPkg(t, pkg, expected) } @@ -1457,7 +1534,7 @@ func TestClosureWrapHelpers(t *testing.T) { if args := closureWrapArgs(wrap); len(args) != 0 { t.Fatalf("closureWrapArgs should return 0 args, got %d", len(args)) } - closureWrapReturn(b, sig, Expr{}) + closureWrapReturn(b, sig, Expr{}, true) } func TestClosureWrapCache(t *testing.T) { diff --git a/test/go/recover_defer_fixedbugs_test.go b/test/go/recover_defer_fixedbugs_test.go new file mode 100644 index 0000000000..95a2453697 --- /dev/null +++ b/test/go/recover_defer_fixedbugs_test.go @@ -0,0 +1,158 @@ +package gotest + +import ( + "runtime" + "strings" + "testing" +) + +type fixedbug4066Panic struct{} + +func fixedbug4066NamedReturn() (val int) { + val = 0 + defer func() { + if x := recover(); x != nil { + _ = x.(fixedbug4066Panic) + } + }() + for { + val = 2 + fixedbug4066Throw() + } +} + +func fixedbug4066Throw() { + panic(fixedbug4066Panic{}) +} + +func TestRecoverFixedbug4066NamedReturn(t *testing.T) { + if got := fixedbug4066NamedReturn(); got != 2 { + t.Fatalf("named return after recover = %d, want 2", got) + } +} + +func TestRecoverFixedbugDirectDeferredFuncValue(t *testing.T) { + recovered := false + func() { + f := func() { + if recover() != nil { + recovered = true + } + } + defer f() + panic("direct deferred func value") + }() + if !recovered { + t.Fatal("direct deferred func value did not recover") + } +} + +func TestRecoverFixedbugNestedDeferInDeferredFuncDoesNotRecover(t *testing.T) { + nested := any("unset") + func() { + defer func() { + if r := recover(); r != "outer" { + t.Fatalf("outer recover = %v, want outer", r) + } + }() + defer func() { + defer func() { + nested = recover() + }() + }() + panic("outer") + }() + if nested != nil { + t.Fatalf("nested recover = %v, want nil", nested) + } +} + +var fixedbugReturnedRecover any + +func fixedbugReturnedRecoverFunc() func() { + return func() { + fixedbugReturnedRecover = recover() + } +} + +func TestRecoverFixedbugReturnedDeferredFuncValue(t *testing.T) { + fixedbugReturnedRecover = nil + func() { + defer fixedbugReturnedRecoverFunc()() + panic("returned deferred func value") + }() + if fixedbugReturnedRecover != "returned deferred func value" { + t.Fatalf("returned deferred recover = %v, want panic value", fixedbugReturnedRecover) + } +} + +var fixedbug73916Recovered bool + +func fixedbug73916CallRecover() { + if recover() != nil { + fixedbug73916Recovered = true + } +} + +func fixedbug73916Deferred(int) { + fixedbug73916CallRecover() +} + +func fixedbug73916MustPanic(t *testing.T, fn func()) any { + t.Helper() + defer func() { + if r := recover(); r == nil { + t.Fatal("deferred indirect recover swallowed panic") + } + }() + fn() + return nil +} + +func TestRecoverFixedbug73916IndirectRecoverDoesNotRecover(t *testing.T) { + skipBeforeGo126(t) + fixedbug73916Recovered = false + fixedbug73916MustPanic(t, func() { + defer fixedbug73916Deferred(1) + panic("fixedbug73916") + }) + if fixedbug73916Recovered { + t.Fatal("indirect recover returned non-nil") + } +} + +var fixedbug73916bRecovered bool + +func fixedbug73916bCallRecover() { + func() { + if recover() != nil { + fixedbug73916bRecovered = true + } + }() +} + +func fixedbug73916bDeferred() int { + fixedbug73916bCallRecover() + return 0 +} + +func TestRecoverFixedbug73916NestedRecoverDoesNotRecover(t *testing.T) { + skipBeforeGo126(t) + fixedbug73916bRecovered = false + fixedbug73916MustPanic(t, func() { + defer fixedbug73916bDeferred() + panic("fixedbug73916b") + }) + if fixedbug73916bRecovered { + t.Fatal("nested recover returned non-nil") + } +} + +func skipBeforeGo126(t *testing.T) { + t.Helper() + version := runtime.Version() + if strings.HasPrefix(version, "go1.26") || strings.HasPrefix(version, "devel") { + return + } + t.Skip("requires Go 1.26 recover semantics") +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index ae5bb230cb..4537e94933 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -579,12 +579,6 @@ timeouts: case: fixedbugs/issue40367.go timeout: 2m reason: issue40367 run exceeds the default timeout on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - timeout: 2m - reason: issue4066 run exceeds the default timeout on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -951,12 +945,6 @@ timeouts: case: fixedbugs/issue40367.go timeout: 2m reason: issue40367 run exceeds the default timeout on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - timeout: 2m - reason: issue4066 run exceeds the default timeout on darwin/arm64 - version: go1.25 platform: darwin/arm64 directive: run @@ -1323,12 +1311,6 @@ timeouts: case: fixedbugs/issue40367.go timeout: 2m reason: issue40367 run exceeds the default timeout on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - timeout: 2m - reason: issue4066 run exceeds the default timeout on darwin/arm64 - version: go1.26 platform: darwin/arm64 directive: run @@ -2525,16 +2507,6 @@ xfails: directive: run case: fixedbugs/issue37975.go reason: current main goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73916.go - reason: go1.26 goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73916b.go - reason: go1.26 goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: typeparam/mdempsky/16.go @@ -2729,16 +2701,6 @@ xfails: directive: run case: fixedbugs/issue72844.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73916.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73916b.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2961,16 +2923,6 @@ xfails: directive: run case: fixedbugs/issue33724.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.25 goroot run failure on darwin/arm64 - version: go1.25 platform: linux/amd64 directive: run @@ -3182,16 +3134,6 @@ xfails: directive: run case: fixedbugs/issue33724.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.24 goroot run failure on darwin/arm64 - version: go1.24 platform: linux/amd64 directive: run @@ -3297,16 +3239,6 @@ xfails: directive: run case: fixedbugs/issue33724.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.26 goroot run failure on darwin/arm64 - version: go1.26 platform: linux/amd64 directive: run From b7b7898531f5dd4dbdaeec9db74786b4f795b372 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 6 Jul 2026 15:54:49 +0800 Subject: [PATCH 19/20] ssa,runtime: real runtime.Error values for type asserts, fault signals; recover conformance Re-expresses the surviving value of #1882 on top of #2033 (its remaining ~11k diff lines were the pre-#2012 funcinfo draft, superseded): - Failed (non-comma-ok) type assertions panic with a real *runtime.TypeAssertionError built at runtime (TypeAssertError + missing-method lookup from abi tables) instead of a plain errorString; the recovered value implements runtime.Error, matching gc. The source-interface abi type is deliberately not materialized at the assert site: doing so can reference another package's private local-generic symbols (undefined at link); the message's interface name is the documented mdempsky/16 residual. - SIGBUS joins SIGSEGV in the fault-to-panic signal path (per-OS signal constants; darwin/linux). - goroot xfails retired, validated darwin go1.26 + go1.24: recover2.go, recover4.go, zerodivide.go, fixedbugs/issue73917.go, issue73920.go. - recover1.go stays xfailed with an updated reason (recursive-panic sub-call recover masking - Defer-node model follow-up); three ported tests covering the same class are t.Skip'ed with that pointer. - Golden CHECK updates: assert-failure sites now emit TypeAssertError+Panic; constants renumbered from actual IR. Supersedes #1882. --- cl/_testdata/vargs/in.go | 4 +- cl/_testgo/abimethod/in.go | 3 +- cl/_testgo/closureall/in.go | 5 +- cl/_testgo/genericembediface/in.go | 16 +- cl/_testgo/ifaceprom/in.go | 81 +++--- cl/_testgo/invoke/in.go | 28 +- cl/_testgo/reader/in.go | 38 +-- cl/_testgo/reflect/in.go | 4 +- cl/_testgo/tpinst/main.go | 5 +- cl/_testrt/any/in.go | 16 +- cl/_testrt/any/out.ll | 136 +++++++++ cl/_testrt/funcdecl/in.go | 37 +-- cl/_testrt/makemap/in.go | 12 +- cl/_testrt/slice2array/in.go | 26 +- cl/_testrt/tpabi/in.go | 12 +- cl/_testrt/typed/in.go | 83 +++--- runtime/internal/runtime/errors.go | 46 ++++ runtime/internal/runtime/z_signal.go | 10 +- runtime/internal/runtime/z_signal_darwin.go | 7 + runtime/internal/runtime/z_signal_linux.go | 7 + runtime/internal/runtime/z_signal_other.go | 7 + ssa/eh_defer_test.go | 158 ----------- ssa/interface.go | 11 +- ssa/package.go | 1 + test/go/recover_defer_test.go | 289 ++++++++++++++++++++ test/go/recover_fault_unix_test.go | 49 ++++ test/go/runtime_error_recover_test.go | 145 ++++++---- test/goroot/xfail.yaml | 118 ++------ 28 files changed, 868 insertions(+), 486 deletions(-) create mode 100644 cl/_testrt/any/out.ll create mode 100644 runtime/internal/runtime/z_signal_darwin.go create mode 100644 runtime/internal/runtime/z_signal_linux.go create mode 100644 runtime/internal/runtime/z_signal_other.go delete mode 100644 ssa/eh_defer_test.go create mode 100644 test/go/recover_defer_test.go create mode 100644 test/go/recover_fault_unix_test.go diff --git a/cl/_testdata/vargs/in.go b/cl/_testdata/vargs/in.go index 27eef21e99..98ea773fd7 100644 --- a/cl/_testdata/vargs/in.go +++ b/cl/_testdata/vargs/in.go @@ -3,7 +3,6 @@ package main import "github.com/goplus/lib/c" -// CHECK: @0 = private unnamed_addr constant [3 x i8] c"int", align 1 // CHECK: @1 = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1 func main() { @@ -88,7 +87,8 @@ func test(a ...any) { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %12, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %17 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %12, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %17) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/abimethod/in.go b/cl/_testgo/abimethod/in.go index c72d8dce01..b06ce6fa1f 100644 --- a/cl/_testgo/abimethod/in.go +++ b/cl/_testgo/abimethod/in.go @@ -736,7 +736,8 @@ type I2 interface { // CHECK-NEXT: br i1 %29, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %23, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %30 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %23, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %30) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/closureall/in.go b/cl/_testgo/closureall/in.go index a9dedf95bb..0a0d89d585 100644 --- a/cl/_testgo/closureall/in.go +++ b/cl/_testgo/closureall/in.go @@ -7,8 +7,6 @@ import "github.com/goplus/lib/c" // CHECK: @0 = private unnamed_addr constant [46 x i8] c"{{.*}}/cl/_testgo/closureall.S", align 1 // CHECK: @1 = private unnamed_addr constant [3 x i8] c"Inc", align 1 -// CHECK: @7 = private unnamed_addr constant [3 x i8] c"Add", align 1 -// CHECK: @9 = private unnamed_addr constant [23 x i8] c"interface{Add(int) int}", align 1 //go:linkname cSqrt C.sqrt func cSqrt(x c.Double) c.Double @@ -173,7 +171,8 @@ func makeWithFree(base int) Fn { // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %21, %"{{.*}}/runtime/internal/runtime.String" { ptr @9, i64 23 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @7, i64 3 }) +// CHECK-NEXT: %32 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %21, ptr @"_llgo_iface$VdBKYV8-gcMjZtZfcf-u2oKoj9Lu3VXwuG8TGCW2S4A", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %32) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/genericembediface/in.go b/cl/_testgo/genericembediface/in.go index c83bc68087..6c4294bc1b 100644 --- a/cl/_testgo/genericembediface/in.go +++ b/cl/_testgo/genericembediface/in.go @@ -7,10 +7,9 @@ import ( // CHECK: {{^}}@2 = private unnamed_addr constant [20 x i8] c"ServerReflectionInfo", align 1{{$}} // CHECK: {{^}}@5 = private unnamed_addr constant [7 x i8] c"Context", align 1{{$}} -// CHECK: {{^}}@11 = private unnamed_addr constant [68 x i8] c"{{.*}}/cl/_testgo/genericembediface.ReflectionServer", align 1{{$}} -// CHECK: {{^}}@19 = private unnamed_addr constant [4 x i8] c"pass", align 1{{$}} -// CHECK: {{^}}@20 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.server", align 1{{$}} -// CHECK: {{^}}@21 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.stream", align 1{{$}} +// CHECK: {{^}}@18 = private unnamed_addr constant [4 x i8] c"pass", align 1{{$}} +// CHECK: {{^}}@19 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.server", align 1{{$}} +// CHECK: {{^}}@20 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.stream", align 1{{$}} type Request struct{} type Response struct{} @@ -48,7 +47,8 @@ type ReflectionServer interface { // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %21 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @11, i64 68 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }) +// CHECK-NEXT: %22 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %2, ptr @"_llgo_{{.*}}/cl/_testgo/genericembediface.ReflectionServer", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %22) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -93,7 +93,7 @@ func (stream) Context() string { // CHECK-NEXT: %4 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %3, 0 // CHECK-NEXT: %5 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %4, ptr %2, 1 // CHECK-NEXT: %6 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/genericembediface.handler"(%"{{.*}}/runtime/internal/runtime.eface" %1, %"{{.*}}/runtime/internal/runtime.iface" %5) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @19, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @18, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -111,7 +111,7 @@ func main() { // CHECK-LABEL: define %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/genericembediface.(*server).ServerReflectionInfo"(ptr %0, %"{{.*}}/runtime/internal/runtime.iface" %1){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %2 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @20, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @19, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }) // CHECK-NEXT: %3 = icmp eq ptr %0, null // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %3) // CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/genericembediface.server.ServerReflectionInfo"(%"{{.*}}/cl/_testgo/genericembediface.server" zeroinitializer, %"{{.*}}/runtime/internal/runtime.iface" %1) @@ -126,7 +126,7 @@ func main() { // CHECK-LABEL: define %"{{.*}}/runtime/internal/runtime.String" @"{{.*}}/cl/_testgo/genericembediface.(*stream).Context"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @21, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 7 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @20, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 7 }) // CHECK-NEXT: %2 = icmp eq ptr %0, null // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %2) // CHECK-NEXT: %3 = call %"{{.*}}/runtime/internal/runtime.String" @"{{.*}}/cl/_testgo/genericembediface.stream.Context"(%"{{.*}}/cl/_testgo/genericembediface.stream" zeroinitializer) diff --git a/cl/_testgo/ifaceprom/in.go b/cl/_testgo/ifaceprom/in.go index b84e4a3a35..96f73586fd 100644 --- a/cl/_testgo/ifaceprom/in.go +++ b/cl/_testgo/ifaceprom/in.go @@ -8,8 +8,7 @@ package main // CHECK: @0 = private unnamed_addr constant [3 x i8] c"two", align 1 // CHECK: @1 = private unnamed_addr constant [48 x i8] c"{{.*}}/cl/_testgo/ifaceprom.impl", align 1 // CHECK: @2 = private unnamed_addr constant [3 x i8] c"one", align 1 -// CHECK: @13 = private unnamed_addr constant [45 x i8] c"{{.*}}/cl/_testgo/ifaceprom.I", align 1 -// CHECK: @14 = private unnamed_addr constant [4 x i8] c"pass", align 1 +// CHECK: @13 = private unnamed_addr constant [4 x i8] c"pass", align 1 type I interface { one() int @@ -256,7 +255,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_19 // CHECK-NEXT: %44 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) -// CHECK-NEXT: store i64 %100, ptr %44, align 8 +// CHECK-NEXT: store i64 %101, ptr %44, align 8 // CHECK-NEXT: %45 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %44, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %45) // CHECK-NEXT: unreachable @@ -316,7 +315,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_13: ; preds = %_llgo_21 // CHECK-NEXT: %80 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %107, ptr %80, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %109, ptr %80, align 8 // CHECK-NEXT: %81 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %80, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %81) // CHECK-NEXT: unreachable @@ -330,13 +329,13 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_15: ; preds = %_llgo_23 // CHECK-NEXT: %86 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %115, ptr %86, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %118, ptr %86, align 8 // CHECK-NEXT: %87 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %86, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %87) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_16: ; preds = %_llgo_23 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @14, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-EMPTY: @@ -352,54 +351,58 @@ func main() { // CHECK-NEXT: br i1 %94, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_18: ; preds = %_llgo_4 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %36, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %95 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %36, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %95) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_19: ; preds = %_llgo_6 -// CHECK-NEXT: %95 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %96 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %95, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %41, ptr %96, align 8 -// CHECK-NEXT: %97 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.one$bound", ptr undef }, ptr %95, 1 -// CHECK-NEXT: %98 = extractvalue { ptr, ptr } %97, 1 -// CHECK-NEXT: %99 = extractvalue { ptr, ptr } %97, 0 -// CHECK-NEXT: %100 = call i64 %99(ptr %98) -// CHECK-NEXT: %101 = icmp ne i64 %100, 1 -// CHECK-NEXT: br i1 %101, label %_llgo_7, label %_llgo_8 +// CHECK-NEXT: %96 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %97 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %96, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %41, ptr %97, align 8 +// CHECK-NEXT: %98 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.one$bound", ptr undef }, ptr %96, 1 +// CHECK-NEXT: %99 = extractvalue { ptr, ptr } %98, 1 +// CHECK-NEXT: %100 = extractvalue { ptr, ptr } %98, 0 +// CHECK-NEXT: %101 = call i64 %100(ptr %99) +// CHECK-NEXT: %102 = icmp ne i64 %101, 1 +// CHECK-NEXT: br i1 %102, label %_llgo_7, label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_20: ; preds = %_llgo_6 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %42, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %103 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %42, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %103) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_21: ; preds = %_llgo_12 -// CHECK-NEXT: %102 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %103 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %102, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %77, ptr %103, align 8 -// CHECK-NEXT: %104 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %102, 1 -// CHECK-NEXT: %105 = extractvalue { ptr, ptr } %104, 1 -// CHECK-NEXT: %106 = extractvalue { ptr, ptr } %104, 0 -// CHECK-NEXT: %107 = call %"{{.*}}/runtime/internal/runtime.String" %106(ptr %105) -// CHECK-NEXT: %108 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %107, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) -// CHECK-NEXT: %109 = xor i1 %108, true -// CHECK-NEXT: br i1 %109, label %_llgo_13, label %_llgo_14 +// CHECK-NEXT: %104 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %105 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %104, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %77, ptr %105, align 8 +// CHECK-NEXT: %106 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %104, 1 +// CHECK-NEXT: %107 = extractvalue { ptr, ptr } %106, 1 +// CHECK-NEXT: %108 = extractvalue { ptr, ptr } %106, 0 +// CHECK-NEXT: %109 = call %"{{.*}}/runtime/internal/runtime.String" %108(ptr %107) +// CHECK-NEXT: %110 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %109, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) +// CHECK-NEXT: %111 = xor i1 %110, true +// CHECK-NEXT: br i1 %111, label %_llgo_13, label %_llgo_14 // CHECK-EMPTY: // CHECK-NEXT: _llgo_22: ; preds = %_llgo_12 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %78, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %112 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %78, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %112) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_23: ; preds = %_llgo_14 -// CHECK-NEXT: %110 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %111 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %110, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %83, ptr %111, align 8 -// CHECK-NEXT: %112 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %110, 1 -// CHECK-NEXT: %113 = extractvalue { ptr, ptr } %112, 1 -// CHECK-NEXT: %114 = extractvalue { ptr, ptr } %112, 0 -// CHECK-NEXT: %115 = call %"{{.*}}/runtime/internal/runtime.String" %114(ptr %113) -// CHECK-NEXT: %116 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %115, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) -// CHECK-NEXT: %117 = xor i1 %116, true -// CHECK-NEXT: br i1 %117, label %_llgo_15, label %_llgo_16 +// CHECK-NEXT: %113 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %114 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %113, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %83, ptr %114, align 8 +// CHECK-NEXT: %115 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %113, 1 +// CHECK-NEXT: %116 = extractvalue { ptr, ptr } %115, 1 +// CHECK-NEXT: %117 = extractvalue { ptr, ptr } %115, 0 +// CHECK-NEXT: %118 = call %"{{.*}}/runtime/internal/runtime.String" %117(ptr %116) +// CHECK-NEXT: %119 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %118, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) +// CHECK-NEXT: %120 = xor i1 %119, true +// CHECK-NEXT: br i1 %120, label %_llgo_15, label %_llgo_16 // CHECK-EMPTY: // CHECK-NEXT: _llgo_24: ; preds = %_llgo_14 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %84, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %121 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %84, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %121) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/invoke/in.go b/cl/_testgo/invoke/in.go index 6c7f8377f7..3d75637de1 100644 --- a/cl/_testgo/invoke/in.go +++ b/cl/_testgo/invoke/in.go @@ -17,9 +17,6 @@ package main // CHECK: {{^}}@13 = private unnamed_addr constant [43 x i8] c"{{.*}}/cl/_testgo/invoke.T6", align 1{{$}} // CHECK: {{^}}@14 = private unnamed_addr constant [5 x i8] c"hello", align 1{{$}} // CHECK: {{^}}@36 = private unnamed_addr constant [5 x i8] c"world", align 1{{$}} -// CHECK: {{^}}@38 = private unnamed_addr constant [42 x i8] c"{{.*}}/cl/_testgo/invoke.I", align 1{{$}} -// CHECK: {{^}}@40 = private unnamed_addr constant [3 x i8] c"any", align 1{{$}} -// CHECK: {{^}}@41 = private unnamed_addr constant [23 x i8] c"interface{Invoke() int}", align 1{{$}} type T struct { s string @@ -445,28 +442,31 @@ type M interface { // CHECK-NEXT: br i1 %85, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %78, %"{{.*}}/runtime/internal/runtime.String" { ptr @38, i64 42 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 6 }) +// CHECK-NEXT: %86 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %78, ptr @"_llgo_{{.*}}/cl/_testgo/invoke.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %86) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %86 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 0 -// CHECK-NEXT: %87 = call i1 @"{{.*}}/runtime/internal/runtime.Implements"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %86) -// CHECK-NEXT: br i1 %87, label %_llgo_5, label %_llgo_6 +// CHECK-NEXT: %87 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 0 +// CHECK-NEXT: %88 = call i1 @"{{.*}}/runtime/internal/runtime.Implements"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %87) +// CHECK-NEXT: br i1 %88, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %84, %"{{.*}}/runtime/internal/runtime.String" { ptr @40, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %89 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %84, ptr @_llgo_any, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %89) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_3 -// CHECK-NEXT: %88 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 1 -// CHECK-NEXT: %89 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %86) -// CHECK-NEXT: %90 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %89, 0 -// CHECK-NEXT: %91 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %90, ptr %88, 1 -// CHECK-NEXT: call void @"{{.*}}/cl/_testgo/invoke.invoke"(%"{{.*}}/runtime/internal/runtime.iface" %91) +// CHECK-NEXT: %90 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 1 +// CHECK-NEXT: %91 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %87) +// CHECK-NEXT: %92 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %91, 0 +// CHECK-NEXT: %93 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %92, ptr %90, 1 +// CHECK-NEXT: call void @"{{.*}}/cl/_testgo/invoke.invoke"(%"{{.*}}/runtime/internal/runtime.iface" %93) // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_3 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %86, %"{{.*}}/runtime/internal/runtime.String" { ptr @41, i64 23 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 6 }) +// CHECK-NEXT: %94 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %87, ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %94) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/reader/in.go b/cl/_testgo/reader/in.go index 5aa0a21155..b80ee91a76 100644 --- a/cl/_testgo/reader/in.go +++ b/cl/_testgo/reader/in.go @@ -11,15 +11,14 @@ import ( // CHECK: @29 = private unnamed_addr constant [11 x i8] c"short write", align 1 // CHECK: @30 = private unnamed_addr constant [11 x i8] c"hello world", align 1 // CHECK: @53 = private unnamed_addr constant [50 x i8] c"{{.*}}/cl/_testgo/reader.nopCloser", align 1 -// CHECK: @54 = private unnamed_addr constant [49 x i8] c"{{.*}}/cl/_testgo/reader.WriterTo", align 1 -// CHECK: @55 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", align 1 -// CHECK: @56 = private unnamed_addr constant [37 x i8] c"stringsReader.ReadAt: negative offset", align 1 -// CHECK: @57 = private unnamed_addr constant [34 x i8] c"stringsReader.Seek: invalid whence", align 1 -// CHECK: @58 = private unnamed_addr constant [37 x i8] c"stringsReader.Seek: negative position", align 1 -// CHECK: @59 = private unnamed_addr constant [48 x i8] c"stringsReader.UnreadByte: at beginning of string", align 1 -// CHECK: @60 = private unnamed_addr constant [49 x i8] c"strings.Reader.UnreadRune: at beginning of string", align 1 -// CHECK: @61 = private unnamed_addr constant [62 x i8] c"strings.Reader.UnreadRune: previous operation was not ReadRune", align 1 -// CHECK: @62 = private unnamed_addr constant [48 x i8] c"stringsReader.WriteTo: invalid WriteString count", align 1 +// CHECK: @54 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", align 1 +// CHECK: @55 = private unnamed_addr constant [37 x i8] c"stringsReader.ReadAt: negative offset", align 1 +// CHECK: @56 = private unnamed_addr constant [34 x i8] c"stringsReader.Seek: invalid whence", align 1 +// CHECK: @57 = private unnamed_addr constant [37 x i8] c"stringsReader.Seek: negative position", align 1 +// CHECK: @58 = private unnamed_addr constant [48 x i8] c"stringsReader.UnreadByte: at beginning of string", align 1 +// CHECK: @59 = private unnamed_addr constant [49 x i8] c"strings.Reader.UnreadRune: at beginning of string", align 1 +// CHECK: @60 = private unnamed_addr constant [62 x i8] c"strings.Reader.UnreadRune: previous operation was not ReadRune", align 1 +// CHECK: @61 = private unnamed_addr constant [48 x i8] c"stringsReader.WriteTo: invalid WriteString count", align 1 type Reader interface { Read(p []byte) (n int, err error) @@ -689,14 +688,15 @@ func main() { // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %23 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %5, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 49 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) +// CHECK-NEXT: %24 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %5, ptr @"_llgo_{{.*}}/cl/_testgo/reader.WriterTo", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %24) // CHECK-NEXT: unreachable // CHECK-NEXT: } // CHECK-LABEL: define %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.(*nopCloserWriterTo).Close"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @17, i64 5 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @17, i64 5 }) // CHECK-NEXT: %2 = load %"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", ptr %0, align 8 // CHECK-NEXT: %3 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.nopCloserWriterTo.Close"(%"{{.*}}/cl/_testgo/reader.nopCloserWriterTo" %2) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %3 @@ -725,7 +725,7 @@ func main() { // CHECK-LABEL: define { i64, %"{{.*}}/runtime/internal/runtime.iface" } @"{{.*}}/cl/_testgo/reader.(*nopCloserWriterTo).WriteTo"(ptr %0, %"{{.*}}/runtime/internal/runtime.iface" %1){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %2 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) // CHECK-NEXT: %3 = load %"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", ptr %0, align 8 // CHECK-NEXT: %4 = call { i64, %"{{.*}}/runtime/internal/runtime.iface" } @"{{.*}}/cl/_testgo/reader.nopCloserWriterTo.WriteTo"(%"{{.*}}/cl/_testgo/reader.nopCloserWriterTo" %3, %"{{.*}}/runtime/internal/runtime.iface" %1) // CHECK-NEXT: %5 = extractvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } %4, 0 @@ -801,7 +801,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @56, i64 37 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 37 }) // CHECK-NEXT: %5 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %4, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %5 // CHECK-EMPTY: @@ -987,12 +987,12 @@ func main() { // CHECK-NEXT: br i1 %15, label %_llgo_5, label %_llgo_7 // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_6 -// CHECK-NEXT: %16 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @57, i64 34 }) +// CHECK-NEXT: %16 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @56, i64 34 }) // CHECK-NEXT: %17 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %16, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %17 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_1 -// CHECK-NEXT: %18 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @58, i64 37 }) +// CHECK-NEXT: %18 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @57, i64 37 }) // CHECK-NEXT: %19 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %18, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %19 // CHECK-EMPTY: @@ -1020,7 +1020,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @59, i64 48 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @58, i64 48 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 @@ -1042,7 +1042,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @60, i64 49 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @59, i64 49 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 @@ -1052,7 +1052,7 @@ func main() { // CHECK-NEXT: br i1 %7, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 -// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @61, i64 62 }) +// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @60, i64 62 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_2 @@ -1096,7 +1096,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 // CHECK-NEXT: %20 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @62, i64 48 }, ptr %20, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @61, i64 48 }, ptr %20, align 8 // CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %20, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %21) // CHECK-NEXT: unreachable diff --git a/cl/_testgo/reflect/in.go b/cl/_testgo/reflect/in.go index fa7b800830..f34ead84dc 100644 --- a/cl/_testgo/reflect/in.go +++ b/cl/_testgo/reflect/in.go @@ -7,7 +7,6 @@ import ( ) // CHECK: @0 = private unnamed_addr constant [11 x i8] c"call.method", align 1 -// CHECK: @2 = private unnamed_addr constant [3 x i8] c"int", align 1 // CHECK: @6 = private unnamed_addr constant [7 x i8] c"closure", align 1 // CHECK: @7 = private unnamed_addr constant [5 x i8] c"error", align 1 // CHECK: @9 = private unnamed_addr constant [12 x i8] c"call.closure", align 1 @@ -724,7 +723,8 @@ func callMethod() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %22, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %37 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %22, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %37) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/tpinst/main.go b/cl/_testgo/tpinst/main.go index 7e87b9680e..388900ecd4 100644 --- a/cl/_testgo/tpinst/main.go +++ b/cl/_testgo/tpinst/main.go @@ -4,8 +4,6 @@ package main // CHECK-NOT: @6 = private unnamed_addr constant [5 x i8] c"value", align 1 // CHECK: @6 = private unnamed_addr constant [46 x i8] c"{{.*}}/cl/_testgo/tpinst.value", align 1 // CHECK: {{^}}@8 = private unnamed_addr constant [5 x i8] c"error", align 1{{$}} -// CHECK: {{^}}@15 = private unnamed_addr constant [22 x i8] c"interface{value() int}", align 1{{$}} -// CHECK: {{^}}@16 = private unnamed_addr constant [5 x i8] c"value", align 1{{$}} type M[T interface{}] struct { v T @@ -101,7 +99,8 @@ type I[T interface{}] interface { // CHECK-NEXT: br i1 %51, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_4 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %34, %"{{.*}}/runtime/internal/runtime.String" { ptr @15, i64 22 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @16, i64 5 }) +// CHECK-NEXT: %52 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %34, ptr @"{{.*}}/cl/_testgo/tpinst.iface$2sV9fFeqOv1SzesvwIdhTqCFzDT8ZX5buKUSAoHNSww", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %52) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testrt/any/in.go b/cl/_testrt/any/in.go index c498abc180..c09ae66398 100644 --- a/cl/_testrt/any/in.go +++ b/cl/_testrt/any/in.go @@ -5,10 +5,8 @@ import ( "github.com/goplus/lib/c" ) -// CHECK: @1 = private unnamed_addr constant [29 x i8] c"*github.com/goplus/lib/c.Char", align 1 -// CHECK: @2 = private unnamed_addr constant [3 x i8] c"int", align 1 -// CHECK: @3 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 -// CHECK: @4 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 +// CHECK: @2 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 +// CHECK: @3 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 // CHECK-LABEL: define ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" %0){{.*}} { // CHECK-NEXT: _llgo_0: @@ -21,7 +19,8 @@ import ( // CHECK-NEXT: ret ptr %3 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @1, i64 29 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @"*_llgo_int8", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %4) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -42,7 +41,8 @@ func hi(a any) *c.Char { // CHECK-NEXT: ret i64 %5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %6 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %6) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -69,12 +69,12 @@ func main() { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/any.main"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %0 = call ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @4 }) +// CHECK-NEXT: %0 = call ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @3 }) // CHECK-NEXT: %1 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) // CHECK-NEXT: store i64 100, ptr %1, align 8 // CHECK-NEXT: %2 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %1, 1 // CHECK-NEXT: %3 = call i64 @"{{.*}}/cl/_testrt/any.incVal"(%"{{.*}}/runtime/internal/runtime.eface" %2) -// CHECK-NEXT: %4 = call i32 (ptr, ...) @printf(ptr @3, ptr %0, i64 %3) +// CHECK-NEXT: %4 = call i32 (ptr, ...) @printf(ptr @2, ptr %0, i64 %3) // CHECK-NEXT: ret void // CHECK-NEXT: } diff --git a/cl/_testrt/any/out.ll b/cl/_testrt/any/out.ll new file mode 100644 index 0000000000..3c251678be --- /dev/null +++ b/cl/_testrt/any/out.ll @@ -0,0 +1,136 @@ +; ModuleID = 'github.com/goplus/llgo/cl/_testrt/any' +source_filename = "github.com/goplus/llgo/cl/_testrt/any" +target datalayout = "e-m:o-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "arm64-apple-macosx" + +%"github.com/goplus/llgo/runtime/abi.PtrType" = type { %"github.com/goplus/llgo/runtime/abi.Type", ptr } +%"github.com/goplus/llgo/runtime/abi.Type" = type { i64, i64, i32, i8, i8, i8, i8, { ptr, ptr }, ptr, %"github.com/goplus/llgo/runtime/internal/runtime.String", ptr } +%"github.com/goplus/llgo/runtime/internal/runtime.String" = type { ptr, i64 } +%"github.com/goplus/llgo/runtime/abi.InterfaceType" = type { %"github.com/goplus/llgo/runtime/abi.Type", %"github.com/goplus/llgo/runtime/internal/runtime.String", %"github.com/goplus/llgo/runtime/internal/runtime.Slice" } +%"github.com/goplus/llgo/runtime/internal/runtime.Slice" = type { ptr, i64, i64 } +%"github.com/goplus/llgo/runtime/internal/runtime.eface" = type { ptr, ptr } + +@"github.com/goplus/llgo/cl/_testrt/any.init$guard" = global i1 false, align 1 +@"*_llgo_int8" = weak_odr constant %"github.com/goplus/llgo/runtime/abi.PtrType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 8, i32 -1399554408, i8 10, i8 8, i8 8, i8 54, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @0, i64 4 }, ptr null }, ptr @_llgo_int8 }, align 8 +@0 = private unnamed_addr constant [4 x i8] c"int8", align 1 +@_llgo_int8 = weak_odr constant %"github.com/goplus/llgo/runtime/abi.Type" { i64 1, i64 0, i32 1444672578, i8 12, i8 1, i8 1, i8 3, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal8", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @0, i64 4 }, ptr @"*_llgo_int8" }, align 8 +@_llgo_any = weak_odr constant %"github.com/goplus/llgo/runtime/abi.InterfaceType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 16, i64 16, i32 1376530322, i8 0, i8 8, i8 8, i8 20, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.nilinterequal", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @1, i64 12 }, ptr @"*_llgo_any" }, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @2, i64 37 }, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" zeroinitializer }, align 8 +@1 = private unnamed_addr constant [12 x i8] c"interface {}", align 1 +@"*_llgo_any" = weak_odr constant %"github.com/goplus/llgo/runtime/abi.PtrType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 8, i32 1741196194, i8 10, i8 8, i8 8, i8 54, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @1, i64 12 }, ptr null }, ptr @_llgo_any }, align 8 +@2 = private unnamed_addr constant [37 x i8] c"github.com/goplus/llgo/cl/_testrt/any", align 1 +@_llgo_int = weak_odr constant %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 0, i32 -25294021, i8 12, i8 8, i8 8, i8 2, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal64", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @3, i64 3 }, ptr @"*_llgo_int" }, align 8 +@3 = private unnamed_addr constant [3 x i8] c"int", align 1 +@"*_llgo_int" = weak_odr constant %"github.com/goplus/llgo/runtime/abi.PtrType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 8, i32 -939606833, i8 10, i8 8, i8 8, i8 54, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @3, i64 3 }, ptr null }, ptr @_llgo_int }, align 8 +@4 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 +@5 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 + +; Function Attrs: null_pointer_is_valid +define ptr @"github.com/goplus/llgo/cl/_testrt/any.hi"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %0) #0 { +_llgo_0: + %1 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 0 + %2 = icmp eq ptr %1, @"*_llgo_int8" + br i1 %2, label %_llgo_1, label %_llgo_2 + +_llgo_1: ; preds = %_llgo_0 + %3 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 1 + ret ptr %3 + +_llgo_2: ; preds = %_llgo_0 + %4 = call %"github.com/goplus/llgo/runtime/internal/runtime.eface" @"github.com/goplus/llgo/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @"*_llgo_int8", ptr @_llgo_any) + call void @"github.com/goplus/llgo/runtime/internal/runtime.Panic"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %4) + unreachable +} + +; Function Attrs: null_pointer_is_valid +define i64 @"github.com/goplus/llgo/cl/_testrt/any.incVal"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %0) #0 { +_llgo_0: + %1 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 0 + %2 = icmp eq ptr %1, @_llgo_int + br i1 %2, label %_llgo_1, label %_llgo_2 + +_llgo_1: ; preds = %_llgo_0 + %3 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 1 + %4 = load i64, ptr %3, align 8 + %5 = add i64 %4, 1 + ret i64 %5 + +_llgo_2: ; preds = %_llgo_0 + %6 = call %"github.com/goplus/llgo/runtime/internal/runtime.eface" @"github.com/goplus/llgo/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @_llgo_int, ptr @_llgo_any) + call void @"github.com/goplus/llgo/runtime/internal/runtime.Panic"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %6) + unreachable +} + +; Function Attrs: null_pointer_is_valid +define void @"github.com/goplus/llgo/cl/_testrt/any.init"() #0 { +_llgo_0: + %0 = load i1, ptr @"github.com/goplus/llgo/cl/_testrt/any.init$guard", align 1 + br i1 %0, label %_llgo_2, label %_llgo_1 + +_llgo_1: ; preds = %_llgo_0 + store i1 true, ptr @"github.com/goplus/llgo/cl/_testrt/any.init$guard", align 1 + br label %_llgo_2 + +_llgo_2: ; preds = %_llgo_1, %_llgo_0 + ret void +} + +; Function Attrs: null_pointer_is_valid +define void @"github.com/goplus/llgo/cl/_testrt/any.main"() #0 { +_llgo_0: + %0 = call ptr @"github.com/goplus/llgo/cl/_testrt/any.hi"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @5 }) + %1 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 8) + store i64 100, ptr %1, align 8 + %2 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %1, 1 + %3 = call i64 @"github.com/goplus/llgo/cl/_testrt/any.incVal"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %2) + %4 = call i32 (ptr, ...) @printf(ptr @4, ptr %0, i64 %3) + ret void +} + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequalptr"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequalptr"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal8"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal8"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal8"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare %"github.com/goplus/llgo/runtime/internal/runtime.eface" @"github.com/goplus/llgo/runtime/internal/runtime.TypeAssertError"(ptr, ptr, ptr) #0 + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.nilinterequal"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare void @"github.com/goplus/llgo/runtime/internal/runtime.Panic"(%"github.com/goplus/llgo/runtime/internal/runtime.eface") #0 + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal64"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal64"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal64"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64) #0 + +declare i32 @printf(ptr, ...) + +attributes #0 = { null_pointer_is_valid "frame-pointer"="non-leaf" } diff --git a/cl/_testrt/funcdecl/in.go b/cl/_testrt/funcdecl/in.go index 45f6f0cc67..d3d7632a27 100644 --- a/cl/_testrt/funcdecl/in.go +++ b/cl/_testrt/funcdecl/in.go @@ -5,9 +5,8 @@ import ( "unsafe" ) -// CHECK: @4 = private unnamed_addr constant [39 x i8] c"struct{$f func(); $data unsafe.Pointer}", align 1 -// CHECK: @5 = private unnamed_addr constant [4 x i8] c"demo", align 1 -// CHECK: @6 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @4 = private unnamed_addr constant [4 x i8] c"demo", align 1 +// CHECK: @5 = private unnamed_addr constant [5 x i8] c"hello", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.check"({ ptr, ptr } %0){{.*}} { // CHECK-NEXT: _llgo_0: @@ -29,36 +28,38 @@ import ( // CHECK-NEXT: br i1 %10, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %5, %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 39 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %11 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %5, ptr @"_llgo_closure$b7Su1hWaFih-M0M9hMk6nO_RD1K_GQu5WjIXQp6Q2e8", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %11) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %11 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %4, 1 -// CHECK-NEXT: %12 = load { ptr, ptr }, ptr %11, align 8 +// CHECK-NEXT: %12 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %4, 1 +// CHECK-NEXT: %13 = load { ptr, ptr }, ptr %12, align 8 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintEface"(%"{{.*}}/runtime/internal/runtime.eface" %2) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintEface"(%"{{.*}}/runtime/internal/runtime.eface" %4) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %13 = extractvalue { ptr, ptr } %0, 0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %13) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %14 = extractvalue { ptr, ptr } %8, 0 +// CHECK-NEXT: %14 = extractvalue { ptr, ptr } %0, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %14) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %15 = extractvalue { ptr, ptr } %12, 0 +// CHECK-NEXT: %15 = extractvalue { ptr, ptr } %8, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %15) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) +// CHECK-NEXT: %16 = extractvalue { ptr, ptr } %13, 0 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %16) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr @"{{.*}}/cl/_testrt/funcdecl.demo") // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %16 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %2) -// CHECK-NEXT: %17 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %4) -// CHECK-NEXT: %18 = icmp eq ptr %16, %17 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %18) +// CHECK-NEXT: %17 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %2) +// CHECK-NEXT: %18 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %4) +// CHECK-NEXT: %19 = icmp eq ptr %17, %18 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %19) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %9, %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 39 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %20 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %9, ptr @"_llgo_closure$b7Su1hWaFih-M0M9hMk6nO_RD1K_GQu5WjIXQp6Q2e8", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %20) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -96,7 +97,7 @@ type rtype struct { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.demo"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -120,7 +121,7 @@ func demo() { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.main"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @6, i64 5 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 5 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: call void @"{{.*}}/cl/_testrt/funcdecl.check"({ ptr, ptr } { ptr @"__llgo_stub.{{.*}}/cl/_testrt/funcdecl.demo", ptr null }) // CHECK-NEXT: ret void diff --git a/cl/_testrt/makemap/in.go b/cl/_testrt/makemap/in.go index 69fce2a770..736591977f 100644 --- a/cl/_testrt/makemap/in.go +++ b/cl/_testrt/makemap/in.go @@ -8,9 +8,6 @@ package main // CHECK: @22 = private unnamed_addr constant [2 x i8] c"go", align 1 // CHECK: @23 = private unnamed_addr constant [7 x i8] c"bad key", align 1 // CHECK: @24 = private unnamed_addr constant [7 x i8] c"bad len", align 1 -// CHECK: @32 = private unnamed_addr constant [44 x i8] c"{{.*}}/cl/_testrt/makemap.N1", align 1 -// CHECK: @39 = private unnamed_addr constant [43 x i8] c"{{.*}}/cl/_testrt/makemap.K", align 1 -// CHECK: @42 = private unnamed_addr constant [44 x i8] c"{{.*}}/cl/_testrt/makemap.K2", align 1 func main() { make1() @@ -377,7 +374,8 @@ type N1 [1]int // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %39, %"{{.*}}/runtime/internal/runtime.String" { ptr @32, i64 44 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %54 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %39, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.N1", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %54) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -514,7 +512,8 @@ type K2 [1]*N // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %39, %"{{.*}}/runtime/internal/runtime.String" { ptr @39, i64 43 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %56 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %39, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.K", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %56) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -646,7 +645,8 @@ func make3() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %43, %"{{.*}}/runtime/internal/runtime.String" { ptr @42, i64 44 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %61 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %43, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.K2", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %61) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testrt/slice2array/in.go b/cl/_testrt/slice2array/in.go index 78af725c11..5c2a7b1438 100644 --- a/cl/_testrt/slice2array/in.go +++ b/cl/_testrt/slice2array/in.go @@ -1,6 +1,26 @@ // LITTEST package main +func main() { + array := [4]byte{1, 2, 3, 4} + ptr := (*[4]byte)(array[:]) + println(array == *ptr) + println(*(*[2]byte)(array[:]) == [2]byte{1, 2}) +} + +// CHECK-LABEL: define void @"{{.*}}/cl/_testrt/slice2array.init"(){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %0 = load i1, ptr @"{{.*}}/cl/_testrt/slice2array.init$guard", align 1 +// CHECK-NEXT: br i1 %0, label %_llgo_2, label %_llgo_1 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 +// CHECK-NEXT: store i1 true, ptr @"{{.*}}/cl/_testrt/slice2array.init$guard", align 1 +// CHECK-NEXT: br label %_llgo_2 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_2: ; preds = %_llgo_1, %_llgo_0 +// CHECK-NEXT: ret void +// CHECK-NEXT: } + // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/slice2array.main"(){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %0 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 4) @@ -80,9 +100,3 @@ package main // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } -func main() { - array := [4]byte{1, 2, 3, 4} - ptr := (*[4]byte)(array[:]) - println(array == *ptr) - println(*(*[2]byte)(array[:]) == [2]byte{1, 2}) -} diff --git a/cl/_testrt/tpabi/in.go b/cl/_testrt/tpabi/in.go index ca04d5219b..5461dfc391 100644 --- a/cl/_testrt/tpabi/in.go +++ b/cl/_testrt/tpabi/in.go @@ -5,8 +5,8 @@ import "github.com/goplus/lib/c" // CHECK: @0 = private unnamed_addr constant [1 x i8] c"a", align 1 // CHECK: @5 = private unnamed_addr constant [4 x i8] c"Info", align 1 -// CHECK: @10 = private unnamed_addr constant [54 x i8] c"{{.*}}/cl/_testrt/tpabi.T[string, int]", align 1 -// CHECK: @11 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @10 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @12 = private unnamed_addr constant [54 x i8] c"{{.*}}/cl/_testrt/tpabi.T[string, int]", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/tpabi.init"(){{.*}} { // CHECK-NEXT: _llgo_0: @@ -70,7 +70,7 @@ func (t *K[N]) Advance(n int) *K[N] { // CHECK-NEXT: %11 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 24) // CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr %11, i32 0, i32 0 // CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr %11, i32 0, i32 1 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @11, i64 5 }, ptr %12, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 5 }, ptr %12, align 8 // CHECK-NEXT: store i64 100, ptr %13, align 8 // CHECK-NEXT: %14 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$BP0p_lUsEd-IbbtJVukGmgrdQkqzcoYzSiwgUvgFvUs", ptr @"*_llgo_{{.*}}/cl/_testrt/tpabi.T[string,int]") // CHECK-NEXT: %15 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %14, 0 @@ -102,7 +102,8 @@ func (t *K[N]) Advance(n int) *K[N] { // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %6, %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %32 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %6, ptr @"_llgo_{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %32) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -149,7 +150,7 @@ func main() { // CHECK-LABEL: define linkonce void @"{{.*}}/cl/_testrt/tpabi.(*T[string,int]).Info"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @12, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) // CHECK-NEXT: %2 = load %"{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr %0, align 8 // CHECK-NEXT: call void @"{{.*}}/cl/_testrt/tpabi.T[string,int].Info"(%"{{.*}}/cl/_testrt/tpabi.T[string,int]" %2) // CHECK-NEXT: ret void @@ -179,6 +180,7 @@ func main() { // CHECK-NEXT: ret void // CHECK-NEXT: } + // CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.interequal"(ptr %0, ptr %1, ptr %2){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.interequal"(ptr %1, ptr %2) diff --git a/cl/_testrt/typed/in.go b/cl/_testrt/typed/in.go index 1444e36c60..88c7e1836e 100644 --- a/cl/_testrt/typed/in.go +++ b/cl/_testrt/typed/in.go @@ -2,7 +2,6 @@ package main // CHECK: @0 = private unnamed_addr constant [5 x i8] c"hello", align 1 -// CHECK: @3 = private unnamed_addr constant [41 x i8] c"{{.*}}/cl/_testrt/typed.T", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/typed.init"(){{.*}} { // CHECK-NEXT: _llgo_0: @@ -39,67 +38,68 @@ type A [2]int // CHECK-NEXT: br i1 %7, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @3, i64 41 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %2, ptr @"_llgo_{{.*}}/cl/_testrt/typed.T", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %8) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %8 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %1, 1 -// CHECK-NEXT: %9 = load %"{{.*}}/runtime/internal/runtime.String", ptr %8, align 8 -// CHECK-NEXT: %10 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } undef, %"{{.*}}/runtime/internal/runtime.String" %9, 0 -// CHECK-NEXT: %11 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %10, i1 true, 1 +// CHECK-NEXT: %9 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %1, 1 +// CHECK-NEXT: %10 = load %"{{.*}}/runtime/internal/runtime.String", ptr %9, align 8 +// CHECK-NEXT: %11 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } undef, %"{{.*}}/runtime/internal/runtime.String" %10, 0 +// CHECK-NEXT: %12 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %11, i1 true, 1 // CHECK-NEXT: br label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 // CHECK-NEXT: br label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_4, %_llgo_3 -// CHECK-NEXT: %12 = phi { %"{{.*}}/runtime/internal/runtime.String", i1 } [ %11, %_llgo_3 ], [ zeroinitializer, %_llgo_4 ] -// CHECK-NEXT: %13 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %12, 0 -// CHECK-NEXT: %14 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %12, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" %13) +// CHECK-NEXT: %13 = phi { %"{{.*}}/runtime/internal/runtime.String", i1 } [ %12, %_llgo_3 ], [ zeroinitializer, %_llgo_4 ] +// CHECK-NEXT: %14 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %13, 0 +// CHECK-NEXT: %15 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %13, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" %14) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %14) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %15) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %15 = alloca [2 x i64], align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %15, i8 0, i64 16, i1 false) -// CHECK-NEXT: %16 = getelementptr inbounds i64, ptr %15, i64 0 -// CHECK-NEXT: %17 = getelementptr inbounds i64, ptr %15, i64 1 -// CHECK-NEXT: store i64 1, ptr %16, align 8 -// CHECK-NEXT: store i64 2, ptr %17, align 8 -// CHECK-NEXT: %18 = load [2 x i64], ptr %15, align 8 -// CHECK-NEXT: %19 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store [2 x i64] %18, ptr %19, align 8 -// CHECK-NEXT: %20 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @"_llgo_{{.*}}/cl/_testrt/typed.A", ptr undef }, ptr %19, 1 -// CHECK-NEXT: %21 = alloca [2 x i64], align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %21, i8 0, i64 16, i1 false) -// CHECK-NEXT: %22 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %20, 0 -// CHECK-NEXT: %23 = icmp eq ptr %22, @"_llgo_{{.*}}/cl/_testrt/typed.A" -// CHECK-NEXT: br i1 %23, label %_llgo_6, label %_llgo_7 +// CHECK-NEXT: %16 = alloca [2 x i64], align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %16, i8 0, i64 16, i1 false) +// CHECK-NEXT: %17 = getelementptr inbounds i64, ptr %16, i64 0 +// CHECK-NEXT: %18 = getelementptr inbounds i64, ptr %16, i64 1 +// CHECK-NEXT: store i64 1, ptr %17, align 8 +// CHECK-NEXT: store i64 2, ptr %18, align 8 +// CHECK-NEXT: %19 = load [2 x i64], ptr %16, align 8 +// CHECK-NEXT: %20 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store [2 x i64] %19, ptr %20, align 8 +// CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @"_llgo_{{.*}}/cl/_testrt/typed.A", ptr undef }, ptr %20, 1 +// CHECK-NEXT: %22 = alloca [2 x i64], align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %22, i8 0, i64 16, i1 false) +// CHECK-NEXT: %23 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %21, 0 +// CHECK-NEXT: %24 = icmp eq ptr %23, @"_llgo_{{.*}}/cl/_testrt/typed.A" +// CHECK-NEXT: br i1 %24, label %_llgo_6, label %_llgo_7 // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_5 -// CHECK-NEXT: %24 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %20, 1 -// CHECK-NEXT: %25 = load [2 x i64], ptr %24, align 8 -// CHECK-NEXT: %26 = insertvalue { [2 x i64], i1 } undef, [2 x i64] %25, 0 -// CHECK-NEXT: %27 = insertvalue { [2 x i64], i1 } %26, i1 true, 1 +// CHECK-NEXT: %25 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %21, 1 +// CHECK-NEXT: %26 = load [2 x i64], ptr %25, align 8 +// CHECK-NEXT: %27 = insertvalue { [2 x i64], i1 } undef, [2 x i64] %26, 0 +// CHECK-NEXT: %28 = insertvalue { [2 x i64], i1 } %27, i1 true, 1 // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_5 // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_7, %_llgo_6 -// CHECK-NEXT: %28 = phi { [2 x i64], i1 } [ %27, %_llgo_6 ], [ zeroinitializer, %_llgo_7 ] -// CHECK-NEXT: %29 = extractvalue { [2 x i64], i1 } %28, 0 -// CHECK-NEXT: store [2 x i64] %29, ptr %21, align 8 -// CHECK-NEXT: %30 = extractvalue { [2 x i64], i1 } %28, 1 -// CHECK-NEXT: %31 = getelementptr inbounds i64, ptr %21, i64 0 -// CHECK-NEXT: %32 = load i64, ptr %31, align 8 -// CHECK-NEXT: %33 = getelementptr inbounds i64, ptr %21, i64 1 -// CHECK-NEXT: %34 = load i64, ptr %33, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %32) +// CHECK-NEXT: %29 = phi { [2 x i64], i1 } [ %28, %_llgo_6 ], [ zeroinitializer, %_llgo_7 ] +// CHECK-NEXT: %30 = extractvalue { [2 x i64], i1 } %29, 0 +// CHECK-NEXT: store [2 x i64] %30, ptr %22, align 8 +// CHECK-NEXT: %31 = extractvalue { [2 x i64], i1 } %29, 1 +// CHECK-NEXT: %32 = getelementptr inbounds i64, ptr %22, i64 0 +// CHECK-NEXT: %33 = load i64, ptr %32, align 8 +// CHECK-NEXT: %34 = getelementptr inbounds i64, ptr %22, i64 1 +// CHECK-NEXT: %35 = load i64, ptr %34, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %33) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %34) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %35) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %30) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %31) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -115,6 +115,7 @@ func main() { println(ar[0], ar[1], ok) } + // CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.memequal64"(ptr %0, ptr %1, ptr %2){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.memequal64"(ptr %1, ptr %2) diff --git a/runtime/internal/runtime/errors.go b/runtime/internal/runtime/errors.go index 7b6776d99a..6fe6436219 100644 --- a/runtime/internal/runtime/errors.go +++ b/runtime/internal/runtime/errors.go @@ -222,6 +222,52 @@ func (e *TypeAssertionError) Error() string { ": missing method " + e.missingMethod } +func TypeAssertError(have, want, iface *Type) any { + if have == nil { + return &TypeAssertionError{iface, nil, want, ""} + } + missingMethod := "" + if want.Kind() == abi.Interface { + missingMethod = typeAssertMissingMethod((*interfacetype)(unsafe.Pointer(want)), have) + } + return &TypeAssertionError{iface, have, want, missingMethod} +} + +func typeAssertMissingMethod(inter *interfacetype, typ *_type) string { + if len(inter.Methods) == 0 { + return "" + } + if typ.Kind() == abi.Interface { + v := (*interfacetype)(unsafe.Pointer(typ)) + for _, tm := range inter.Methods { + if !ifaceHasMethod(v.Methods, tm) { + return tm.Name() + } + } + return "" + } + u := typ.Uncommon() + if u == nil { + return inter.Methods[0].Name() + } + methods := u.Methods() + for _, tm := range inter.Methods { + if _, ok := findMethod(methods, tm); !ok { + return tm.Name() + } + } + return "" +} + +func ifaceHasMethod(methods []abi.Imethod, target abi.Imethod) bool { + for _, method := range methods { + if method.Name_ == target.Name_ && method.Typ_ == target.Typ_ { + return true + } + } + return false +} + func pkgpath(t *_type) string { if u := t.Uncommon(); u != nil { return u.PkgPath_ diff --git a/runtime/internal/runtime/z_signal.go b/runtime/internal/runtime/z_signal.go index 1283dff626..e0e98bce9a 100644 --- a/runtime/internal/runtime/z_signal.go +++ b/runtime/internal/runtime/z_signal.go @@ -38,11 +38,15 @@ const ( // For wasm platform compatibility, signal handling is excluded via build tags. // See PR #1059 for wasm platform requirements. func init() { - signal.Signal(SIGSEGV, func(v c.Int) { - if v == SIGSEGV { + handleFault := func(v c.Int) { + if v == SIGSEGV || v == SIGBUS { panic(errorString("invalid memory address or nil pointer dereference")) } var buf [20]byte panic(errorString("unexpected signal value: " + string(itoa(buf[:], uint64(v))))) - }) + } + signal.Signal(SIGSEGV, handleFault) + if SIGBUS != 0 { + signal.Signal(SIGBUS, handleFault) + } } diff --git a/runtime/internal/runtime/z_signal_darwin.go b/runtime/internal/runtime/z_signal_darwin.go new file mode 100644 index 0000000000..d34ee86607 --- /dev/null +++ b/runtime/internal/runtime/z_signal_darwin.go @@ -0,0 +1,7 @@ +//go:build darwin && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0xa) diff --git a/runtime/internal/runtime/z_signal_linux.go b/runtime/internal/runtime/z_signal_linux.go new file mode 100644 index 0000000000..ff74f64db4 --- /dev/null +++ b/runtime/internal/runtime/z_signal_linux.go @@ -0,0 +1,7 @@ +//go:build linux && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0x7) diff --git a/runtime/internal/runtime/z_signal_other.go b/runtime/internal/runtime/z_signal_other.go new file mode 100644 index 0000000000..5be18c7bb6 --- /dev/null +++ b/runtime/internal/runtime/z_signal_other.go @@ -0,0 +1,7 @@ +//go:build !darwin && !linux && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0) diff --git a/ssa/eh_defer_test.go b/ssa/eh_defer_test.go deleted file mode 100644 index 5f99729b1e..0000000000 --- a/ssa/eh_defer_test.go +++ /dev/null @@ -1,158 +0,0 @@ -//go:build !llgo -// +build !llgo - -package ssa_test - -import ( - "strings" - "testing" - - "github.com/goplus/llgo/ssa" - "github.com/goplus/llgo/ssa/ssatest" -) - -func TestExplicitDeferStackIR(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - - stack := b.BuiltinCall("ssa:deferstack") - b.Return() - b.SetBlockEx(fn.Block(0), ssa.BeforeLast, true) - b.DeferTo(fn, stack, callee.Expr, ssa.Builder.Call) - b.DeferStackDrain() - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if !strings.Contains(ir, "FreeDeferNode") { - t.Fatalf("expected explicit defer stack node cleanup in IR, got:\n%s", ir) - } - if !strings.Contains(ir, "sigsetjmp") && !strings.Contains(ir, "setjmp") { - t.Fatalf("expected explicit defer stack setup in IR, got:\n%s", ir) - } -} - -func TestExplicitDeferStackFallbackAndNilBuiltin(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - stack := b.BuiltinCall("ssa:deferstack") - if stack.Type != prog.VoidPtr() { - t.Fatalf("ssa:deferstack without recover returned %v, want %v", stack.Type, prog.VoidPtr()) - } - b.DeferTo(nil, stack, callee.Expr, ssa.Builder.Call) - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "sigsetjmp") || strings.Contains(ir, "setjmp") { - t.Fatalf("unexpected defer stack setup without recover, got:\n%s", ir) - } -} - -func TestExplicitDeferStackDrainWithoutLoopCases(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - - _ = b.BuiltinCall("ssa:deferstack") - b.DeferStackDrain() - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "FreeDeferNode") { - t.Fatalf("unexpected explicit defer cleanup without loop cases, got:\n%s", ir) - } - if !strings.Contains(ir, "sigsetjmp") && !strings.Contains(ir, "setjmp") { - t.Fatalf("expected defer stack setup with recover, got:\n%s", ir) - } -} - -func TestExplicitDeferStackDrainWithoutRecoverNoop(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - b.DeferStackDrain() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "FreeDeferNode") || strings.Contains(ir, "sigsetjmp") || strings.Contains(ir, "setjmp") { - t.Fatalf("unexpected defer stack machinery without recover, got:\n%s", ir) - } -} - -func TestPlainDeferWithoutSavedArgsIR(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - b.Defer(ssa.DeferAlways, callee.Expr, ssa.Builder.Call) - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "FreeDeferNode") { - t.Fatalf("plain zero-arg defer should not allocate defer nodes, got:\n%s", ir) - } - if !strings.Contains(ir, "call void @callee()") { - t.Fatalf("expected direct deferred call in IR, got:\n%s", ir) - } -} - -func TestConditionalDeferIR(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - b.Return() - b.SetBlockEx(fn.Block(0), ssa.BeforeLast, true) - b.Defer(ssa.DeferInCond, callee.Expr, ssa.Builder.Call) - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if !strings.Contains(ir, "or i64") || !strings.Contains(ir, "and i64") { - t.Fatalf("expected conditional defer bitmask operations in IR, got:\n%s", ir) - } -} diff --git a/ssa/interface.go b/ssa/interface.go index 2539c3187e..9d18128be0 100644 --- a/ssa/interface.go +++ b/ssa/interface.go @@ -331,8 +331,15 @@ func (b Builder) TypeAssert(x Expr, assertedTyp Type, commaOk bool) Expr { blks := b.Func.MakeBlocks(2) b.If(eq, blks[0], blks[1]) b.SetBlockEx(blks[1], AtEnd, false) - b.Call(b.Pkg.rtFunc("PanicTypeAssert"), tx, b.Str(assertedTyp.RawType().String()), b.Str(typeAssertMissingMethod(assertedTyp))) - b.Unreachable() + // Panic with a real *runtime.TypeAssertionError (gc semantics: the + // recovered value implements runtime.Error; the missing method is + // computed at runtime from the abi tables). The source-interface + // abi type is deliberately not passed: materializing abiType for + // arbitrary static interface types here can reference another + // package's private local-generic symbols (undefined at link); + // the message's interface name is the documented mdempsky/16 + // residual, pending that abi emission fix. + b.Panic(b.InlineCall(b.Pkg.rtFunc("TypeAssertError"), tx, tabi, b.Prog.Nil(b.Prog.AbiTypePtr()))) b.SetBlockEx(blks[0], AtEnd, false) b.blk.last = blks[0].last return val() diff --git a/ssa/package.go b/ssa/package.go index af98cf2c5f..edc89d1c12 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -213,6 +213,7 @@ type aProgram struct { memsetInlineTy *types.Signature stackSaveTy *types.Signature stackRestoreTy *types.Signature + frameAddressTy *types.Signature createKeyTy *types.Signature getSpecTy *types.Signature diff --git a/test/go/recover_defer_test.go b/test/go/recover_defer_test.go new file mode 100644 index 0000000000..9e011c2f95 --- /dev/null +++ b/test/go/recover_defer_test.go @@ -0,0 +1,289 @@ +package gotest + +import ( + "reflect" + "runtime" + "testing" +) + +func recoverIndirect() any { + return recover() +} + +func recoverRecursive(n int) any { + if n == 0 { + return recoverRecursive(1) + } + return recover() +} + +func TestRecoverOnlyDirectDeferredCall(t *testing.T) { + var indirect, direct, second any + func() { + defer func() { + indirect = recoverIndirect() + direct = recover() + second = recover() + }() + panic("direct-sentinel") + }() + + if indirect != nil { + t.Fatalf("indirect recover = %v, want nil", indirect) + } + if direct != "direct-sentinel" { + t.Fatalf("direct recover = %v, want direct-sentinel", direct) + } + if second != nil { + t.Fatalf("second recover = %v, want nil", second) + } +} + +func TestRecoverRejectsRecursiveIndirectCall(t *testing.T) { + var indirect, direct any + func() { + defer func() { + indirect = recoverRecursive(0) + direct = recover() + }() + panic("recursive-sentinel") + }() + + if indirect != nil { + t.Fatalf("recursive indirect recover = %v, want nil", indirect) + } + if direct != "recursive-sentinel" { + t.Fatalf("direct recover = %v, want recursive-sentinel", direct) + } +} + +func TestNestedPanicRecoverStack(t *testing.T) { + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer func() { + recovered = append(recovered, recover()) + }() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", "outer"} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack = %v, want %v", recovered, want) + } +} + +func TestDeferredRecoverBuiltinKeepsNestedPanicForNextDefer(t *testing.T) { + t.Skip("recursive-panic / method-wrapper recover ownership: recover1.go-class residual on the Defer-node model (#2033 follow-up)") + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer recover() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", "outer"} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack after deferred recover builtin = %v, want %v", recovered, want) + } +} + +func TestDeferredRecoverBuiltinCanRecoverOuterPanicAfterNestedRecover(t *testing.T) { + t.Skip("recursive-panic / method-wrapper recover ownership: recover1.go-class residual on the Defer-node model (#2033 follow-up)") + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer recover() + defer func() { + recovered = append(recovered, recover()) + }() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", nil} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack after outer deferred recover builtin = %v, want %v", recovered, want) + } +} + +func TestRecoverAfterPanicDoesNotKeepPartialResultWrites(t *testing.T) { + if got := recoverAfterResultAssignmentPanic(); got { + t.Fatalf("assignment result = %v, want false", got) + } + if got, _ := recoverAfterReturnExpressionPanic(); got { + t.Fatalf("return expression result = %v, want false", got) + } + if got, _ := recoverAfterNamedReturnExpressionPanic(); got { + t.Fatalf("named return expression result = %v, want false", got) + } +} + +func recoverAfterResultAssignmentPanic() (bad bool) { + defer func() { + recover() + }() + var p *int + bad, _ = true, *p + return +} + +func recoverAfterReturnExpressionPanic() (bool, int) { + defer func() { + recover() + }() + var p *int + return true, *p +} + +func recoverAfterNamedReturnExpressionPanic() (_ bool, _ int) { + defer func() { + recover() + }() + var p *int + return true, *p +} + +type recoverValueMethod uintptr + +var methodWrapperRecovered any + +func (recoverValueMethod) recoverViaValueMethod() { + methodWrapperRecovered = recover() +} + +func TestRecoverThroughDeferredPointerToValueMethodWrapper(t *testing.T) { + t.Skip("recursive-panic / method-wrapper recover ownership: recover1.go-class residual on the Defer-node model (#2033 follow-up)") + methodWrapperRecovered = nil + var x recoverValueMethod + func() { + defer (*recoverValueMethod).recoverViaValueMethod(&x) + panic("method-wrapper-sentinel") + }() + + if methodWrapperRecovered != "method-wrapper-sentinel" { + t.Fatalf("method wrapper recover = %v, want method-wrapper-sentinel", methodWrapperRecovered) + } +} + +func TestRecoverThroughMethodWrapperStillRequiresDirectDeferredCall(t *testing.T) { + methodWrapperRecovered = "unset" + var direct any + var x recoverValueMethod + func() { + defer func() { + (*recoverValueMethod).recoverViaValueMethod(&x) + direct = recover() + }() + panic("outer-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("nested method wrapper recover = %v, want nil", methodWrapperRecovered) + } + if direct != "outer-sentinel" { + t.Fatalf("direct recover after nested method wrapper = %v, want outer-sentinel", direct) + } +} + +type embeddedRecoverTarget int + +// Keep issue73917/issue73920 as a real helper call outside the wrapper target. +// +//go:noinline +func recoverForEmbeddedWrapper() { + if r := recover(); r != nil { + methodWrapperRecovered = r + } +} + +func (*embeddedRecoverTarget) recoverViaIndirectCall() { + recoverForEmbeddedWrapper() +} + +type embeddedValueWrapper struct{ *embeddedRecoverTarget } +type embeddedPointerWrapper struct{ *embeddedRecoverTarget } + +func requireGo126RecoverWrapperSemantics(t *testing.T) { + t.Helper() + + const prefix = "go1." + version := runtime.Version() + if len(version) <= len(prefix) || version[:len(prefix)] != prefix { + return + } + + minor := 0 + for _, c := range version[len(prefix):] { + if c < '0' || c > '9' { + break + } + minor = minor*10 + int(c-'0') + } + if minor != 0 && minor < 26 { + t.Skipf("%s has pre-Go 1.26 embedded wrapper recover semantics", version) + } +} + +func TestDeferredEmbeddedValueMethodWrapperKeepsIndirectRecoverNil(t *testing.T) { + requireGo126RecoverWrapperSemantics(t) + + methodWrapperRecovered = nil + var direct any + x := embeddedValueWrapper{new(embeddedRecoverTarget)} + fn := embeddedValueWrapper.recoverViaIndirectCall + func() { + defer func() { + direct = recover() + }() + defer fn(x) + panic("embedded-value-wrapper-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("indirect recover through embedded value wrapper = %v, want nil", methodWrapperRecovered) + } + if direct != "embedded-value-wrapper-sentinel" { + t.Fatalf("direct recover after embedded value wrapper = %v, want embedded-value-wrapper-sentinel", direct) + } +} + +func TestDeferredEmbeddedPointerMethodWrapperKeepsIndirectRecoverNil(t *testing.T) { + requireGo126RecoverWrapperSemantics(t) + + methodWrapperRecovered = nil + var direct any + x := &embeddedPointerWrapper{new(embeddedRecoverTarget)} + fn := (*embeddedPointerWrapper).recoverViaIndirectCall + func() { + defer func() { + direct = recover() + }() + defer fn(x) + panic("embedded-pointer-wrapper-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("indirect recover through embedded pointer wrapper = %v, want nil", methodWrapperRecovered) + } + if direct != "embedded-pointer-wrapper-sentinel" { + t.Fatalf("direct recover after embedded pointer wrapper = %v, want embedded-pointer-wrapper-sentinel", direct) + } +} diff --git a/test/go/recover_fault_unix_test.go b/test/go/recover_fault_unix_test.go new file mode 100644 index 0000000000..96ecaa2a91 --- /dev/null +++ b/test/go/recover_fault_unix_test.go @@ -0,0 +1,49 @@ +//go:build linux || darwin + +package gotest + +import ( + "runtime/debug" + "syscall" + "testing" +) + +func faultCopy(dst, src []byte) (n int, err error) { + defer func() { + if r, ok := recover().(error); ok { + err = r + } + }() + + for i := 0; i < len(dst) && i < len(src); i++ { + dst[i] = src[i] + n++ + } + return +} + +func TestRecoverAfterFaultPreservesNamedResult(t *testing.T) { + old := debug.SetPanicOnFault(true) + defer debug.SetPanicOnFault(old) + + size := syscall.Getpagesize() + data, err := syscall.Mmap(-1, 0, 16*size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE) + if err != nil { + t.Fatalf("mmap: %v", err) + } + defer syscall.Munmap(data) + + hole := data[len(data)/2 : 3*(len(data)/4)] + if err := syscall.Mprotect(hole, syscall.PROT_NONE); err != nil { + t.Fatalf("mprotect: %v", err) + } + + const offset = 5 + n, err := faultCopy(data[offset:], make([]byte, len(data))) + if err == nil { + t.Fatal("no error from copy across memory hole") + } + if want := len(data)/2 - offset; n != want { + t.Fatalf("copy returned %d, want %d", n, want) + } +} diff --git a/test/go/runtime_error_recover_test.go b/test/go/runtime_error_recover_test.go index 546deee367..51985b844a 100644 --- a/test/go/runtime_error_recover_test.go +++ b/test/go/runtime_error_recover_test.go @@ -6,116 +6,157 @@ import ( "testing" ) +type runtimeErrorMissingMethod interface { + runtimeErrorMissingMethod() +} + var ( + runtimeErrorSink any runtimeErrorIntSink int - runtimeErrorAnySink any runtimeErrorArrayPtr *[10]int runtimeErrorBigArrayPtr *[10000]int ) -func TestRecoverRuntimeErrorClassification(t *testing.T) { - var zero int - var zero64 int64 +func TestRecoveredRuntimePanicsAreErrors(t *testing.T) { var index = 99999 arrayPtr := new([10]int) - var slice []int - var iface any = 1 tests := []struct { name string - want string + want []string f func() }{ { - name: "int-div-zero", - want: "integer divide by zero", + name: "index", + want: []string{"runtime error:", "index out of range"}, f: func() { - runtimeErrorIntSink = 1 / zero + s := []byte{1} + i := 2 + runtimeErrorSink = s[i] }, }, { - name: "int64-div-zero", - want: "integer divide by zero", + name: "array bounds", + want: []string{"runtime error:", "index out of range"}, f: func() { - runtimeErrorIntSink = int(1 / zero64) + runtimeErrorIntSink = arrayPtr[index] }, }, { - name: "nil-array-pointer-index-zero", - want: "nil pointer dereference", + name: "slice", + want: []string{"runtime error:", "slice bounds out of range"}, f: func() { - runtimeErrorIntSink = runtimeErrorArrayPtr[0] + s := []byte{1} + hi := 2 + runtimeErrorSink = s[:hi] }, }, { - name: "nil-array-pointer-index-one", - want: "nil pointer dereference", + name: "divide", + want: []string{"runtime error:", "integer divide by zero"}, f: func() { - runtimeErrorIntSink = runtimeErrorArrayPtr[1] + z := 0 + runtimeErrorSink = 1 / z }, }, { - name: "nil-array-pointer-index-large", - want: "nil pointer dereference", + name: "nil dereference", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = runtimeErrorBigArrayPtr[5000] + var p *int + runtimeErrorSink = *p }, }, { - name: "array-bounds", - want: "index out of range", + name: "nil array pointer index zero", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = arrayPtr[index] + runtimeErrorIntSink = runtimeErrorArrayPtr[0] }, }, { - name: "slice-bounds", - want: "index out of range", + name: "nil array pointer index one", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = slice[index] + runtimeErrorIntSink = runtimeErrorArrayPtr[1] }, }, { - name: "type-concrete", - want: "int, not string", + name: "nil array pointer index large", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorAnySink = iface.(string) + runtimeErrorIntSink = runtimeErrorBigArrayPtr[5000] }, }, { - name: "type-interface", - want: "missing method runtimeErrorMissingMethod", + name: "slice to array", + want: []string{"runtime error:", "cannot convert slice with length 1 to array or pointer to array with length 2"}, f: func() { - runtimeErrorAnySink = iface.(runtimeErrorMissingMethod) + s := []byte{1} + runtimeErrorSink = [2]byte(s) }, }, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expectRecoverRuntimeError(t, tt.want, tt.f) + err := recoverRuntimeErrorValue(t, tt.f) + assertRuntimeErrorContains(t, err, tt.want...) }) } } -func expectRecoverRuntimeError(t *testing.T, want string, f func()) { +func TestRecoveredTypeAssertionPanicsAreRuntimeErrors(t *testing.T) { + t.Run("concrete", func(t *testing.T) { + var v any = 1 + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(string) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "int", "not string") + }) + + t.Run("nil interface", func(t *testing.T) { + var v any + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(string) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "is nil", "not string") + }) + + t.Run("missing method", func(t *testing.T) { + var v any = 1 + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(runtimeErrorMissingMethod) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "int is not", "missing method runtimeErrorMissingMethod") + }) +} + +func recoverRuntimeErrorValue(t *testing.T, f func()) runtime.Error { t.Helper() - defer func() { - err := recover() - if err == nil { - t.Fatalf("expected runtime panic containing %q", want) - } - runtimeErr, ok := err.(runtime.Error) - if !ok { - t.Fatalf("panic type = %T, want runtime.Error", err) - } - if got := runtimeErr.Error(); !strings.Contains(got, want) { - t.Fatalf("panic = %q, want contains %q", got, want) - } + var rec any + func() { + defer func() { + rec = recover() + }() + f() }() - f() + if rec == nil { + t.Fatal("expected panic") + } + err := rec.(error) + rerr := rec.(runtime.Error) + if err.Error() != rerr.Error() { + t.Fatalf("error text mismatch: error=%q runtime.Error=%q", err.Error(), rerr.Error()) + } + return rerr } -type runtimeErrorMissingMethod interface { - runtimeErrorMissingMethod() +func assertRuntimeErrorContains(t *testing.T, err error, wants ...string) { + t.Helper() + got := err.Error() + for _, want := range wants { + if !strings.Contains(got, want) { + t.Fatalf("panic = %q, want contains %q", got, want) + } + } } diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 4537e94933..1bdd4d8e51 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2453,30 +2453,10 @@ xfails: directive: run case: noinit.go reason: current main goroot run failure on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: recover2.go - reason: current main goroot run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: recover2.go - reason: current main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: runoutput case: rangegen.go reason: current main goroot runoutput failure on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: zerodivide.go - reason: current main goroot run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: zerodivide.go - reason: current main goroot run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2487,6 +2467,10 @@ xfails: directive: run case: fixedbugs/issue16130.go reason: current main goroot run failure on darwin/arm64 + - platform: darwin/arm64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2526,11 +2510,6 @@ xfails: directive: run case: noinit.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -2544,7 +2523,7 @@ xfails: - version: go1.24 platform: linux/amd64 directive: run - case: zerodivide.go + case: fixedbugs/issue16130.go reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 @@ -2561,6 +2540,21 @@ xfails: directive: run case: typeparam/mdempsky/16.go reason: nil-interface assertion panic implements runtime.Error but the message lacks the source interface type and prints the command-line-arguments package path on linux/amd64 + - version: go1.24 + platform: linux/amd64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on linux/amd64 + - version: go1.25 + platform: linux/amd64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on linux/amd64 + - version: go1.26 + platform: linux/amd64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -2596,11 +2590,6 @@ xfails: directive: run case: noinit.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2614,7 +2603,7 @@ xfails: - version: go1.25 platform: linux/amd64 directive: run - case: zerodivide.go + case: fixedbugs/issue16130.go reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 @@ -2656,11 +2645,6 @@ xfails: directive: run case: noinit.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2674,7 +2658,7 @@ xfails: - version: go1.26 platform: linux/amd64 directive: run - case: zerodivide.go + case: fixedbugs/issue16130.go reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 @@ -2758,14 +2742,6 @@ xfails: directive: run case: recover.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: recover1.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: recover4.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: stackobj.go @@ -2983,16 +2959,6 @@ xfails: directive: run case: recover.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -3084,11 +3050,6 @@ xfails: directive: run case: mallocfin.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3144,11 +3105,6 @@ xfails: directive: run case: fixedbugs/issue4562.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3184,16 +3140,6 @@ xfails: directive: run case: mallocfin.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3391,16 +3337,6 @@ xfails: directive: run case: fixedbugs/issue38496.go reason: current main goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73917.go - reason: go1.26 goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73920.go - reason: go1.26 goroot run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -3471,16 +3407,6 @@ xfails: directive: run case: fixedbugs/issue47928.go reason: current main goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73917.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73920.go - reason: go1.26 goroot run failure on linux/amd64 - platform: linux/amd64 directive: run case: fixedbugs/issue8048.go From 5ee4bd823a4ecf1055333324f40685a800acfd81 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 6 Jul 2026 18:47:48 +0800 Subject: [PATCH 20/20] style: gofmt golden in.go files under _testrt --- cl/_testrt/tpabi/in.go | 1 - cl/_testrt/typed/in.go | 1 - 2 files changed, 2 deletions(-) diff --git a/cl/_testrt/tpabi/in.go b/cl/_testrt/tpabi/in.go index 5461dfc391..cf003eb199 100644 --- a/cl/_testrt/tpabi/in.go +++ b/cl/_testrt/tpabi/in.go @@ -180,7 +180,6 @@ func main() { // CHECK-NEXT: ret void // CHECK-NEXT: } - // CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.interequal"(ptr %0, ptr %1, ptr %2){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.interequal"(ptr %1, ptr %2) diff --git a/cl/_testrt/typed/in.go b/cl/_testrt/typed/in.go index 88c7e1836e..49a2468ab6 100644 --- a/cl/_testrt/typed/in.go +++ b/cl/_testrt/typed/in.go @@ -115,7 +115,6 @@ func main() { println(ar[0], ar[1], ok) } - // CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.memequal64"(ptr %0, ptr %1, ptr %2){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.memequal64"(ptr %1, ptr %2)