Skip to content
Draft
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
19 changes: 17 additions & 2 deletions doc/Function-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func Fatal(message string) {

The properties apply to definitions and imported declarations, including methods, generic instances and declarations connected by linkname. They are preserved by ABI conversion and cached builds. The runtime uses the same source directives as ordinary packages.

## Parameter and single-result attributes
## Parameter and result attributes

Use `//llgo:param(name|index)`, `//llgo:receiver`, or `//llgo:result(name|index)` to select a whole Go value. Indices start at zero; the receiver is separate. A function with exactly one result also permits `//llgo:result`. Both comment spacings accepted for function attributes are accepted here.

Expand All @@ -43,5 +43,20 @@ func Checked(p *int) *int {

Source selectors are resolved before exported signatures can lose parameter names. Imports, linkname declarations and generic instances preserve the guarantees; concrete generic types are checked at instantiation. ABI conversion preserves attributes on the corresponding scalar values when other arguments are packed or passed indirectly.

Result attributes currently require exactly one result. Multiple-result guarantees, `sameas`, `access` and `noalias` are subsequent steps of #2590.
### Result relations and multiple results

`sameas(name)` states that an integer or pointer result equals the entry value of the ordinary parameter named `name`. Integer types must match; pointer conversions must preserve the pointer. Like other result guarantees, it applies after normal return, including changes made by deferred functions.

```go
//llgo:result(out) nonnull sameas(p)
//llgo:result(count) range(0,64)
func Make(p *int, n uint32) (out *int, count uint32) {
if p == nil { panic("nil pointer") }
return p, n & 63
}
```

With multiple Go results, select each whole result by name or zero-based index. The selector remains stable if the ABI packs or splits the results or returns them through caller-provided storage. The call and its possible panic remain; only the guaranteed results can be simplified. `sameas` uses the evaluated input value, even if memory supplying that input changes during the call.

`access` and `noalias` are the final step of #2590.

6 changes: 6 additions & 0 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,9 @@ func buildMainLink(ctx *context, pkg *packages.Package, preparation *mainLinkPre
ctx.stripDarwinLTOLocals = false
entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, &preparation.gen)
cExports := preparation.gen.cExports
if err := ctx.prog.MaterializeValueAttributes(entryPkg.LPkg.Module()); err != nil {
return nil, err
}
if len(cExports) != 0 {
llabi.LowerLargeAggregates(ctx.prog.TargetData(), entryPkg.LPkg.Module())
ctx.cTransformer.TransformModule(entryPkg.LPkg.Path(), entryPkg.LPkg.Module())
Expand Down Expand Up @@ -2853,6 +2856,9 @@ func compilePackageModule(ctx *context, aPkg *aPackage, externs []string, verbos
ret := aPkg.LPkg

ctx.cTransformer.SetSkipFuncs(cabiSkipFuncsForPlan9Asm(ctx, pkgPath, ret.Module()))
if err := ctx.prog.MaterializeValueAttributes(ret.Module()); err != nil {
return err
}
llabi.LowerLargeAggregates(ctx.prog.TargetData(), ret.Module())
ctx.cTransformer.TransformModule(ret.Path(), ret.Module())
ctx.cTransformer.SetSkipFuncs(nil)
Expand Down
131 changes: 131 additions & 0 deletions internal/cabi/result_attributes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package cabi

import (
"go/ast"
"go/parser"
"go/token"
"go/types"
"strings"
"testing"

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

func contractTestDeclaration(t *testing.T, prog llssa.Program, pkg llssa.Package, source, name string) llssa.Function {
t.Helper()
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "contracts.go", "package contracts\n"+source, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
info := &types.Info{Defs: make(map[*ast.Ident]types.Object)}
if _, err := (&types.Config{}).Check("contracts", fset, []*ast.File{file}, info); err != nil {
t.Fatal(err)
}
decl := file.Decls[len(file.Decls)-1].(*ast.FuncDecl)
attrs, err := funcattrs.Parse(fset, decl)
if err != nil {
t.Fatal(err)
}
if err := prog.SetValueAttributes(name, attrs); err != nil {
t.Fatal(err)
}
return pkg.NewFunc(name, info.Defs[decl.Name].Type().(*types.Signature), llssa.InGo)
}

func TestValueContractsSurviveByvalSretAndPackedABI(t *testing.T) {
llvm.InitializeAllTargets()
llvm.InitializeAllTargetInfos()
llvm.InitializeAllTargetMCs()
llvm.InitializeAllAsmPrinters()
for _, target := range []llssa.Target{
{GOOS: "linux", GOARCH: "amd64"},
{GOOS: "darwin", GOARCH: "arm64"},
{GOOS: "windows", GOARCH: "386"},
{GOOS: "wasip1", GOARCH: "wasm32"},
} {
t.Run(target.GOARCH, func(t *testing.T) {
prog := llssa.NewProgram(&target)
defer prog.Dispose()
if target.GOARCH == "386" {
prog.TypeSizes(types.SizesFor("gc", "386"))
}
pkg := prog.NewPackage("contracts", "contracts")
callee := contractTestDeclaration(t, prog, pkg, `
type Pair struct { P *int; N int64; Extra [24]byte }
//llgo:param(p) nonnull
//llgo:result(q) nonnull sameas(p)
//llgo:result(n) range(0,7)
func F(input Pair, p *int) (q *int, n int64, output Pair)
`, "contracts.F")
calleeType := callee.Type.RawType().(*types.Signature)
callerSig := types.NewSignatureType(nil, nil, nil, calleeType.Params(),
types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.Bool])), false)
caller := pkg.NewFunc("contracts.Caller", callerSig, llssa.InGo)
b := caller.MakeBody(1)
r := b.Call(callee.Expr, caller.Param(0), caller.Param(1))
pointer := b.Extract(r, 0)
badPointer := b.BinOp(token.EQL, pointer, prog.Nil(pointer.Type))
n := b.Extract(r, 1)
negative := b.BinOp(token.LSS, n, prog.IntVal(0, n.Type))
large := b.BinOp(token.GEQ, n, prog.IntVal(7, n.Type))
b.Return(b.BinOp(token.OR, badPointer, b.BinOp(token.OR, negative, large)))
b.EndBuild()

packed := contractTestDeclaration(t, prog, pkg, `
type Packed struct { N int8; Other [7]byte }
//llgo:param(n) range(-3,4)
func PackedInput(p Packed, n int8) bool
`, "contracts.PackedInput")
pbody := packed.MakeBody(1)
value := packed.Param(1)
pbody.Return(pbody.BinOp(token.GTR, value, prog.IntVal(3, value.Type)))
pbody.EndBuild()

mod := pkg.Module()
logicalType := mod.NamedFunction("contracts.F").GlobalValueType()
if err := prog.MaterializeValueAttributes(mod); err != nil {
t.Fatal(err)
}
NewTransformer(prog, mod.Target(), "", false).TransformModule("contracts", mod)
physical := mod.NamedFunction("contracts.F")

if physical.GlobalValueType() == logicalType {
t.Fatal("test did not exercise aggregate ABI rewriting")
}
if physical.GetEnumAttributeAtIndex(1, llvm.AttributeKindID("sret")).IsNil() {
t.Fatalf("large source results were not transported via sret:\n%s", physical.String())
}
if target.GOARCH == "amd64" && physical.GetEnumAttributeAtIndex(2, llvm.AttributeKindID("byval")).IsNil() {
t.Fatalf("amd64 input did not exercise native byval:\n%s", physical.String())
}
if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil {
t.Fatalf("invalid transformed contracts: %v\n%s", err, mod.String())
}
options := llvm.NewPassBuilderOptions()
defer options.Dispose()
if err := mod.RunPasses("default<O2>", prog.TargetMachine(), options); err != nil {
t.Fatal(err)
}
for _, name := range []string{"contracts.Caller", "contracts.PackedInput"} {
body := mod.NamedFunction(name).String()
if !strings.Contains(body, "ret i1 false") {
t.Fatalf("%s did not consume reconstructed value facts:\n%s", name, body)
}
}
if !strings.Contains(mod.NamedFunction("contracts.Caller").String(), "@contracts.F(") {
t.Fatal("result facts removed the unknown external invocation")
}
if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil {
t.Fatal(err)
}
assembly, err := prog.TargetMachine().EmitToMemoryBuffer(mod, llvm.AssemblyFile)
if err != nil {
t.Fatalf("cannot emit transformed contract assembly: %v", err)
}
assembly.Dispose()
})
}
}
8 changes: 7 additions & 1 deletion internal/funcattrs/llvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ func Apply(ctx llvm.Context, fn llvm.Value, sig *types.Signature, attrs []Attrib
return err
}
for _, attr := range attrs {
if attr.Name == "sameas" || (attr.Target.Scope == Result && sig.Results().Len() != 1) {
continue
}
index := 1 + environment
if attr.Target.Scope == Result {
index = 0
Expand Down Expand Up @@ -72,7 +75,10 @@ func Apply(ctx llvm.Context, fn llvm.Value, sig *types.Signature, attrs []Attrib

// CopyValueAttributes is used only when ABI conversion preserves the whole value.
func CopyValueAttributes(from, to llvm.Value, old, new int) {
for _, name := range []string{"nonnull", "range"} {
for _, name := range []string{"nonnull", "range", "returned"} {
if name == "returned" && from.GlobalValueType().ReturnType() != to.GlobalValueType().ReturnType() {
continue
}
if attr := from.GetEnumAttributeAtIndex(old, llvm.AttributeKindID(name)); !attr.IsNil() {
to.AddAttributeAtIndex(new, attr)
}
Expand Down
46 changes: 42 additions & 4 deletions internal/funcattrs/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type Attribute struct {
Args string // Canonical spelling; backends consume the typed operands below.
Position token.Position
Range *RangeBounds
From *Target // Ordinary parameter entry value used by sameas.
}

func (a Attribute) Error(format string, args ...any) error {
Expand All @@ -76,7 +77,7 @@ func IsSourceDirective(d directive.Directive) bool {
name = name[:i]
}
switch name {
case "param", "result", "receiver", "nonnull", "range", "nonnegative":
case "param", "result", "receiver", "nonnull", "range", "nonnegative", "sameas":
return true
}
return false
Expand All @@ -101,9 +102,7 @@ func Parse(fset *token.FileSet, decl *ast.FuncDecl) ([]Attribute, error) {
if err != nil {
return nil, base.Error("%v", err)
}
if base.Target.Scope == Result && fieldCount(decl.Type.Results) != 1 {
return nil, base.Error("result attributes currently require exactly one source result")
}

words = words[1:]
if len(words) == 0 {
return nil, base.Error("expected an attribute after selector")
Expand All @@ -118,6 +117,16 @@ func Parse(fset *token.FileSet, decl *ast.FuncDecl) ([]Attribute, error) {
return nil, a.Error("%v", err)
}

if a.Name == "sameas" {
if !token.IsIdentifier(a.Args) || a.Args == "_" {
return nil, a.Error("sameas expects a parameter name")
}
index, err := selectIndex(decl.Type.Params, a.Args)
if err != nil {
return nil, a.Error("sameas: %v", err)
}
a.From = &Target{Scope: Parameter, Index: index}
}
a, err = normalize(a)
if err != nil {
return nil, err
Expand Down Expand Up @@ -287,6 +296,8 @@ func normalize(a Attribute) (Attribute, error) {
valid = input || result
case "range":
valid, takesArgs = input || result, true
case "sameas":
valid, takesArgs = result, true
default:
return a, a.Error("unsupported attribute %q", a.Name)
}
Expand All @@ -306,6 +317,12 @@ func normalize(a Attribute) (Attribute, error) {
a.Args = lo.String() + "," + hi.String()
}

case "sameas":
if a.From == nil || a.From.Scope != Parameter {
err = fmt.Errorf("sameas requires a parameter name")
} else {
a.Args = a.From.String()
}
}
if err != nil {
return a, a.Error("%v", err)
Expand All @@ -325,6 +342,10 @@ func Merge(attrs ...[]Attribute) ([]Attribute, error) {
return nil, err
}

if a.From != nil {
from := *a.From
a.From = &from
}
duplicate := false
for _, prev := range out {
if !prev.Target.Equal(a.Target) {
Expand Down Expand Up @@ -437,6 +458,23 @@ func Validate(attrs []Attribute, sig *types.Signature, intBits int, deferTypePar
return a.Error("%s requires a pointer, got %s", a.Name, t)
}

case "sameas":
if !pointer(t) && !integer(t) {
return a.Error("sameas requires an integer or pointer result, got %s", t)
}
if a.From == nil {
return a.Error("sameas requires an input selector")
}
from, err := ResolveTarget(sig, *a.From)
if err != nil {
return a.Error("sameas input: %v", err)
}
if unresolved(from) && deferTypeParams {
continue
}
if !(pointer(t) && pointer(from)) && !types.Identical(types.Unalias(t), types.Unalias(from)) {
return a.Error("sameas requires compatible pointer types or identical integer source types, got %s and %s", from, t)
}
case "range", "nonnegative":
if _, _, _, err := IntegerRange(a, t, intBits); err != nil {
return err
Expand Down
25 changes: 24 additions & 1 deletion internal/funcattrs/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ func TestSourceValueDiagnostics(t *testing.T) {
{"receiver nonnull", "func F() {}", "requires a method"},
{"param(0) nonnull", "func F(p string) {}", "requires a pointer"},
{"result", "func F() {}", "exactly one source result"},
{"result(0) nonnull", "func F() (*int,*int) { return nil,nil }", "exactly one source result"},
{"result", "func F() *int { return nil }", "expected an attribute"},
{"param(0) nonnull)", "func F(p *int) {}", "unbalanced"},
{"param(0) nonnull(", "func F(p *int) {}", "unbalanced"},
Expand Down Expand Up @@ -158,3 +157,27 @@ func TestGenericValueAttributeValidation(t *testing.T) {
}
}
}

func TestSourceResultRelations(t *testing.T) {
for _, tc := range []struct{ selector, signature, want string }{
{"result sameas(p)", "func F(p *int) *int { return p }", ""},
{"result(out) sameas(p) nonnull", "func F(p *int) (out *int,n int) { return p,0 }", ""},
{"result sameas(p)", "func F(p int32) uint32 { return 0 }", "identical integer"},
{"result sameas(p)", "func F(p bool) bool { return p }", "integer or pointer"},
{"result sameas(0)", "func F(p int) int { return p }", "parameter name"},
{"result sameas(missing)", "func F(p int) int { return p }", "unknown source value"},
{"param(p) sameas(p)", "func F(p int) int { return p }", "not supported on parameter"},
} {
attrs, sig, err := parseTest(t, "//llgo:"+tc.selector+"\n"+tc.signature)
if err == nil {
err = Validate(attrs, sig, 64, false)
}
if tc.want == "" {
if err != nil {
t.Fatal(err)
}
} else if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("%s: %v", tc.selector, err)
}
}
}
Loading
Loading