Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions amd64_translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,10 @@ func (c *amd64Ctx) lowerInstr(bi int, ii int, ins Instr, emitBr amd64EmitBr, emi
return true, fmt.Errorf("amd64 RET expects at most 1 operand: %q", ins.Raw)
}
return true, c.lowerRET()
case "PCALIGN", "NO_LOCAL_POINTERS", "PCDATA", "FUNCDATA", "NOP",
case "PCALIGN", "GO_ARGS", "NO_LOCAL_POINTERS", "PCDATA", "FUNCDATA", "NOP",
"PUSH_REGS_HOST_TO_ABI0()", "POP_REGS_HOST_TO_ABI0()":
// Alignment directive emitted by stdlib asm; no semantic effect in our IR.
// Go assembler metadata (including GO_ARGS's argument pointer map) has
// no machine-code effect here; LLVM handles the argument values directly.
return false, nil
case "ADJSP":
if c.goarch != "386" {
Expand Down
4 changes: 2 additions & 2 deletions doc/plan9asm-corpus.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ silently include a nested `/v2` module that happens to share its import prefix.
The currently tracked reports are:

- `xgo-dev/llgo#2464`: `github.com/coder/websocket v1.8.15`
- `xgo-dev/llgo#2552`: `github.com/klauspost/compress v1.20.0`
- `xgo-dev/llgo#2552`: `github.com/klauspost/compress v1.20.1`
- `xgo-dev/llgo#2576`: `github.com/tmthrgd/go-hex` at its latest pseudo-version

The ecosystem scan also pins these latest modules:
Expand All @@ -229,7 +229,7 @@ The ecosystem scan also pins these latest modules:
- `github.com/klauspost/reedsolomon v1.14.2`
- `github.com/minio/highwayhash v1.0.4`
- `github.com/modern-go/gls v0.0.0-20250215024828-78308f6bb19d`
- `github.com/pierrec/lz4/v4 v4.1.30`
- `github.com/pierrec/lz4/v4 v4.1.31`
- `github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50`
- `github.com/tmthrgd/go-bitwise v0.0.0-20190904053232-1430ee983fca`
- `github.com/tmthrgd/go-popcount v0.0.0-20190904054823-afb1ace8b04f`
Expand Down
46 changes: 46 additions & 0 deletions go_translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,13 @@ func goLLVMTypeForTypeWithSizes(t types.Type, goarch string, sz types.Sizes) (LL
if tt.NumFields() == 0 {
return LLVMType("[0 x i8]"), nil
}
if parts, ok := goWordStructParts(tt, goarch, sz, sz); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aggregate LLVM type here is derived from a second, value-only goWordStructParts call. But on the non-flatten argument path in goLLVMArgsAndFrameSlotsForTuple, goFramePartsForTypeWithSizes has already run goWordStructParts and produced parts whose .Type fields are exactly these element types — so the struct type is classified and each field re-lowered twice per struct argument.

Beyond the duplicate compile-time work, this creates an implicit coupling: the arg path stays correct only because goWordStructParts requires word(sz) == word(frameSz), so the value-only call agrees with the frame-aware one. If that guard is ever relaxed (e.g. to allow value-word != frame-word structs), the aggregate type and the frame layout could silently diverge. Consider building the LLVM aggregate string from the already-computed parts in the non-flatten branch instead of recomputing via a separate value-only call.

fields := make([]string, len(parts))
for i, part := range parts {
fields[i] = string(part.Type)
}
return LLVMType("{ " + strings.Join(fields, ", ") + " }"), nil
}
return "", fmt.Errorf("unsupported struct type %s", tt.String())
case *types.Named:
return goLLVMTypeForTypeWithSizes(tt.Underlying(), goarch, sz)
Expand Down Expand Up @@ -829,6 +836,9 @@ func goLLVMArgsAndFrameSlotsForTuple(tup *types.Tuple, goarch string, sz, frameS
off += int64(frameSz.Sizeof(t))
continue
}
if st, ok := t.Underlying().(*types.Struct); ok && st.NumFields() != 0 {
return nil, nil, 0, fmt.Errorf("unsupported struct frame layout %s", t.String())
}

ty, e := goLLVMTypeForTypeWithSizes(t, goarch, sz)
if e != nil {
Expand Down Expand Up @@ -868,10 +878,46 @@ func goFramePartsForTypeWithSizes(t types.Type, goarch string, sz, frameSz types
return []goFramePart{{Offset: 0, Type: Ptr, Field: 0}, {Offset: word, Type: wordTy, Field: 1}, {Offset: 2 * word, Type: wordTy, Field: 2}}, true
case *types.Interface:
return []goFramePart{{Offset: 0, Type: Ptr, Field: 0}, {Offset: word, Type: Ptr, Field: 1}}, true
case *types.Struct:
return goWordStructParts(u, goarch, sz, frameSz)
}
return nil, false
}

// goWordStructParts accepts only flat structures whose fields are exactly one
// integer/pointer word each in both the Go value and the assembly FP frame.
// Other structures need target-specific padding and register classification.
Comment thread
cpunion marked this conversation as resolved.
// Even word-sized nested structs are rejected: supporting them would require
// recursive flattening and nested field-index mapping instead of a single Field.
func goWordStructParts(st *types.Struct, goarch string, sz, frameSz types.Sizes) ([]goFramePart, bool) {
word := int64(goWordSizeForSizes(goarch, sz))
if st.NumFields() == 0 || word != int64(goWordSizeForSizes(goarch, frameSz)) {
return nil, false
}
fields := make([]*types.Var, st.NumFields())
for i := range fields {
fields[i] = st.Field(i)
}
valueOffsets := sz.Offsetsof(fields)
frameOffsets := frameSz.Offsetsof(fields)
if sz.Sizeof(st) != int64(len(fields))*word || frameSz.Sizeof(st) != int64(len(fields))*word {
return nil, false
}
parts := make([]goFramePart, len(fields))
for i, field := range fields {
if valueOffsets[i] != int64(i)*word || frameOffsets[i] != int64(i)*word ||
sz.Sizeof(field.Type()) != word || frameSz.Sizeof(field.Type()) != word {
return nil, false
}
ty, err := goLLVMTypeForTypeWithSizes(field.Type(), goarch, sz)
if err != nil || (ty != I32 && ty != I64 && ty != Ptr) {
return nil, false
}
parts[i] = goFramePart{Offset: frameOffsets[i], Type: ty, Field: i}
}
return parts, true
}

func goWordSize(goarch string) int {
switch goarch {
case "amd64", "arm64", "wasm":
Expand Down
93 changes: 92 additions & 1 deletion go_translate_deep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import (
"go/parser"
"go/token"
"go/types"
"reflect"
"strings"
"testing"

"github.com/xgo-dev/llvm"
)

func mustGoPackageWithImports(t *testing.T, pkgPath string, files map[string]string) GoPackage {
Expand Down Expand Up @@ -135,7 +138,8 @@ func TestGoTranslateTypeCoverage(t *testing.T) {
{alias, "arm64", I64, true},
{types.Typ[types.Complex64], "amd64", "", false},
{types.NewStruct(nil, nil), "amd64", LLVMType("[0 x i8]"), true},
{types.NewStruct([]*types.Var{types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])}, nil), "amd64", "", false},
{types.NewStruct([]*types.Var{types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])}, nil), "amd64", "{ i64 }", true},
{types.NewStruct([]*types.Var{types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int8])}, nil), "amd64", "", false},
} {
got, err := goLLVMTypeForType(tc.typ, tc.goarch)
if (err == nil) != tc.ok || got != tc.want {
Expand Down Expand Up @@ -179,6 +183,93 @@ func TestGoTranslateTypeCoverage(t *testing.T) {
}
}

func TestGoTranslateWordStructABI(t *testing.T) {
pkg := mustGoPackage(t, "test/pkg", `package testpkg
type Uint128 struct { Lo, Hi uint64 }
func Y(t *byte, a, b Uint128, res uintptr) int32
func X(t *byte, a, b Uint128, res uintptr) int32
`)
fn := pkg.Types.Scope().Lookup("Y").(*types.Func)
sz := types.SizesFor("gc", "amd64")
sig, err := goFuncSigForDeclaredFunc("test/pkg.Y", fn, "amd64", sz, sz, true)
if err != nil {
t.Fatal(err)
}
if want := []LLVMType{Ptr, "{ i64, i64 }", "{ i64, i64 }", I64}; !reflect.DeepEqual(sig.Args, want) {
t.Fatalf("args = %v, want %v", sig.Args, want)
}
if want := []FrameSlot{
{Offset: 0, Type: Ptr, Index: 0, Field: -1},
{Offset: 8, Type: I64, Index: 1, Field: 0},
{Offset: 16, Type: I64, Index: 1, Field: 1},
{Offset: 24, Type: I64, Index: 2, Field: 0},
{Offset: 32, Type: I64, Index: 2, Field: 1},
{Offset: 40, Type: I64, Index: 3, Field: -1},
}; !reflect.DeepEqual(sig.Frame.Params, want) {
t.Fatalf("frame params = %v, want %v", sig.Frame.Params, want)
}
if want := []FrameSlot{{Offset: 48, Type: I32, Index: 0, Field: -1}}; !reflect.DeepEqual(sig.Frame.Results, want) {
t.Fatalf("frame results = %v, want %v", sig.Frame.Results, want)
}
if _, err := goFuncSigForDeclaredFunc("test/pkg.Y", fn, "386", types.SizesFor("gc", "386"), nil, true); err == nil {
t.Fatal("Uint128 must not be flattened into 32-bit frame words")
}

tr, err := TranslateGoModule(pkg, []byte(`TEXT ·Y(SB),$56-52
GO_ARGS
NO_LOCAL_POINTERS
MOVQ t+0(FP), AX
MOVQ AX, 0(SP)
MOVQ a_Lo+8(FP), AX
MOVQ AX, 8(SP)
MOVQ a_Hi+16(FP), AX
MOVQ AX, 16(SP)
MOVQ b_Lo+24(FP), AX
MOVQ AX, 24(SP)
MOVQ b_Hi+32(FP), AX
MOVQ AX, 32(SP)
MOVQ res+40(FP), AX
MOVQ AX, 40(SP)
CALL ·X(SB)
MOVL 48(SP), AX
MOVL AX, ret+48(FP)
RET
`), GoModuleOptions{GOARCH: "amd64", TargetTriple: "x86_64-unknown-linux-gnu", ResolveSym: testResolveSym("test/pkg")})
if err != nil {
t.Fatal(err)
}
defer tr.Module.Dispose()
if err := llvm.VerifyModule(tr.Module, llvm.ReturnStatusAction); err != nil {
t.Fatalf("invalid LLVM IR: %v", err)
}
ir := tr.Module.String()
if !strings.Contains(ir, "extractvalue { i64, i64 } %arg1, 0") || !strings.Contains(ir, "extractvalue { i64, i64 } %arg2, 1") || !strings.Contains(ir, "call i32 @\"test/pkg.X\"") {
t.Fatalf("missing aggregate argument forwarding in LLVM IR:\n%s", ir)
Comment thread
cpunion marked this conversation as resolved.
}
}

func TestGoTranslateWordStructGuards(t *testing.T) {
for _, tc := range []struct {
name, arg, frameArch string
}{
{"padding", "struct { a uint8; b uint64 }", "amd64"},
{"subword", "struct { a, b uint32 }", "amd64"},
{"nested", "struct { inner struct { x uint64 } }", "amd64"},
{"float", "struct { x float64 }", "amd64"},
{"frame-word-mismatch", "struct { x uintptr }", "386"},
} {
t.Run(tc.name, func(t *testing.T) {
pkg := mustGoPackage(t, "test/pkg", "package testpkg\nfunc F(x "+tc.arg+")")
fn := pkg.Types.Scope().Lookup("F").(*types.Func)
sz := types.SizesFor("gc", "amd64")
frameSz := types.SizesFor("gc", tc.frameArch)
if _, err := goFuncSigForDeclaredFunc("test/pkg.F", fn, "amd64", sz, frameSz, true); err == nil || !strings.Contains(err.Error(), "unsupported struct") {
t.Fatalf("expected unsupported struct layout, got %v", err)
}
})
}
}

func TestGoTranslateSignatureCoverage(t *testing.T) {
pkg := mustGoPackage(t, "test/pkg", `package testpkg
type S struct{}
Expand Down
4 changes: 2 additions & 2 deletions testdata/corpus/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ require (
github.com/dgryski/go-bits v0.0.0-20180113010104-bd8a69a71dc2
github.com/dgryski/go-marvin32 v0.0.0-20240117220238-0d39e8c5a8a9
github.com/golang/snappy v1.0.0
github.com/klauspost/compress v1.20.0
github.com/klauspost/compress v1.20.1
github.com/klauspost/cpuid v1.3.1
github.com/klauspost/cpuid/v2 v2.4.0
github.com/klauspost/reedsolomon v1.14.2
github.com/minio/highwayhash v1.0.4
github.com/modern-go/gls v0.0.0-20250215024828-78308f6bb19d
github.com/pierrec/lz4/v4 v4.1.30
github.com/pierrec/lz4/v4 v4.1.31
github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50
github.com/tmthrgd/go-bitwise v0.0.0-20190904053232-1430ee983fca
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc
Expand Down
4 changes: 4 additions & 0 deletions testdata/corpus/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
Expand All @@ -43,6 +45,8 @@ github.com/pierrec/lz4/v4 v4.1.29 h1:CDQY6qZOLI4DW0Nx6R1vRrifrCeQHnNXkMb0hZWXFjg
github.com/pierrec/lz4/v4 v4.1.29/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pierrec/lz4/v4 v4.1.30 h1:cchX8N2DVP668WkElI9QMwVyoNabLkq1LofDHFeIrdg=
github.com/pierrec/lz4/v4 v4.1.30/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pierrec/lz4/v4 v4.1.31 h1:TI8ck6XSudzSzotzAmy0+kh/KpRHaVsKLPzS97gRyNg=
github.com/pierrec/lz4/v4 v4.1.31/go.mod h1:7SE9MC2STkNtL4PIwGhjmyVwvILaGI9/COYQNBhKM/c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
Expand Down
25 changes: 17 additions & 8 deletions testdata/corpus/reported-libraries.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"https://github.com/xgo-dev/llgo/issues/2552"
],
"module": "github.com/klauspost/compress",
"version": "v1.20.0",
"version": "v1.20.1",
"inventory": {
"darwin/amd64": {
"asm_files": 8,
Expand Down Expand Up @@ -360,11 +360,14 @@
"id": "pierrec-lz4-v4",
"origin": "ecosystem-scan",
"module": "github.com/pierrec/lz4/v4",
"version": "v4.1.30",
"version": "v4.1.31",
"inventory": {
"darwin/amd64": {
"asm_files": 1,
"packages": ["github.com/pierrec/lz4/v4/internal/lz4block"]
"asm_files": 2,
"packages": [
"github.com/pierrec/lz4/v4/internal/lz4block",
"github.com/pierrec/lz4/v4/internal/xxh32"
]
},
"darwin/arm64": {
"asm_files": 2,
Expand All @@ -374,8 +377,11 @@
]
},
"linux/amd64": {
"asm_files": 1,
"packages": ["github.com/pierrec/lz4/v4/internal/lz4block"]
"asm_files": 2,
"packages": [
"github.com/pierrec/lz4/v4/internal/lz4block",
"github.com/pierrec/lz4/v4/internal/xxh32"
]
},
"linux/arm": {
"asm_files": 2,
Expand All @@ -392,8 +398,11 @@
]
},
"windows/amd64": {
"asm_files": 1,
"packages": ["github.com/pierrec/lz4/v4/internal/lz4block"]
"asm_files": 2,
"packages": [
"github.com/pierrec/lz4/v4/internal/lz4block",
"github.com/pierrec/lz4/v4/internal/xxh32"
]
},
"windows/arm64": {
"asm_files": 2,
Expand Down
Loading