diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 10f186ee..fe288190 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -5,6 +5,7 @@ on: branches: - "**" - "!dependabot/**" + - "!xgopilot/**" pull_request: branches: ["**"] @@ -20,7 +21,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: "20" diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index ee3ae5b7..8778d79e 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -5,6 +5,7 @@ on: branches: - "**" - "!dependabot/**" + - "!xgopilot/**" pull_request: branches: ["**"] diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7870f7c5..2e706690 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -8,6 +8,7 @@ on: branches: - "**" - "!dependabot/**" + - "!xgopilot/**" pull_request: branches: ["**"] diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 18b80c91..4441d04a 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -8,6 +8,7 @@ on: branches: - "**" - "!dependabot/**" + - "!xgopilot/**" pull_request: branches: ["**"] @@ -26,7 +27,7 @@ jobs: wget -P ./_demo/c/llama2-c https://huggingface.co/karpathy/tinyllamas/resolve/main/stories15M.bin - name: Upload model as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: llama2-model path: ./_demo/c/llama2-c/stories15M.bin @@ -51,7 +52,7 @@ jobs: with: llvm-version: ${{matrix.llvm}} - name: Download model artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: llama2-model path: ./_demo/c/llama2-c/ diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 85c44f4b..48b8e9c3 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -2,7 +2,10 @@ name: Release Build on: push: - branches: ["**"] + branches: + - "**" + - "!dependabot/**" + - "!xgopilot/**" tags: ["*"] pull_request: branches: ["**"] @@ -79,7 +82,7 @@ jobs: release --verbose --skip=publish,nfpm,snapcraft --snapshot --clean - name: Upload Darwin AMD64 Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: llgo-darwin-amd64 path: .dist/*darwin-amd64.tar.gz @@ -87,7 +90,7 @@ jobs: include-hidden-files: true - name: Upload Darwin ARM64 Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: llgo-darwin-arm64 path: .dist/*darwin-arm64.tar.gz @@ -95,7 +98,7 @@ jobs: include-hidden-files: true - name: Upload Linux AMD64 Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: llgo-linux-amd64 path: .dist/*linux-amd64.tar.gz @@ -103,7 +106,7 @@ jobs: include-hidden-files: true - name: Upload Linux ARM64 Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: llgo-linux-arm64 path: .dist/*linux-arm64.tar.gz @@ -111,7 +114,7 @@ jobs: include-hidden-files: true - name: Upload Checksums - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: llgo-checksums path: .dist/*checksums.txt @@ -155,7 +158,7 @@ jobs: with: go-version: ${{ matrix.go-version }} - name: Download Platform Artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: llgo-${{ matrix.goos }}-${{ matrix.goarch }} path: . diff --git a/.github/workflows/targets.yml b/.github/workflows/targets.yml index 3237b3af..85ced2d9 100644 --- a/.github/workflows/targets.yml +++ b/.github/workflows/targets.yml @@ -6,6 +6,7 @@ on: branches: - "**" - "!dependabot/**" + - "!xgopilot/**" pull_request: branches: ["**"] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..8aba87af --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,132 @@ +# LLGo Project AI Assistant Guide + +This document provides essential information for AI assistants to help fix bugs and implement features in the LLGo project. + +## About LLGo + +LLGo is a Go compiler based on LLVM designed to better integrate Go with the C ecosystem, including Python and JavaScript. It's a subproject of the XGo project that aims to expand the boundaries of Go/XGo for game development, AI and data science, WebAssembly, and embedded development. + +## Project Structure + +- `cmd/llgo` - Main llgo compiler command (usage similar to `go` command) +- `cl/` - Core compiler logic that converts Go packages to LLVM IR +- `ssa/` - LLVM IR file generation using Go SSA semantics +- `internal/build/` - Build process orchestration +- `runtime/` - LLGo runtime library +- `chore/` - Development tools (llgen, llpyg, ssadump, etc.) +- `_demo/` - Example programs demonstrating C/C++ interop (`c/hello`, `c/qsort`) and Python integration (`py/callpy`, `py/numpy`) +- `_cmptest/` - Comparison tests to verify the same program gets the same output with Go and LLGo + +## Development Environment + +For detailed dependency requirements and installation instructions, see the [Dependencies](README.md#dependencies) and [How to install](README.md#how-to-install) sections in the README. + +## Testing & Validation + +The following commands and workflows are essential when fixing bugs or implementing features in the LLGo project: + +### Run all tests +```bash +go test ./... +``` + +**Note:** Some tests may fail if optional dependencies (like Python) are not properly configured. The test suite includes comprehensive tests for: +- Compiler functionality +- SSA generation +- C interop +- Python integration (requires Python development headers) + +### Write and run tests for your changes + +When adding new functionality or fixing bugs, create appropriate test cases: + +```bash +# Add your test to the relevant package's *_test.go file +# Then run tests for that package +go test ./path/to/package + +# Or run all tests +go test ./... +``` + +**Important:** The `LLGO_ROOT` environment variable must be set to the repository root when running llgo commands during development. + +## Code Quality + +Before submitting any code updates, you must run the following formatting and validation commands: + +### Format code +```bash +go fmt ./... +``` + +**Important:** Always run `go fmt ./...` before committing code changes. This ensures consistent code formatting across the project. + +### Run static analysis +```bash +go vet ./... +``` + +**Note:** Currently reports some issues related to lock passing by value in `ssa/type_cvt.go` and a possible unsafe.Pointer misuse in `cl/builtin_test.go`. These are known issues. + + +## Common Development Tasks + +### Build the entire project +```bash +go build -v ./... +``` + +### Build llgo command specifically +```bash +go build -o llgo ./cmd/llgo +``` + +### Check llgo version +```bash +llgo version +``` + +### Install llgo for system-wide use +```bash +./install.sh +``` + +### Build development tools +```bash +go install -v ./cmd/... +go install -v ./chore/... +``` + +## Key Modules for Understanding + +- `ssa` - Generates LLVM IR using Go SSA semantics +- `cl` - Core compiler converting Go to LLVM IR +- `internal/build` - Orchestrates the compilation process + +## Debugging + +### Disable Garbage Collection +For testing purposes, you can disable GC: +```bash +LLGO_ROOT=/path/to/llgo llgo run -tags nogc . +``` + +## LLGO_ROOT Environment Variable + +**CRITICAL:** Always set `LLGO_ROOT` to the repository root when running llgo during development: + +```bash +export LLGO_ROOT=/path/to/llgo +# or +LLGO_ROOT=/path/to/llgo llgo run . +``` + +## Important Notes + +1. **Testing Requirement:** All bug fixes and features MUST include tests +2. **Demo Directory:** Examples in `_demo` are prefixed with `_` to prevent standard `go` command from trying to compile them +3. **Defer in Loops:** LLGo intentionally does not support `defer` in loops (considered bad practice) +4. **C Ecosystem Integration:** LLGo uses `go:linkname` directive to link external symbols through ABI +5. **Python Integration:** Third-party Python libraries require separate installation of library files + diff --git a/README.md b/README.md index c0793def..570de5d5 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,7 @@ Here are the Go packages that can be imported correctly: * [hash/adler32](https://pkg.go.dev/hash/adler32) * [hash/crc32](https://pkg.go.dev/hash/crc32) (partially) * [hash/crc64](https://pkg.go.dev/hash/crc64) +* [hash/maphash](https://pkg.go.dev/hash/maphash) (partially) * [crypto](https://pkg.go.dev/crypto) * [crypto/md5](https://pkg.go.dev/crypto/md5) * [crypto/sha1](https://pkg.go.dev/crypto/sha1) diff --git a/_demo/go/gobuild/demo.go b/_demo/go/gobuild/demo.go new file mode 100644 index 00000000..b2177544 --- /dev/null +++ b/_demo/go/gobuild/demo.go @@ -0,0 +1,147 @@ +package main + +import ( + "fmt" + "go/build" + "runtime" + "strings" +) + +func main() { + fmt.Printf("runtime.Compiler = %q\n", runtime.Compiler) + + // Test 1: Check build.Default context + ctx := build.Default + fmt.Printf("build.Default.Compiler = %q\n", ctx.Compiler) + if ctx.Compiler != "gc" { + panic(fmt.Sprintf("expected build.Default.Compiler to be \"gc\", got %q", ctx.Compiler)) + } + + if len(ctx.ToolTags) == 0 { + panic("expected build.Default.ToolTags to be non-empty") + } + fmt.Printf("build.Default.ToolTags = %v\n", ctx.ToolTags) + + if len(ctx.ReleaseTags) == 0 { + panic("expected build.Default.ReleaseTags to be non-empty") + } + fmt.Printf("build.Default.ReleaseTags count = %d\n", len(ctx.ReleaseTags)) + + // Validate GOOS and GOARCH are set + if ctx.GOOS == "" { + panic("expected build.Default.GOOS to be non-empty") + } + if ctx.GOARCH == "" { + panic("expected build.Default.GOARCH to be non-empty") + } + fmt.Printf("build.Default.GOOS = %q, GOARCH = %q\n", ctx.GOOS, ctx.GOARCH) + + // Test 2: Import standard library package with FindOnly + pkg, err := build.Import("fmt", "", build.FindOnly) + if err != nil { + panic(fmt.Sprintf("build.Import(\"fmt\") failed: %v", err)) + } + if pkg.ImportPath != "fmt" { + panic(fmt.Sprintf("expected ImportPath \"fmt\", got %q", pkg.ImportPath)) + } + if !pkg.Goroot { + panic("expected fmt package to be in GOROOT") + } + fmt.Printf("build.Import(\"fmt\"): ImportPath=%s, Goroot=%v\n", pkg.ImportPath, pkg.Goroot) + + // Test 3: Import nested standard library package + osPkg, err := build.Import("os/exec", "", build.FindOnly) + if err != nil { + panic(fmt.Sprintf("build.Import(\"os/exec\") failed: %v", err)) + } + if osPkg.ImportPath != "os/exec" { + panic(fmt.Sprintf("expected ImportPath \"os/exec\", got %q", osPkg.ImportPath)) + } + if !osPkg.Goroot { + panic("expected os/exec package to be in GOROOT") + } + fmt.Printf("build.Import(\"os/exec\"): ImportPath=%s, Goroot=%v\n", osPkg.ImportPath, osPkg.Goroot) + + // Test 4: Import internal package (should succeed with FindOnly) + internalPkg, err := build.Import("internal/cpu", "", build.FindOnly) + if err != nil { + panic(fmt.Sprintf("build.Import(\"internal/cpu\") failed: %v", err)) + } + if internalPkg.ImportPath != "internal/cpu" { + panic(fmt.Sprintf("expected ImportPath \"internal/cpu\", got %q", internalPkg.ImportPath)) + } + fmt.Printf("build.Import(\"internal/cpu\"): ImportPath=%s\n", internalPkg.ImportPath) + + // Test 5: Import with srcDir parameter + runtimePkg, err := build.Import("runtime", "", build.FindOnly) + if err != nil { + panic(fmt.Sprintf("build.Import(\"runtime\") failed: %v", err)) + } + if runtimePkg.ImportPath != "runtime" { + panic(fmt.Sprintf("expected ImportPath \"runtime\", got %q", runtimePkg.ImportPath)) + } + if runtimePkg.Dir == "" { + panic("expected runtime package Dir to be non-empty") + } + fmt.Printf("build.Import(\"runtime\"): ImportPath=%s, Dir exists=%v\n", runtimePkg.ImportPath, runtimePkg.Dir != "") + + // Test 6: ImportDir with current directory + dirPkg, err := build.ImportDir(".", build.FindOnly) + if err != nil { + panic(fmt.Sprintf("build.ImportDir(\".\") failed: %v", err)) + } + // Note: Name might be empty with FindOnly mode as it doesn't read source files + fmt.Printf("build.ImportDir(\".\"): Dir=%s, ImportPath=%s\n", dirPkg.Dir, dirPkg.ImportPath) + + // Test 7: IsLocalImport with various paths + testCases := []struct { + path string + expected bool + }{ + {"./foo", true}, + {"../bar", true}, + {"./", true}, + {"fmt", false}, + {"github.com/user/repo", false}, + {"", false}, + } + for _, tc := range testCases { + result := build.IsLocalImport(tc.path) + if result != tc.expected { + panic(fmt.Sprintf("build.IsLocalImport(%q): expected %v, got %v", tc.path, tc.expected, result)) + } + } + fmt.Printf("build.IsLocalImport: all test cases passed\n") + + // Test 8: Verify Context has expected fields + if ctx.GOPATH == "" && ctx.GOROOT == "" { + panic("expected either GOPATH or GOROOT to be set") + } + fmt.Printf("build.Default.GOROOT exists = %v\n", ctx.GOROOT != "") + + // Test 9: Import with AllowBinary flag + binaryPkg, err := build.Import("fmt", "", build.FindOnly|build.AllowBinary) + if err != nil { + panic(fmt.Sprintf("build.Import with AllowBinary failed: %v", err)) + } + if binaryPkg.ImportPath != "fmt" { + panic(fmt.Sprintf("expected ImportPath \"fmt\", got %q", binaryPkg.ImportPath)) + } + fmt.Printf("build.Import(\"fmt\") with AllowBinary: success\n") + + // Test 10: Verify compiler tag in build context + hasCompilerTag := false + for _, tag := range ctx.ReleaseTags { + if strings.HasPrefix(tag, "go1.") { + hasCompilerTag = true + break + } + } + if !hasCompilerTag { + panic("expected at least one go1.x release tag") + } + fmt.Printf("build.Default.ReleaseTags: contains go1.x tags = %v\n", hasCompilerTag) + + fmt.Printf("\nSuccess! All go/build public functions work correctly with llgo\n") + fmt.Printf("Total tests passed: 10\n") +} diff --git a/_demo/go/maphash/maphash.go b/_demo/go/maphash/maphash.go new file mode 100644 index 00000000..a395e0a4 --- /dev/null +++ b/_demo/go/maphash/maphash.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "hash/maphash" +) + +func main() { + testHashBasics() + testMakeSeed() + testSetSeed() + testWriteMethods() + testBytes() + testString() +} + +func testHashBasics() { + fmt.Println("=== Test Hash Basics ===") + var h maphash.Hash + n, err := h.WriteString("hello") + if err != nil { + panic(fmt.Sprintf("WriteString failed: %v", err)) + } + if n != 5 { + panic(fmt.Sprintf("WriteString returned %d, expected 5", n)) + } + hash1 := h.Sum64() + fmt.Printf("Hash of 'hello': 0x%x\n", hash1) + + h.Reset() + n, err = h.WriteString("world") + if err != nil { + panic(fmt.Sprintf("WriteString failed: %v", err)) + } + hash2 := h.Sum64() + fmt.Printf("Hash of 'world': 0x%x\n", hash2) + + h.Reset() + n, err = h.WriteString("hello") + if err != nil { + panic(fmt.Sprintf("WriteString failed: %v", err)) + } + hash3 := h.Sum64() + if hash1 != hash3 { + panic(fmt.Sprintf("Hash mismatch: 0x%x != 0x%x", hash1, hash3)) + } + fmt.Printf("Hash consistency verified: 0x%x == 0x%x\n", hash1, hash3) +} + +func testMakeSeed() { + fmt.Println("\n=== Test MakeSeed ===") + seed1 := maphash.MakeSeed() + seed2 := maphash.MakeSeed() + fmt.Printf("Seed 1: %v\n", seed1) + fmt.Printf("Seed 2: %v\n", seed2) + if seed1 == seed2 { + fmt.Println("Warning: Seeds are identical (rare but possible)") + } +} + +func testSetSeed() { + fmt.Println("\n=== Test SetSeed ===") + var h1, h2 maphash.Hash + seed := maphash.MakeSeed() + + h1.SetSeed(seed) + _, err := h1.WriteString("test") + if err != nil { + panic(fmt.Sprintf("WriteString failed: %v", err)) + } + hash1 := h1.Sum64() + + h2.SetSeed(seed) + _, err = h2.WriteString("test") + if err != nil { + panic(fmt.Sprintf("WriteString failed: %v", err)) + } + hash2 := h2.Sum64() + + if hash1 != hash2 { + panic(fmt.Sprintf("Hashes with same seed should match: 0x%x != 0x%x", hash1, hash2)) + } + fmt.Printf("Same seed produces same hash: 0x%x == 0x%x\n", hash1, hash2) +} + +func testWriteMethods() { + fmt.Println("\n=== Test Write Methods ===") + var h maphash.Hash + + data := []byte("hello") + n, err := h.Write(data) + if err != nil { + panic(fmt.Sprintf("Write failed: %v", err)) + } + if n != len(data) { + panic(fmt.Sprintf("Write returned %d, expected %d", n, len(data))) + } + hash1 := h.Sum64() + fmt.Printf("Hash after Write: 0x%x\n", hash1) + + h.Reset() + err = h.WriteByte('A') + if err != nil { + panic(fmt.Sprintf("WriteByte failed: %v", err)) + } + hash2 := h.Sum64() + fmt.Printf("Hash after WriteByte('A'): 0x%x\n", hash2) +} + +func testBytes() { + fmt.Println("\n=== Test Bytes Function ===") + seed := maphash.MakeSeed() + data := []byte("test data") + hash := maphash.Bytes(seed, data) + fmt.Printf("Bytes hash: 0x%x\n", hash) +} + +func testString() { + fmt.Println("\n=== Test String Function ===") + seed := maphash.MakeSeed() + str := "test string" + hash := maphash.String(seed, str) + fmt.Printf("String hash: 0x%x\n", hash) +} diff --git a/_demo/go/reflectindirect/reflect-indirect.go b/_demo/go/reflectindirect/reflect-indirect.go new file mode 100644 index 00000000..69332a47 --- /dev/null +++ b/_demo/go/reflectindirect/reflect-indirect.go @@ -0,0 +1,49 @@ +package main + +import ( + "reflect" +) + +func main() { + x := 42 + p := &x + + // Test 1: Non-pointer value - should return same value + v1 := reflect.Indirect(reflect.ValueOf(x)) + if !v1.IsValid() || v1.Interface() != 42 { + panic("Non-pointer test failed: expected 42") + } + + // Test 2: Pointer - should dereference + v2 := reflect.Indirect(reflect.ValueOf(p)) + if !v2.IsValid() || v2.Interface() != 42 { + panic("Pointer dereference test failed: expected 42") + } + + // Test 3: Nil pointer - should return invalid Value + var nilPtr *int + v3 := reflect.Indirect(reflect.ValueOf(nilPtr)) + if v3.IsValid() { + panic("Nil pointer test failed: expected invalid Value") + } + + // Test 4: Struct value - should return same value + type Person struct { + Name string + Age int + } + person := Person{Name: "Alice", Age: 30} + v4 := reflect.Indirect(reflect.ValueOf(person)) + if !v4.IsValid() || v4.Interface().(Person).Name != "Alice" || v4.Interface().(Person).Age != 30 { + panic("Struct value test failed: expected Person{Name: Alice, Age: 30}") + } + + // Test 5: Struct pointer - should dereference + personPtr := &Person{Name: "Bob", Age: 25} + v5 := reflect.Indirect(reflect.ValueOf(personPtr)) + if !v5.IsValid() || v5.Interface().(Person).Name != "Bob" || v5.Interface().(Person).Age != 25 { + panic("Struct pointer test failed: expected Person{Name: Bob, Age: 25}") + } + + println("PASS") +} diff --git a/go.mod b/go.mod index 1b81620d..4f51e685 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.1 require ( github.com/goplus/cobra v1.9.12 //gop:class - github.com/goplus/gogen v1.19.2 + github.com/goplus/gogen v1.19.3 github.com/goplus/lib v0.3.0 github.com/goplus/llgo/runtime v0.0.0-00010101000000-000000000000 github.com/goplus/llvm v0.8.5 diff --git a/go.sum b/go.sum index 1cbaeea0..fc6a71c7 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/goplus/cobra v1.9.12 h1:0F9EdEbeGyITGz+mqoHoJ5KpUw97p1CkxV74IexHw5s= github.com/goplus/cobra v1.9.12/go.mod h1:p4LhfNJDKEpiGjGiNn0crUXL5dUPA5DX2ztYpEJR34E= -github.com/goplus/gogen v1.19.2 h1:0WCfbMy9V2gGIUG4gnlhbFkLLuEwVuxOgkzjJjeGsP0= -github.com/goplus/gogen v1.19.2/go.mod h1:owX2e1EyU5WD+Nm6oH2m/GXjLdlBYcwkLO4wN8HHXZI= +github.com/goplus/gogen v1.19.3 h1:sMTe7xME8lWFdPL6NcULykdJtFs9CtXkNACRbaAKTiQ= +github.com/goplus/gogen v1.19.3/go.mod h1:owX2e1EyU5WD+Nm6oH2m/GXjLdlBYcwkLO4wN8HHXZI= github.com/goplus/lib v0.3.0 h1:y0ZGb5Q/RikW1oMMB4Di7XIZIpuzh/7mlrR8HNbxXCA= github.com/goplus/lib v0.3.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= github.com/goplus/llvm v0.8.5 h1:DUnFeYC3Rco622tBEKGg8xkigRAV2fh5ZIfBCt7gOSs= diff --git a/runtime/build.go b/runtime/build.go index a4ac3753..ce5b8915 100644 --- a/runtime/build.go +++ b/runtime/build.go @@ -22,8 +22,10 @@ var hasAltPkg = map[string]none{ "crypto/sha256": {}, "crypto/sha512": {}, "crypto/subtle": {}, + "go/build": {}, "go/parser": {}, "hash/crc32": {}, + "hash/maphash": {}, "internal/abi": {}, "internal/bytealg": {}, "internal/chacha8rand": {}, diff --git a/runtime/internal/clite/bdwgc/bdwgc.go b/runtime/internal/clite/bdwgc/bdwgc.go index 92077a36..8f0af818 100644 --- a/runtime/internal/clite/bdwgc/bdwgc.go +++ b/runtime/internal/clite/bdwgc/bdwgc.go @@ -40,6 +40,20 @@ func Realloc(ptr c.Pointer, size uintptr) c.Pointer //go:linkname Free C.GC_free func Free(ptr c.Pointer) +// AddRoots registers a memory region [start, end) as a GC root. The caller +// must ensure that the range remains valid until RemoveRoots is invoked with +// the same boundaries. This is typically used for TLS slots that store Go +// pointers. +// +//go:linkname AddRoots C.GC_add_roots +func AddRoots(start, end c.Pointer) + +// RemoveRoots unregisters a region previously registered with AddRoots. The +// start and end pointers must exactly match the earlier AddRoots call. +// +//go:linkname RemoveRoots C.GC_remove_roots +func RemoveRoots(start, end c.Pointer) + // ----------------------------------------------------------------------------- //go:linkname RegisterFinalizer C.GC_register_finalizer diff --git a/runtime/internal/clite/tls/tls_common.go b/runtime/internal/clite/tls/tls_common.go new file mode 100644 index 00000000..4741365e --- /dev/null +++ b/runtime/internal/clite/tls/tls_common.go @@ -0,0 +1,128 @@ +//go:build llgo + +/* + * Copyright (c) 2025 The GoPlus Authors (goplus.org). 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 tls provides generic thread-local storage backed by POSIX pthread +// TLS. When built with the GC-enabled configuration (llgo && !nogc), TLS slots +// are automatically registered with the BDWGC garbage collector so pointers +// stored in thread-local state remain visible to the collector. Builds without +// GC integration (llgo && nogc) simply fall back to pthread TLS without root +// registration. +// +// Basic usage: +// +// h := tls.Alloc[int](nil) +// h.Set(42) +// val := h.Get() // returns 42 +// +// With destructor: +// +// h := tls.Alloc[*Resource](func(r **Resource) { +// if r != nil && *r != nil { +// (*r).Close() +// } +// }) +// +// Build tags: +// - llgo && !nogc: Enables GC-aware slot registration via BDWGC +// - llgo && nogc: Disables GC integration; TLS acts as plain pthread TLS +package tls + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/pthread" +) + +type Handle[T any] struct { + key pthread.Key + destructor func(*T) +} + +// Alloc creates a TLS handle backed by pthread TLS. +func Alloc[T any](destructor func(*T)) Handle[T] { + var key pthread.Key + if ret := key.Create(slotDestructor[T]); ret != 0 { + c.Fprintf(c.Stderr, c.Str("tls: pthread_key_create failed (errno=%d)\n"), ret) + panic("tls: failed to create thread local storage key") + } + return Handle[T]{key: key, destructor: destructor} +} + +// Get returns the value stored in the current thread's slot. +func (h Handle[T]) Get() T { + if ptr := h.key.Get(); ptr != nil { + return (*slot[T])(ptr).value + } + var zero T + return zero +} + +// Set stores v in the current thread's slot, creating it if necessary. +func (h Handle[T]) Set(v T) { + s := h.ensureSlot() + s.value = v +} + +// Clear zeroes the current thread's slot value without freeing the slot. +func (h Handle[T]) Clear() { + if ptr := h.key.Get(); ptr != nil { + s := (*slot[T])(ptr) + var zero T + s.value = zero + } +} + +func (h Handle[T]) ensureSlot() *slot[T] { + if ptr := h.key.Get(); ptr != nil { + return (*slot[T])(ptr) + } + size := unsafe.Sizeof(slot[T]{}) + mem := c.Calloc(1, size) + if mem == nil { + panic("tls: failed to allocate thread slot") + } + s := (*slot[T])(mem) + s.destructor = h.destructor + if existing := h.key.Get(); existing != nil { + c.Free(mem) + return (*slot[T])(existing) + } + if ret := h.key.Set(mem); ret != 0 { + c.Free(mem) + c.Fprintf(c.Stderr, c.Str("tls: pthread_setspecific failed (errno=%d)\n"), ret) + panic("tls: failed to set thread local storage value") + } + registerSlot(s) + return s +} + +func slotDestructor[T any](ptr c.Pointer) { + s := (*slot[T])(ptr) + if s == nil { + return + } + if s.destructor != nil { + s.destructor(&s.value) + } + deregisterSlot(s) + var zero T + s.value = zero + s.destructor = nil + c.Free(ptr) +} diff --git a/runtime/internal/clite/tls/tls_gc.go b/runtime/internal/clite/tls/tls_gc.go new file mode 100644 index 00000000..d6341827 --- /dev/null +++ b/runtime/internal/clite/tls/tls_gc.go @@ -0,0 +1,74 @@ +//go:build llgo && !nogc + +/* + * Copyright (c) 2025 The GoPlus Authors (goplus.org). 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 tls + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/bdwgc" +) + +const slotRegistered = 1 << iota + +const maxSlotSize = 1 << 20 // 1 MiB sanity cap + +type slot[T any] struct { + value T + state uintptr + destructor func(*T) +} + +func registerSlot[T any](s *slot[T]) { + if s.state&slotRegistered != 0 { + return + } + start, end := s.rootRange() + size := uintptr(end) - uintptr(start) + if size == 0 { + return + } + if size > maxSlotSize { + panic("tls: slot size exceeds maximum") + } + bdwgc.AddRoots(start, end) + s.state |= slotRegistered +} + +func deregisterSlot[T any](s *slot[T]) { + if s == nil || s.state&slotRegistered == 0 { + return + } + s.state &^= slotRegistered + start, end := s.rootRange() + if uintptr(end) > uintptr(start) { + bdwgc.RemoveRoots(start, end) + } +} + +func (s *slot[T]) rootRange() (start, end c.Pointer) { + begin := unsafe.Pointer(s) + size := unsafe.Sizeof(*s) + beginAddr := uintptr(begin) + if beginAddr > ^uintptr(0)-size { + panic("tls: pointer arithmetic overflow in rootRange") + } + endPtr := unsafe.Pointer(beginAddr + size) + return c.Pointer(begin), c.Pointer(endPtr) +} diff --git a/runtime/internal/clite/tls/tls_nogc.go b/runtime/internal/clite/tls/tls_nogc.go new file mode 100644 index 00000000..15f7cd1e --- /dev/null +++ b/runtime/internal/clite/tls/tls_nogc.go @@ -0,0 +1,28 @@ +//go:build llgo && nogc + +/* + * Copyright (c) 2025 The GoPlus Authors (goplus.org). 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 tls + +type slot[T any] struct { + value T + destructor func(*T) +} + +func registerSlot[T any](s *slot[T]) {} + +func deregisterSlot[T any](s *slot[T]) {} diff --git a/runtime/internal/clite/tls/tls_test.go b/runtime/internal/clite/tls/tls_test.go new file mode 100644 index 00000000..9c0bd307 --- /dev/null +++ b/runtime/internal/clite/tls/tls_test.go @@ -0,0 +1,141 @@ +//go:build llgo + +/* + * Copyright (c) 2025 The GoPlus Authors (goplus.org). 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 tls_test + +import ( + "fmt" + "sync" + "testing" + + "github.com/goplus/llgo/runtime/internal/clite/tls" +) + +func TestAllocReadWrite(t *testing.T) { + h := tls.Alloc[int](nil) + if got := h.Get(); got != 0 { + t.Fatalf("zero slot = %d, want 0", got) + } + h.Set(42) + if got := h.Get(); got != 42 { + t.Fatalf("Set/Get mismatch: got %d", got) + } + h.Clear() + if got := h.Get(); got != 0 { + t.Fatalf("Clear() did not reset slot, got %d", got) + } +} + +func TestAllocThreadLocalIsolation(t *testing.T) { + h := tls.Alloc[int](nil) + h.Set(7) + + const want = 99 + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if got := h.Get(); got != 0 { + t.Errorf("new goroutine initial value = %d, want 0", got) + } + h.Set(want) + if got := h.Get(); got != want { + t.Errorf("goroutine value = %d, want %d", got, want) + } + }() + wg.Wait() + + if got := h.Get(); got != 7 { + t.Fatalf("main goroutine value changed to %d", got) + } +} + +func TestDestructorRuns(t *testing.T) { + var mu sync.Mutex + var calls int + values := make([]int, 0, 1) + + h := tls.Alloc[*int](func(p **int) { + mu.Lock() + defer mu.Unlock() + if p != nil && *p != nil { + calls++ + values = append(values, **p) + } + }) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + val := new(int) + *val = 123 + h.Set(val) + }() + wg.Wait() + + mu.Lock() + defer mu.Unlock() + if calls == 0 { + t.Fatalf("expected destructor to be invoked") + } + if len(values) != 1 || values[0] != 123 { + t.Fatalf("destructor saw unexpected values: %v", values) + } +} + +func TestAllocStress(t *testing.T) { + const sequentialIterations = 200_000 + + h := tls.Alloc[int](nil) + for i := 0; i < sequentialIterations; i++ { + h.Set(i) + if got := h.Get(); got != i { + t.Fatalf("stress iteration %d: got %d want %d", i, got, i) + } + } + + var wg sync.WaitGroup + const ( + goroutines = 32 + iterationsPerGoroutine = 1_000 + ) + errs := make(chan error, goroutines) + wg.Add(goroutines) + for g := 0; g < goroutines; g++ { + go func(offset int) { + defer wg.Done() + local := tls.Alloc[int](nil) + for i := 0; i < iterationsPerGoroutine; i++ { + v := offset*iterationsPerGoroutine + i + local.Set(v) + if got := local.Get(); got != v { + errs <- fmt.Errorf("goroutine %d iteration %d: got %d want %d", offset, i, got, v) + return + } + } + }(g) + } + wg.Wait() + close(errs) + for err := range errs { + if err != nil { + t.Fatal(err) + } + } +} diff --git a/runtime/internal/lib/go/build/build.go b/runtime/internal/lib/go/build/build.go new file mode 100644 index 00000000..576fa0a7 --- /dev/null +++ b/runtime/internal/lib/go/build/build.go @@ -0,0 +1,19 @@ +// Copyright 2024 The GoPlus Authors (goplus.org). All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package build provides alternative implementations for go/build. +// We override build.Default.Compiler in an init function. + +package build + +import ( + "go/build" +) + +func init() { + // LLGO PATCH: Override build.Default.Compiler to be "gc" instead of "llgo" + // This prevents "unknown compiler" errors when user code uses go/build package + // Even though runtime.Compiler = "llgo", we set build.Default.Compiler = "gc" + build.Default.Compiler = "gc" +} diff --git a/runtime/internal/lib/hash/maphash/maphash.go b/runtime/internal/lib/hash/maphash/maphash.go new file mode 100644 index 00000000..ed490b83 --- /dev/null +++ b/runtime/internal/lib/hash/maphash/maphash.go @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 maphash + +import "unsafe" + +// NOTE: The following functions are not yet implemented and will panic with "intrinsic": +// - Comparable(seed Seed, v any) uint64 +// - (*Hash).WriteComparable(v any) (int, error) +// These functions require runtime intrinsic support that is not currently available. + +//go:linkname runtime_rand github.com/goplus/llgo/runtime/internal/runtime.fastrand64 +func runtime_rand() uint64 + +//go:linkname runtime_memhash github.com/goplus/llgo/runtime/internal/runtime.memhash +func runtime_memhash(p unsafe.Pointer, seed, s uintptr) uintptr + +func randUint64() uint64 { + return runtime_rand() +} diff --git a/runtime/internal/lib/internal/bytealg/bytealg.go b/runtime/internal/lib/internal/bytealg/bytealg.go index 8434073c..330c2bef 100644 --- a/runtime/internal/lib/internal/bytealg/bytealg.go +++ b/runtime/internal/lib/internal/bytealg/bytealg.go @@ -23,7 +23,7 @@ import ( "github.com/goplus/llgo/runtime/internal/runtime" ) -// llgo:skip init CompareString +// llgo:skip init type _bytealg struct{} func IndexByte(b []byte, ch byte) int { diff --git a/runtime/internal/lib/reflect/value.go b/runtime/internal/lib/reflect/value.go index 4cec494d..f1657ab5 100644 --- a/runtime/internal/lib/reflect/value.go +++ b/runtime/internal/lib/reflect/value.go @@ -1759,6 +1759,16 @@ func ValueOf(i any) Value { return unpackEface(i) } +// Indirect returns the value that v points to. +// If v is a nil pointer, Indirect returns a zero Value. +// If v is not a pointer, Indirect returns v. +func Indirect(v Value) Value { + if v.Kind() != Pointer { + return v + } + return v.Elem() +} + // arrayAt returns the i-th element of p, // an array whose elements are eltSize bytes wide. // The array pointed at by p must have at least i+1 elements: diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index 04d236eb..a583611f 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -140,6 +140,10 @@ type Func struct { opaque struct{} // unexported field to disallow conversions } +func (f *Func) Name() string { + panic("todo") +} + // moduledata records information about the layout of the executable // image. It is written by the linker. Any changes here must be // matched changes to the code in cmd/link/internal/ld/symtab.go:symtab. diff --git a/ssa/di.go b/ssa/di.go index f89ce818..5778cd4f 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -53,6 +53,96 @@ func newDIBuilder(prog Program, pkg Package, positioner Positioner) diBuilder { return b } +func hasTypeParam(typ types.Type) bool { + visited := make(map[types.Type]bool) + var visit func(types.Type) bool + visit = func(tt types.Type) bool { + if tt == nil { + return false + } + if visited[tt] { + return false + } + visited[tt] = true + switch t := tt.(type) { + case *types.TypeParam: + return true + case *types.Named: + if tp := t.TypeParams(); tp != nil && tp.Len() > 0 { + if ta := t.TypeArgs(); ta == nil || ta.Len() == 0 { + return true + } + } + if ta := t.TypeArgs(); ta != nil { + for i := 0; i < ta.Len(); i++ { + if visit(ta.At(i)) { + return true + } + } + } + return visit(t.Underlying()) + case *types.Pointer: + return visit(t.Elem()) + case *types.Slice: + return visit(t.Elem()) + case *types.Array: + return visit(t.Elem()) + case *types.Map: + return visit(t.Key()) || visit(t.Elem()) + case *types.Chan: + return visit(t.Elem()) + case *types.Signature: + if tp := t.TypeParams(); tp != nil && tp.Len() > 0 { + return true + } + if params := t.Params(); params != nil { + for i := 0; i < params.Len(); i++ { + if visit(params.At(i).Type()) { + return true + } + } + } + if results := t.Results(); results != nil { + for i := 0; i < results.Len(); i++ { + if visit(results.At(i).Type()) { + return true + } + } + } + return false + case *types.Tuple: + for i := 0; i < t.Len(); i++ { + if visit(t.At(i).Type()) { + return true + } + } + return false + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if visit(t.Field(i).Type()) { + return true + } + } + return false + case *types.Interface: + for i := 0; i < t.NumMethods(); i++ { + if visit(t.Method(i).Type()) { + return true + } + } + for i := 0; i < t.NumEmbeddeds(); i++ { + if visit(t.EmbeddedType(i)) { + return true + } + } + return false + default: + return false + } + } + return visit(typ) +} + // New method to add named metadata operand func (b diBuilder) addNamedMetadataOperand(name string, intValue int, stringValue string, intValue2 int) { ctx := b.m.Context() @@ -522,6 +612,9 @@ func (b diBuilder) dbgValue(v Expr, dv DIVar, scope DIScope, pos token.Position, } func (b diBuilder) diType(t Type, pos token.Position) DIType { + if hasTypeParam(t.RawType()) { + return &aDIType{} + } name := t.RawType().String() return b.diTypeEx(name, t, pos) } diff --git a/ssa/di_test.go b/ssa/di_test.go new file mode 100644 index 00000000..86aac367 --- /dev/null +++ b/ssa/di_test.go @@ -0,0 +1,166 @@ +//go:build !llgo + +/* + * Copyright (c) 2025 The GoPlus Authors (goplus.org). 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" + "testing" +) + +func TestHasTypeParam(t *testing.T) { + generic := newGenericNamedType("Box") + instantiated, err := types.Instantiate(types.NewContext(), generic, []types.Type{types.Typ[types.String]}, true) + if err != nil { + t.Fatalf("Instantiate: %v", err) + } + partialArg := newTypeParam("PartialArg") + partialInstance, err := types.Instantiate(types.NewContext(), generic, []types.Type{partialArg}, false) + if err != nil { + t.Fatalf("Instantiate partial: %v", err) + } + + arrayType := func() types.Type { + tp := newTypeParam("ArrayElem") + return types.NewArray(tp, 3) + }() + + chanType := types.NewChan(types.SendRecv, newTypeParam("ChanElem")) + + tupleType := func() types.Type { + tp := newTypeParam("TupleElem") + elem := types.NewVar(token.NoPos, nil, "v", tp) + return types.NewTuple(elem) + }() + + structWithParam := func() types.Type { + tp := newTypeParam("StructElem") + field := types.NewVar(token.NoPos, nil, "value", tp) + return types.NewStruct([]*types.Var{field}, nil) + }() + + signatureWithParam := func() types.Type { + tp := newTypeParam("SigParam") + params := types.NewTuple(types.NewVar(token.NoPos, nil, "x", tp)) + return types.NewSignatureType(nil, nil, []*types.TypeParam{tp}, params, types.NewTuple(), false) + }() + + signatureWithResult := func() types.Type { + tp := newTypeParam("SigResult") + results := types.NewTuple(types.NewVar(token.NoPos, nil, "res", tp)) + return types.NewSignatureType(nil, nil, []*types.TypeParam{tp}, types.NewTuple(), results, false) + }() + signatureParamWithoutDecl := func() types.Type { + tp := newTypeParam("SigParamExternal") + params := types.NewTuple(types.NewVar(token.NoPos, nil, "x", tp)) + return types.NewSignatureType(nil, nil, nil, params, types.NewTuple(), false) + }() + signatureResultWithoutDecl := func() types.Type { + tp := newTypeParam("SigResultExternal") + results := types.NewTuple(types.NewVar(token.NoPos, nil, "res", tp)) + return types.NewSignatureType(nil, nil, nil, types.NewTuple(), results, false) + }() + + interfaceWithParam := func() types.Type { + tp := newTypeParam("IfaceParam") + params := types.NewTuple(types.NewVar(token.NoPos, nil, "v", tp)) + method := types.NewFunc(token.NoPos, nil, "Do", types.NewSignatureType(nil, nil, []*types.TypeParam{tp}, params, types.NewTuple(), false)) + iface := types.NewInterfaceType([]*types.Func{method}, nil) + iface.Complete() + return iface + }() + + interfaceWithEmbed := func() types.Type { + base := interfaceWithParam + tp := newTypeParam("EmbedParam") + embedMethod := types.NewFunc(token.NoPos, nil, "Run", types.NewSignatureType(nil, nil, []*types.TypeParam{tp}, types.NewTuple(), types.NewTuple(), false)) + iface := types.NewInterfaceType([]*types.Func{embedMethod}, []types.Type{base}) + iface.Complete() + return iface + }() + interfaceWithEmbeddedOnly := func() types.Type { + embedded := interfaceWithParam + iface := types.NewInterfaceType(nil, []types.Type{embedded}) + iface.Complete() + return iface + }() + + selfRecursive := func() types.Type { + typeName := types.NewTypeName(token.NoPos, nil, "Node", nil) + placeholder := types.NewStruct(nil, nil) + named := types.NewNamed(typeName, placeholder, nil) + field := types.NewVar(token.NoPos, nil, "next", types.NewPointer(named)) + structType := types.NewStruct([]*types.Var{field}, nil) + named.SetUnderlying(structType) + return named + }() + + tests := []struct { + name string + typ types.Type + want bool + }{ + {"nilType", nil, false}, + {"basic", types.Typ[types.Int], false}, + {"typeParam", newTypeParam("T"), true}, + {"pointerToTypeParam", types.NewPointer(newTypeParam("PtrT")), true}, + {"sliceOfTypeParam", types.NewSlice(newTypeParam("SliceT")), true}, + {"arrayOfTypeParam", arrayType, true}, + {"mapWithTypeParam", types.NewMap(newTypeParam("MapKey"), types.Typ[types.String]), true}, + {"chanOfTypeParam", chanType, true}, + {"tupleWithTypeParam", tupleType, true}, + {"structWithTypeParam", structWithParam, true}, + {"signatureWithTypeParam", signatureWithParam, true}, + {"signatureWithResultTypeParam", signatureWithResult, true}, + {"signatureParamWithoutDecl", signatureParamWithoutDecl, true}, + {"signatureResultWithoutDecl", signatureResultWithoutDecl, true}, + {"interfaceWithTypeParam", interfaceWithParam, true}, + {"interfaceWithEmbeddedTypeParam", interfaceWithEmbed, true}, + {"interfaceWithEmbeddedOnlyTypeParam", interfaceWithEmbeddedOnly, true}, + {"namedGeneric", generic, true}, + {"pointerToNamedGeneric", types.NewPointer(generic), true}, + {"namedInstanceWithTypeParamArg", partialInstance, true}, + {"namedInstanceNoParam", instantiated, false}, + {"selfRecursiveStruct", selfRecursive, false}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + if got := hasTypeParam(tc.typ); got != tc.want { + t.Fatalf("hasTypeParam(%s) = %v, want %v", tc.name, got, tc.want) + } + }) + } +} + +func newTypeParam(name string) *types.TypeParam { + iface := types.NewInterfaceType(nil, nil) + iface.Complete() + return types.NewTypeParam(types.NewTypeName(token.NoPos, nil, name, nil), iface) +} + +func newGenericNamedType(name string) *types.Named { + tp := newTypeParam("T") + field := types.NewVar(token.NoPos, nil, "value", tp) + structType := types.NewStruct([]*types.Var{field}, nil) + named := types.NewNamed(types.NewTypeName(token.NoPos, nil, name, nil), structType, nil) + named.SetTypeParams([]*types.TypeParam{tp}) + return named +}