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
11 changes: 11 additions & 0 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ type context struct {
vargs map[*ssa.Alloc][]llssa.Expr // varargs
funcs map[*ssa.Function]llssa.Function
variadicErrors map[*ssa.Function]error
nativeStorageError error
llvmIntrinsics map[*ssa.Function]*llssa.LLVMIntrinsic
linkOnceFns map[*ssa.Function]none
stackDefers map[*ssa.Function]bool
Expand Down Expand Up @@ -1857,6 +1858,13 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue
ret = p.nilOf(t)
break
}
if err := p.prog.CheckNativeConversion(p.patchType(t), p.patchType(v.X.Type())); err != nil {
if p.nativeStorageError == nil {
p.nativeStorageError = fmt.Errorf("%s: %w", p.fset.Position(v.Pos()), err)
}
ret = p.nilOf(t)
break
}
x := p.compileValue(b, v.X)
ret = b.ChangeType(p.type_(t, llssa.GoDecl), x)
case *ssa.Convert:
Expand Down Expand Up @@ -3151,6 +3159,9 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri
if err = ctx.validateCVariadicWrapperUses(); err != nil {
return nil, nil, err
}
if ctx.nativeStorageError != nil {
return nil, nil, ctx.nativeStorageError
}
ret.MaterializePreserveSyms()
if metaCollect {
if err := ret.FinishMetaCollection(); err != nil {
Expand Down
10 changes: 8 additions & 2 deletions cl/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,8 @@ func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package,

// ParsePkgSyntaxWithOptions collects all Program-side declaration metadata.
// LLVM Package effects such as preserving //export symbols are applied later.
func ParsePkgSyntaxWithOptions(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File, options Options) error {
// An optional Info supplies live RHS bindings while the package is checked.
func ParsePkgSyntaxWithOptions(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File, options Options, typeInfo ...*types.Info) error {
if pkg == nil {
return nil
}
Expand Down Expand Up @@ -1117,6 +1118,11 @@ func ParsePkgSyntaxWithOptions(prog llssa.Program, fset *token.FileSet, pkg *typ
if err := collectPackageLinknames(prog, fset, files, syms, linkDocs); err != nil {
return err
}
var info *types.Info
if len(typeInfo) != 0 {
info = typeInfo[0]
}
registerTypeDefinitions(prog, fset, pkg, files, info)
prog.MarkPackageSyntaxParsed(pkg)
return nil
}
Expand Down Expand Up @@ -1170,7 +1176,7 @@ func handleTypeDecl(prog llssa.Program, fset *token.FileSet, pkg *types.Package,
positions = append(positions, name.Pos())
}
}
prog.SetNativeStructFields(positions)
prog.SetNativeStructFields(positions, spec.Assign.IsValid())
}
return nil
}
Expand Down
83 changes: 83 additions & 0 deletions cl/representation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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"
"sync"

llssa "github.com/xgo-dev/llgo/ssa"
)

// registerTypeDefinitions keeps the source definition edges that go/types
// deliberately omits from Named's public API. Pass the same Info being filled
// by the checker so layout queries can see an RHS as soon as it is bound.
// Declaration positions identify declarations within this package, not the
// owner of shared struct fields. RHS types always come from semantic binding.
//
// A one-shot SSA client may have discarded Info already. For that path, query
// only named RHS expressions in the completed package's original lexical scope.
func registerTypeDefinitions(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File, info *types.Info) {
if pkg == nil {
return
}
rhs := make(map[token.Pos]ast.Expr)
for _, file := range files {
ast.Inspect(file, func(node ast.Node) bool {
if spec, ok := node.(*ast.TypeSpec); ok {
switch ast.Unparen(spec.Type).(type) {
case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.IndexListExpr:
rhs[spec.Name.Pos()] = spec.Type
}
}
return true
})
}
var mu sync.Mutex
resolved := make(map[*types.TypeName]types.Type)
prog.SetTypeDefinitionSource(pkg, func(obj *types.TypeName) types.Type {
expr := rhs[obj.Pos()]
if expr == nil {
return nil
}
if info != nil {
if t := info.TypeOf(expr); t != nil {
return t
}
}
if !pkg.Complete() {
return nil
}
mu.Lock()
defer mu.Unlock()
if t, ok := resolved[obj]; ok {
return t
}
bound := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
if err := types.CheckExpr(fset, pkg, expr.Pos(), expr, bound); err != nil {
// This is already checked source. Silently selecting Go storage
// here would turn a lost scope/import into a representation change.
panic(fmt.Errorf("resolve representation of %s: %w", obj, err))
}
t := bound.TypeOf(expr)
resolved[obj] = t
return t
})
}
100 changes: 100 additions & 0 deletions cl/representation_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package cl

import (
"encoding/json"
"go/token"
"go/types"
"testing"

"github.com/xgo-dev/llgo/internal/frontcache"
llssa "github.com/xgo-dev/llgo/ssa"
)

func TestNativeRepresentationExportMetadata(t *testing.T) {
prog := llssa.NewProgram(&llssa.Target{GOOS: "linux", GOARCH: "amd64"})
defer prog.Dispose()
pkg, _ := checkRepresentationPackage(t, prog, "example.com/native", `package native
//llgo:cdecl
type private struct { Fn func(); Tail uint64 }
type First private
type Exported First
type Alias = private
type ThroughAlias Alias
//llgo:cdecl
type Generic[T any] struct { Fn func(); Tail T }
type Derived[T any] Generic[T]
type Instance Derived[uint64]
//llgo:cdecl
type Anonymous = struct { Fn func(); Tail uint64 }
type FromAnonymous Anonymous
`, nil, true)
index := frontcache.NewTypes(map[*types.Package]string{pkg: pkg.Path()})
index.LayoutIdentity = prog.LayoutIdentity
index.AddPackage(pkg, nil)
exported := frontcache.ExportPackage(pkg, index, token.NewFileSet())
exported.Types.ExportRepresentations(func(obj *types.TypeName) (int, types.Type) {
r := prog.RepresentationOf(obj.Type())
if r.Storage == llssa.NativeRepresentation {
return int(r.NativeABI), r.Origin.Type()
}
return 0, nil
})
data, err := json.Marshal(exported)
if err != nil {
t.Fatal(err)
}
var restored frontcache.PackageExport
if err := json.Unmarshal(data, &restored); err != nil {
t.Fatal(err)
}
importer := &frontcache.TypeImporter{Packages: map[string]*types.Package{pkg.Path(): types.NewPackage(pkg.Path(), pkg.Name())}}
loaded, err := frontcache.ImportPackage(&restored, importer)
if err != nil {
t.Fatal(err)
}
representations, err := importer.ImportRepresentations()
if err != nil {
t.Fatal(err)
}
backend := llssa.NewProgram(&llssa.Target{GOOS: "linux", GOARCH: "amd64"})
defer backend.Dispose()
backend.TypeSizes(types.SizesFor("gc", "amd64"))
for _, name := range []string{"private", "Generic"} {
backend.SetTypeCallConv(pkg.Path()+"."+name, llssa.CDecl)
}
for obj, imported := range representations {
r := llssa.TypeRepresentation{}
if imported.ABI != 0 {
r.Storage, r.NativeABI, r.Origin = llssa.NativeRepresentation, llssa.CallConv(imported.ABI), imported.Origin
}
backend.SetImportedRepresentation(obj, r)
}
layouts, err := importer.ImportLayouts()
if err != nil {
t.Fatal(err)
}
for typ := range layouts {
backend.SetNativeAnonymousLayout(typ)
}
backend.SetTypeCallConv(pkg.Path()+".Anonymous", llssa.CDecl)
for _, name := range []string{"First", "Exported", "ThroughAlias", "Instance", "FromAnonymous"} {
typ := loaded.Scope().Lookup(name).Type()
if backend.RepresentationOf(typ).Storage != llssa.NativeRepresentation {
t.Fatalf("restored %s lost native representation", name)
}
raw := backend.Type(typ, llssa.GoDecl)
if backend.SizeOf(raw) != 16 || backend.OffsetOf(raw, 1) != 8 {
t.Fatalf("restored %s lost native storage", name)
}
}
// Malformed/missing origins must fail before any policy is installed.
for i := range restored.Types.Nodes {
if r := restored.Types.Nodes[i].Representation; r != nil && r.ABI != 0 {
r.Origin = len(restored.Types.Nodes)
if _, err := importer.ImportRepresentations(); err == nil {
t.Fatal("invalid origin reference accepted")
}
break
}
}
}
98 changes: 98 additions & 0 deletions cl/representation_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cl

import (
"strings"
"testing"

llssa "github.com/xgo-dev/llgo/ssa"
)

func TestNativePointerConversionRejectsDifferentStorage(t *testing.T) {
const declarations = `package repr
//llgo:cdecl
type Native struct { Fn func(); Tail uint64 }
type Copy Native
type Plain struct { Fn func(); Tail uint64 }
`
for _, target := range []*llssa.Target{
{GOOS: "linux", GOARCH: "amd64"},
{GOOS: "windows", GOARCH: "386"},
{GOOS: "js", GOARCH: "wasm", LLVMTarget: "wasm32-unknown-emscripten", WasmProfile: "j32"},
} {
t.Run(target.GOOS+"/"+target.GOARCH, func(t *testing.T) {
for _, tc := range []struct {
source string
valid bool
}{
{`func convert(p *Native) *Copy { return (*Copy)(p) }`, true},
{`func convert(p *Native) *Plain { return (*Plain)(p) }`, false},
{`func convert(p *Plain) *Native { return (*Native)(p) }`, false},
} {
pkg, files := buildCallerFrameSSAPackage(t, "repr", declarations+tc.source)
prog := newLLSSAProgForTarget(t, target)
_, _, err := NewPackageEx(prog, nil, nil, pkg, files)
prog.Dispose()
if tc.valid {
if err != nil {
t.Fatal(err)
}
} else if err == nil || !strings.Contains(err.Error(), "caller_frame_compile.go:") || !strings.Contains(err.Error(), "incompatible native storage") {
t.Fatalf("conversion error = %v", err)
}
}
})
}
}

func TestNativeCallbackConversionChecksInheritedABI(t *testing.T) {
const source = `package repr
//llgo:cdecl
type C func() int32
//llgo:stdcall
type S func() int32
type Inherited S
func convert(fn Inherited) C { return C(fn) }
`
for _, arch := range []string{"386", "amd64", "arm64"} {
t.Run(arch, func(t *testing.T) {
pkg, files := buildCallerFrameSSAPackage(t, "repr", source)
prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "windows", GOARCH: arch})
defer prog.Dispose()
_, _, err := NewPackageEx(prog, nil, nil, pkg, files)
if arch == "386" {
if err == nil || !strings.Contains(err.Error(), "incompatible native callback ABI") {
t.Fatalf("inherited stdcall conversion error = %v", err)
}
} else if err != nil {
t.Fatal(err)
}
})
}
}

func TestNativeArrayConversionChecksElementStorage(t *testing.T) {
for _, target := range []*llssa.Target{
{GOOS: "linux", GOARCH: "amd64"},
{GOOS: "js", GOARCH: "wasm", LLVMTarget: "wasm32-unknown-emscripten", WasmProfile: "j32"},
} {
t.Run(target.GOOS+"/"+target.GOARCH, func(t *testing.T) {
pkg, files := buildCallerFrameSSAPackage(t, "repr", `package repr
//llgo:cdecl
type Native [3]uintptr
type Copy Native
func inherited(a Native) Copy { return Copy(a) }
func ordinary(a Native) [3]uintptr { return [3]uintptr(a) }
`)
prog := newLLSSAProgForTarget(t, target)
defer prog.Dispose()
_, _, err := NewPackageEx(prog, nil, nil, pkg, files)
if target.GOARCH == "wasm" {
if err == nil || !strings.Contains(err.Error(), "incompatible native array storage") {
t.Fatalf("array conversion error = %v", err)
}
} else if err != nil {
t.Fatal(err)
}
})
}
}
Loading
Loading