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
2 changes: 1 addition & 1 deletion codegen/src/gen.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

@ModuleInfo { minPklVersion = "0.31.0" }
@ModuleInfo { minPklVersion = "0.32.0" }
module pkl.go.gen

extends "pkl:Command"
Expand Down
21 changes: 21 additions & 0 deletions codegen/src/internal/typegen.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@Unlisted
module pkl.golang.internal.typegen

import "pkl:ref"
import "pkl:reflect"

import "GoMapping.pkl"
Expand Down Expand Up @@ -84,6 +85,8 @@ function generateDeclaredType(
generateSet(type, enclosing, seenMappings)
else if (reflectee == Pair)
generatePair(type, enclosing, seenMappings)
else if (reflectee == ref.Reference)
generateReference(type, enclosing, seenMappings)
else
throw("Cannot generate type \(type.referent.name) as Go.")

Expand Down Expand Up @@ -147,6 +150,18 @@ function generatePair(
typeArguments = type.typeArguments.map((t) -> generateType(t, enclosing, seenMappings))
}

function generateReference(
type: reflect.DeclaredType,
enclosing: reflect.TypeDeclaration,
seenMappings: List<GoMapping>,
): Type = new Type.Declared {
typeName = "Reference"
package = "pkl"
importPath = "github.com/apple/pkl-go/pkl"
// only domain becomes a type argument in go
typeArguments = List(generateType(type.typeArguments.first, enclosing, seenMappings))
}

local function builtInType(typ: String): Type.Declared = new { typeName = typ }

local anyType: Type.Declared = builtInType("any")
Expand Down Expand Up @@ -205,4 +220,10 @@ mappedTypes: Mapping<Class | TypeAlias, Type> = new {
[Bytes] = new Type.Slice {
elem = new Type.Declared { typeName = "byte" }
}
// introduced in Pkl 0.32
[ref.Access] = new Type.Declared {
package = "pkl"
typeName = "ReferenceAccess"
importPath = "github.com/apple/pkl-go/pkl"
}
}
27 changes: 27 additions & 0 deletions pkl/decode_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,33 @@ func (d *decoder) decodeTypeAlias(length int) (*reflect.Value, error) {
return &ret, nil
}

func (d *decoder) decodeReference(typ reflect.Type) (*reflect.Value, error) {
ret := reflect.New(typ).Elem()

domainField := ret.FieldByName("Domain")
domain, err := d.Decode(domainField.Type())
if err != nil {
return nil, err
}
domainField.Set(*domain)

dataField := ret.FieldByName("Data")
data, err := d.Decode(dataField.Type())
if err != nil {
return nil, err
}
dataField.Set(*data)

pathField := ret.FieldByName("Path")
path, err := d.decodeSliceImpl(pathField.Type())
if err != nil {
return nil, err
}
pathField.Set(*path)

return &ret, nil
}

func parseStructOpts(field *reflect.StructField) structFieldOpts {
ret := structFieldOpts{propertyName: field.Name}
tagValue, exists := field.Tag.Lookup(StructTag)
Expand Down
9 changes: 9 additions & 0 deletions pkl/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const (
codeTypeAlias = 0x0D
codeFunction = 0x0E
codeBytes = 0x0F
codeReference = 0x20

codeObjectMemberProperty = 0x10
codeObjectMemberEntry = 0x11
Expand Down Expand Up @@ -242,6 +243,12 @@ func (d *decoder) decodePklObject(typ reflect.Type, requireStruct bool) (res *re
res, err = d.decodeClass(length)
case code == codeTypeAlias:
res, err = d.decodeTypeAlias(length)
case code == codeReference:
if typ == emptyInterfaceType {
res, err = d.decodeReference(reflect.TypeFor[Reference[any]]())
} else {
res, err = d.decodeReference(typ)
}
default:
if requireStruct {
return nil, fmt.Errorf("code %#02x cannot be decoded into a struct", code)
Expand Down Expand Up @@ -291,6 +298,8 @@ func getDecodedLength(code, length int) int {
}
// before pkl 0.30 only the type code is present
return 0
case codeReference:
return 3 // domain, data, path
default:
return 1
}
Expand Down
10 changes: 10 additions & 0 deletions pkl/test_fixtures/gen/reference/A.pkl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions pkl/test_fixtures/gen/reference/D.pkl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pkl/test_fixtures/gen/reference/MapKey.pkl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions pkl/test_fixtures/gen/reference/Reference.pkl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions pkl/test_fixtures/gen/reference/init.pkl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkl/test_fixtures/msgpack/reference.pkl.msgpack
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
”©referenceÙ&pklgo:/pkl/test_fixtures/reference.pkl•“¤res0” ”«reference#DÙ&pklgo:/pkl/test_fixtures/reference.pkl¢hi“¤res1” ”«reference#DÙ&pklgo:/pkl/test_fixtures/reference.pkl¢hi‘”®pkl.ref#Access§pkl:ref”“ªisPropertyÓ«isSubscript“¨property£foo“£keyÀ“¤res2” ”«reference#DÙ&pklgo:/pkl/test_fixtures/reference.pkl¢hi’”®pkl.ref#Access§pkl:ref”“ªisPropertyÓ«isSubscript“¨property£bar“£keyÀ”®pkl.ref#Access§pkl:ref”“ªisProperty“«isSubscriptÓ¨propertyÀ“£key¢hi“¤res3” ”«reference#DÙ&pklgo:/pkl/test_fixtures/reference.pkl¢hi’”®pkl.ref#Access§pkl:ref”“ªisPropertyÓ«isSubscript“¨property£bar“£keyÀ”®pkl.ref#Access§pkl:ref”“ªisProperty“«isSubscriptÓ¨propertyÀ“£keyÀ“¤res4” ”«reference#DÙ&pklgo:/pkl/test_fixtures/reference.pkl¢hi’”®pkl.ref#Access§pkl:ref”“ªisPropertyÓ«isSubscript“¨property£baz“£keyÀ”®pkl.ref#Access§pkl:ref”“ªisProperty“«isSubscriptÓ¨propertyÀ“£key”°reference#MapKeyÙ&pklgo:/pkl/test_fixtures/reference.pkl‘“£key£foo
25 changes: 25 additions & 0 deletions pkl/test_fixtures/reference.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@go.Package { name = "github.com/apple/pkl-go/pkl/test_fixtures/gen/reference" }
module reference

import "pkl:ref"

import ".../codegen/src/go.pkl"

class D extends ref.Domain
local d: D = new {}

class A {
foo: Int
bar: Mapping<String?, Int>
baz: Mapping<MapKey, String>
}

class MapKey {
key: String
}

res0: ref.Reference<D, A> = ref.Reference(d, A, "hi")
res1: ref.Reference<D, Int> = res0.foo
res2: ref.Reference<D, Int> = res0.bar["hi"]
res3: ref.Reference<D, Int> = res0.bar[null]
res4: ref.Reference<D, String> = res0.baz[new MapKey { key = "foo" }]
52 changes: 52 additions & 0 deletions pkl/unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"testing"

"github.com/apple/pkl-go/pkl"
"github.com/apple/pkl-go/pkl/test_fixtures/gen/reference"
unknowntype "github.com/apple/pkl-go/pkl/test_fixtures/gen/unknown_type"

any2 "github.com/apple/pkl-go/pkl/test_fixtures/gen/any"
Expand Down Expand Up @@ -94,6 +95,9 @@ var typesInput []byte
//go:embed test_fixtures/manual/types_pre_0.30.pkl.msgpack
var typesPre030Input []byte

//go:embed test_fixtures/msgpack/reference.pkl.msgpack
var referenceInput []byte

func TestUnmarshall_Primitives(t *testing.T) {
var res primitives.Primitives
expected := primitives.Primitives{
Expand Down Expand Up @@ -488,3 +492,51 @@ func TestUnmarshal_Types_Pre_030(t *testing.T) {

assert.Equal(t, types.Types{}, res)
}

func TestUnmarshal_Reference(t *testing.T) {
var res reference.Reference
assert.NoError(t, pkl.Unmarshal(referenceInput, &res))

foo := "foo"
bar := "bar"
baz := "baz"

assert.Equal(t, reference.Reference{
Res0: pkl.Reference[reference.D]{
Domain: reference.DImpl{},
Data: "hi",
Path: []pkl.ReferenceAccess{},
},
Res1: pkl.Reference[reference.D]{
Domain: reference.DImpl{},
Data: "hi",
Path: []pkl.ReferenceAccess{
{Property: &foo},
},
},
Res2: pkl.Reference[reference.D]{
Domain: reference.DImpl{},
Data: "hi",
Path: []pkl.ReferenceAccess{
{Property: &bar},
{Key: "hi"},
},
},
Res3: pkl.Reference[reference.D]{
Domain: reference.DImpl{},
Data: "hi",
Path: []pkl.ReferenceAccess{
{Property: &bar},
{},
},
},
Res4: pkl.Reference[reference.D]{
Domain: reference.DImpl{},
Data: "hi",
Path: []pkl.ReferenceAccess{
{Property: &baz},
{Key: reference.MapKey{Key: "foo"}},
},
},
}, res)
}
35 changes: 34 additions & 1 deletion pkl/values.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2026 Apple Inc. and the Pkl project authors. 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.
Expand All @@ -23,6 +23,10 @@ import (
"time"
)

func init() {
RegisterMappingFor[ReferenceAccess]("pkl.ref#Access")
}

// Object is the Go representation of `pkl.base#Object`.
// It is a container for properties, entries, and elements.
type Object struct {
Expand Down Expand Up @@ -303,3 +307,32 @@ func ToDataSizeUnit(str string) (DataSizeUnit, error) {
return Bytes, fmt.Errorf("unrecognized DataSize unit: `%s`", str)
}
}

// Reference provides a representation for type-checked references to values that may not be set at runtime.
type Reference[D any] struct {
// Reference domain.
Domain D

// Reference data.
Data any

// Reference access path.
Path []ReferenceAccess
}

// ReferenceAccess is an element of a [Reference]'s access path, representing property or subscript access.
type ReferenceAccess struct {
// If this is a property access, this will be a string containing the name of the accessed property.
// If this is a subscript access, this will be `nil`.
Property *string `pkl:"property"`

// If this access is a subscript access, this will be the value of the key (which may be `nil`) and [Property] will be `nil`.
// If this is a property access, this will be `nil`.
Key any `pkl:"key"`
}

// IsProperty indicates if this represents a property access.
func (a ReferenceAccess) IsProperty() bool { return a.Property != nil }

// IsSubscript indicates if this represents a subscript access.
func (a ReferenceAccess) IsSubscript() bool { return a.Property == nil }
Loading