From 8a43e51e6bf6a6efb447d062ab1dc1f48f1a49d5 Mon Sep 17 00:00:00 2001 From: Jen Basch Date: Tue, 23 Jun 2026 06:26:06 -0700 Subject: [PATCH] SPICE-0020: Deferred, type-safe references (#1354) --- .../ROOT/partials/component-attributes.adoc | 3 + .../pages/binary-encoding.adoc | 10 +- .../language-reference/pages/index.adoc | 613 +++++++++++++----- .../src/main/java/org/pkl/core/PClass.java | 16 +- .../main/java/org/pkl/core/PClassInfo.java | 7 +- .../src/main/java/org/pkl/core/PType.java | 82 ++- .../java/org/pkl/core/PropertiesRenderer.java | 7 + .../src/main/java/org/pkl/core/Reference.java | 101 +++ .../src/main/java/org/pkl/core/TypeAlias.java | 9 +- .../java/org/pkl/core/ValueConverter.java | 4 +- .../main/java/org/pkl/core/ValueVisitor.java | 4 + .../ast/expression/binary/SubscriptNode.java | 14 + .../expression/member/ReadPropertyNode.java | 12 + .../pkl/core/ast/internal/ToStringNode.java | 20 +- .../java/org/pkl/core/ast/type/TypeNode.java | 121 +++- .../pkl/core/ast/type/UnresolvedTypeNode.java | 9 +- .../ast/type/VmTypeMismatchException.java | 41 ++ .../java/org/pkl/core/runtime/Identifier.java | 4 + .../org/pkl/core/runtime/ModuleCache.java | 2 + .../java/org/pkl/core/runtime/RefModule.java | 53 ++ .../java/org/pkl/core/runtime/VmClass.java | 96 +-- .../org/pkl/core/runtime/VmObjectBuilder.java | 7 +- .../pkl/core/runtime/VmPklBinaryEncoder.java | 16 + .../org/pkl/core/runtime/VmReference.java | 473 ++++++++++++++ .../org/pkl/core/runtime/VmTypeAlias.java | 14 +- .../java/org/pkl/core/runtime/VmTypes.java | 5 +- .../pkl/core/runtime/VmValueConverter.java | 2 + .../org/pkl/core/runtime/VmValueRenderer.java | 24 + .../org/pkl/core/runtime/VmValueVisitor.java | 4 +- .../org/pkl/core/stdlib/AbstractRenderer.java | 6 + .../org/pkl/core/stdlib/PklConverter.java | 7 + .../core/stdlib/protobuf/RendererNodes.java | 7 + .../org/pkl/core/stdlib/ref/RefNodes.java | 33 + .../pkl/core/stdlib/ref/ReferenceNodes.java | 46 ++ .../org/pkl/core/stdlib/ref/package-info.java | 4 + .../core/util/pklbinary/PklBinaryCode.java | 2 + .../org/pkl/core/errorMessages.properties | 6 + .../input-helper/errors/ReferencedModule.pkl | 1 + .../ReferencedModuleWithOutputOverride.pkl | 7 + .../input/api/reference.pkl | 134 ++++ .../input/errors/reference1.pkl | 9 + .../input/errors/reference10.pkl | 9 + .../input/errors/reference11.pkl | 12 + .../input/errors/reference12.pkl | 12 + .../input/errors/reference13.pkl | 8 + .../input/errors/reference14.pkl | 8 + .../input/errors/reference15.pkl | 8 + .../input/errors/reference16.pkl | 10 + .../input/errors/reference17.pkl | 10 + .../input/errors/reference18.pkl | 12 + .../input/errors/reference19.pkl | 12 + .../input/errors/reference2.pkl | 9 + .../input/errors/reference20.pkl | 15 + .../input/errors/reference3.pkl | 9 + .../input/errors/reference4.pkl | 12 + .../input/errors/reference5.pkl | 14 + .../input/errors/reference6.pkl | 13 + .../input/errors/reference7.pkl | 9 + .../input/errors/reference8.pkl | 9 + .../input/errors/reference9.pkl | 9 + .../output/api/reference.pcf | 60 ++ .../output/errors/cannotFindStdLibModule.err | 1 + .../output/errors/reference1.err | 15 + .../output/errors/reference10.err | 14 + .../output/errors/reference11.err | 14 + .../output/errors/reference12.err | 14 + .../output/errors/reference13.err | 14 + .../output/errors/reference14.err | 14 + .../output/errors/reference15.err | 14 + .../output/errors/reference16.err | 14 + .../output/errors/reference17.err | 14 + .../output/errors/reference18.err | 14 + .../output/errors/reference19.err | 15 + .../output/errors/reference2.err | 15 + .../output/errors/reference20.err | 22 + .../output/errors/reference3.err | 15 + .../output/errors/reference4.err | 15 + .../output/errors/reference5.err | 15 + .../output/errors/reference6.err | 14 + .../output/errors/reference7.err | 16 + .../output/errors/reference8.err | 14 + .../output/errors/reference9.err | 14 + stdlib/ref.pkl | 207 ++++++ 83 files changed, 2573 insertions(+), 226 deletions(-) create mode 100644 pkl-core/src/main/java/org/pkl/core/Reference.java create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/RefModule.java create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/VmReference.java create mode 100644 pkl-core/src/main/java/org/pkl/core/stdlib/ref/RefNodes.java create mode 100644 pkl-core/src/main/java/org/pkl/core/stdlib/ref/ReferenceNodes.java create mode 100644 pkl-core/src/main/java/org/pkl/core/stdlib/ref/package-info.java create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModule.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModuleWithOutputOverride.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference10.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference11.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference12.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference8.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference9.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/reference.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference10.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference11.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference12.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference20.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference8.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference9.err create mode 100644 stdlib/ref.pkl diff --git a/docs/modules/ROOT/partials/component-attributes.adoc b/docs/modules/ROOT/partials/component-attributes.adoc index c7caac5c8..d04404fc2 100644 --- a/docs/modules/ROOT/partials/component-attributes.adoc +++ b/docs/modules/ROOT/partials/component-attributes.adoc @@ -69,6 +69,7 @@ endif::[] :uri-stdlib-baseModule: {uri-pkl-stdlib-docs}/base :uri-stdlib-CommandModule: {uri-pkl-stdlib-docs}/Command +:uri-stdlib-refModule: {uri-pkl-stdlib-docs}/ref :uri-stdlib-analyzeModule: {uri-pkl-stdlib-docs}/analyze :uri-stdlib-jsonnetModule: {uri-pkl-stdlib-docs}/jsonnet :uri-stdlib-reflectModule: {uri-pkl-stdlib-docs}/reflect @@ -160,6 +161,8 @@ endif::[] :uri-stdlib-Command-CountedFlag: {uri-stdlib-CommandModule}/CountedFlag :uri-stdlib-Command-Argument: {uri-stdlib-CommandModule}/Argument :uri-stdlib-Command-Import: {uri-stdlib-CommandModule}/Import +:uri-stdlib-ref-Reference: {uri-stdlib-baseModule}/Reference +:uri-stdlib-ref-Access: {uri-stdlib-baseModule}/Access :uri-messagepack: https://msgpack.org/index.html :uri-messagepack-spec: https://github.com/msgpack/msgpack/blob/master/spec.md diff --git a/docs/modules/bindings-specification/pages/binary-encoding.adoc b/docs/modules/bindings-specification/pages/binary-encoding.adoc index 637d2530f..0551b0064 100644 --- a/docs/modules/bindings-specification/pages/binary-encoding.adoc +++ b/docs/modules/bindings-specification/pages/binary-encoding.adoc @@ -187,7 +187,15 @@ The array's length is the number of slots that are filled. For example, xref:{ur | | | -|=== + +|link:{uri-stdlib-ref-Reference}[Reference] +|`0x20` +|`` (Typed) +|Domain +|`` +|Data +|link:{uri-messagepack-array}[array] +|Array of link:{uri-stdlib-ref-Access}[`pkl.ref#Access`] values [[type-name-encoding]] [NOTE] diff --git a/docs/modules/language-reference/pages/index.adoc b/docs/modules/language-reference/pages/index.adoc index 8b4f25517..c844c04a6 100644 --- a/docs/modules/language-reference/pages/index.adoc +++ b/docs/modules/language-reference/pages/index.adoc @@ -1,4 +1,5 @@ = Language Reference + include::ROOT:partial$component-attributes.adoc[] :uri-common-mark: https://commonmark.org/ :uri-newspeak: https://newspeaklanguage.org @@ -105,6 +106,7 @@ num2 = 0x012AFF // <1> num3 = 0b00010111 // <2> num4 = 0o755 // <3> ---- + <1> decimal: 76543 <2> decimal: 23 <3> decimal: 493 @@ -119,6 +121,7 @@ num2 = 0x0134_64DE // <2> num3 = 0b0001_0111 // <3> num4 = 0o0134_6475 // <4> ---- + <1> Equivalent to `1000000` <2> Equivalent to `0x013464DE` <3> Equivalent to `0b00010111` @@ -149,6 +152,7 @@ num5 = 5 ~/ 2 // <5> num6 = 5 % 2 // <6> num7 = 5 ** 2 // <7> ---- + <1> addition (result: `7`) <2> subtraction (result: `3`) <3> multiplication (result: `10`) @@ -183,6 +187,7 @@ num2 = 1.23 num3 = 1.23e2 // <1> num4 = 1.23e-2 // <2> ---- + <1> result: 1.23 * 10^2^ <2> result: 1.23 * 10^-2^ @@ -203,7 +208,8 @@ Floats support the same comparison and arithmetic operators as integers. Float literals with a fractional part of zero can be safely replaced with integer literals. For example, it is safe to write `1.3 * 42` instead of `1.3 * 42.0`. -Floats can also include the same underscore separator as integers. For example, `1_000.4_400` is a float whose value is equivalent to `1000.4400`. +Floats can also include the same underscore separator as integers. +For example, `1_000.4_400` is a float whose value is equivalent to `1000.4400`. TIP: As integers are more convenient to use than floats with a fractional part of zero, we recommend requiring `x: Number` instead of `x: Float` in type annotations. @@ -237,6 +243,7 @@ res3 = !false // <3> res4 = true.xor(false) // <4> res5 = true.implies(false) // <5> ---- + <1> logical conjunction (result: `false`) <2> logical disjunction (result: `true`) <3> logical negation (result: `true`) @@ -255,8 +262,7 @@ String literals are enclosed in double quotes: "Hello, World!" ---- -TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences and stricter rules for line indentation in multiline strings.], -String literals have the same syntax and semantics as in Swift 5. Learn one of them, know both of them! +TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences and stricter rules for line indentation in multiline strings.], String literals have the same syntax and semantics as in Swift 5. Learn one of them, know both of them! Inside a string literal, the following character escape sequences have special meaning: @@ -272,6 +278,7 @@ Unicode escape sequences have the form `\u{}`, where `` is ---- "\u{26} \u{E9} \u{1F600}" // <1> ---- + <1> result: `"& Γ© πŸ˜€"` To concatenate strings, use the `+` (plus) operator, as in `"abc" + "def" + "ghi"`. @@ -285,6 +292,7 @@ To embed the result of expression `` in a string, use `\()`: name = "Dodo" greeting = "Hi, \(name)!" // <1> ---- + <1> result: `"Hi, Dodo!"` Before a result is inserted, it is converted to a string: @@ -294,6 +302,7 @@ Before a result is inserted, it is converted to a string: x = 42 str = "\(x + 2) plus \(x * 2) is \(0x80)" // <1> ---- + <1> result: `"44 plus 84 is 128"` === Multiline Strings @@ -333,8 +342,8 @@ the species will be remembered. """ ---- -Each content line must begin with the same whitespace characters as the line containing the closing delimiter, -which is not included in the string. Any further leading whitespace characters are preserved. +Each content line must begin with the same whitespace characters as the line containing the closing delimiter, which is not included in the string. +Any further leading whitespace characters are preserved. In other words, line indentation is controlled by indenting lines relative to the closing delimiter. In the following string, lines have no leading whitespace: @@ -424,6 +433,7 @@ reversedStr = "dodo".reverse() // <2> hasAx = "dodo".contains("alive") // <3> trimmed = " dodo ".trim() // <4> ---- + <1> result: `4` <2> result: `"odod"` <3> result: `false` @@ -477,6 +487,7 @@ res7 = 5.min ~/ 3.min // <7> res8 = 5.min % 3 // <8> res9 = 5.min ** 3 // <9> ---- + <1> result: `5.05.min` <2> result: `4.95.min` <3> result: `15.min` @@ -496,6 +507,7 @@ xMinutes = x.min // <1> y = 3 xySeconds = (x + y).s // <2> ---- + <1> result: `5.min` <2> result: `8.s` @@ -558,6 +570,7 @@ res7 = 5.mb ~/ 3.mb // <7> res8 = 5.mb % 3 // <8> res9 = 5.mb ** 3 // <9> ---- + <1> result: `5.003072.mb` <2> result: `4.996928.mb` <3> result: `15.mb` @@ -577,6 +590,7 @@ xMegabytes = x.mb // <1> y = 3 xyKibibytes = (x + y).kib // <2> ---- + <1> result: `5.mb` <2> result: `8.kib` @@ -588,8 +602,7 @@ An object is an ordered collection of _values_ indexed by _name_. An object's key-value pairs are called its _properties_. Property values are lazily evaluated on the first read. -Because Pkl's objects differ in important ways from objects in general-purpose programming languages, -and because they are the backbone of most data models, understanding objects is the key to understanding Pkl. +Because Pkl's objects differ in important ways from objects in general-purpose programming languages, and because they are the backbone of most data models, understanding objects is the key to understanding Pkl. [[defining-objects]] === Defining Objects @@ -603,8 +616,9 @@ dodo { // <1> extinct = true // <3> } // <4> ---- + <1> Defines a module property named `dodo`. - The open curly brace (`{`) indicates that the value of this property is an object. +The open curly brace (`{`) indicates that the value of this property is an object. <2> Defines an object property named `name` with string value `"Dodo"`. <3> Defines an object property named `extinct` with boolean value `true`. <4> The closing curly brace indicates the end of the object definition. @@ -628,14 +642,16 @@ dodo { } } ---- + <1> Defines an object property named `taxonomy`. - The open curly brace indicates that its value is another object. +The open curly brace indicates that its value is another object. <2> The word `class` is a keyword of Pkl, and needs to be wrapped in backticks (```) to be used as a property. As you probably guessed, the nested property `class` can be accessed with `dodo.taxonomy.class`. Like all values, objects are _immutable_, which is just a fancy (and short!) way to say that their properties never change. -So what happens when Dodo moves to a different street? Do we have to construct a new object from scratch? +So what happens when Dodo moves to a different street? +Do we have to construct a new object from scratch? [[amending-objects]] === Amending Objects @@ -653,19 +669,20 @@ tortoise = (dodo) { // <1> } } ---- + <1> Defines a module property named `tortoise`. - Its value is an object that _amends_ `dodo`. - Note that the amended object must be enclosed in parentheses. +Its value is an object that _amends_ `dodo`. +Note that the amended object must be enclosed in parentheses. <2> Object property `tortoise.taxonomy` _amends_ `dodo.taxonomy`. <3> Object property `tortoise.taxonomy.class` _overrides_ `dodo.taxonomy.class`. -As you can see, it is easy to construct a new object that overrides selected properties of an existing object, -even if, as in our example, the overridden property is nested inside another object. +As you can see, it is easy to construct a new object that overrides selected properties of an existing object, even if, as in our example, the overridden property is nested inside another object. NOTE: If this way of constructing new objects from existing objects reminds you of prototypical inheritance, you are spot-on: Pkl objects use prototypical inheritance as known from languages such as JavaScript. But unlike in JavaScript, their prototype chain cannot be directly accessed or even modified. -Another difference is that in Pkl, object properties are late-bound. Read on to see what this means. +Another difference is that in Pkl, object properties are late-bound. +Read on to see what this means. [[amends-declaration]] [NOTE] @@ -685,6 +702,7 @@ parrot = (pigeon) { // <2> name = "Parrot" } ---- + <1> Amends declaration. <2> Amends expression. @@ -710,6 +728,7 @@ dodo = (pigeon) { extinct = true } // <2> ---- + <1> Chained amends declaration. <2> Chained amends expression (`(pigeon) { ... } { ... }` is the amends expression). ==== @@ -729,6 +748,7 @@ penguin { } adultWeightInGrams = penguin.adultWeightInGrams ---- + <1> result: `4000` We have defined a hypothetical `penguin` object whose `adultWeightInGrams` property is defined in terms of the `eggIncubation` duration. @@ -741,6 +761,7 @@ madeUpBird = (penguin) { } adultWeightInGrams = madeUpBird.adultWeightInGrams // <1> ---- + <1> result: `1100` As you can see, ``madeUpBird``'s `adultWeightInGrams` changed along with its `eggIncubation`. @@ -751,8 +772,7 @@ This is what we mean when we say that object properties are _late-bound_. ==== A good analogy is that object properties behave like spreadsheet cells. When they are linked, changes to "downstream" properties automatically propagate to "upstream" properties. -The main difference is that editing a spreadsheet cell changes the state of the spreadsheet, -whereas "editing" a property results in a new object, leaving the original object untouched. +The main difference is that editing a spreadsheet cell changes the state of the spreadsheet, whereas "editing" a property results in a new object, leaving the original object untouched. It is as if you made a copy of the entire spreadsheet whenever you edited a cell! ==== @@ -809,23 +829,23 @@ To make these boundaries clear, transitioning between _lazy_ and _eager_ data ty Pkl has two kinds of objects: * A link:{uri-stdlib-Dynamic}[Dynamic] object has no predefined structure.{fn-typed-objects} - When a dynamic object is amended, not only can existing properties be overridden or amended, but new properties can also be added. - So far, we have only used dynamic objects in this chapter. +When a dynamic object is amended, not only can existing properties be overridden or amended, but new properties can also be added. +So far, we have only used dynamic objects in this chapter. * A link:{uri-stdlib-Typed}[Typed] object has a fixed structure described by a class definition. - When a typed object is amended, its properties can be overridden or amended, but new properties cannot be added. - In other words, the new object has the same class as the original object. +When a typed object is amended, its properties can be overridden or amended, but new properties cannot be added. +In other words, the new object has the same class as the original object. [TIP] .When to Use Typed vs. Dynamic Objects ==== * Use typed objects to build schema-backed data models that are validatedfootnote:[By "Use typed objects" we mean to define classes and build data models out of instances of these classes.]. - This is what most templates do. +This is what most templates do. * Use dynamic objects to build schema-less data models that are not validated. - Dynamic objects are useful for ad-hoc tasks, tasks that do not justify the effort of writing and maintaining a schema, and for representing data whose structure is unknown. +Dynamic objects are useful for ad-hoc tasks, tasks that do not justify the effort of writing and maintaining a schema, and for representing data whose structure is unknown. ==== -Note that every <> is a typed object. Its properties implicitly define a class, -and new properties cannot be added when amending the module. +Note that every <> is a typed object. +Its properties implicitly define a class, and new properties cannot be added when amending the module. A typed object is backed by a _class_. Let's look at an example: @@ -844,11 +864,12 @@ pigeon = new Bird { // <2> migratory = false } ---- + <1> Defines a class named `Bird` with properties `name`, `lifespan` and `migratory`. <2> Defines a module property named `pigeon`. - Its value is a typed object constructed by instantiating class `Bird`. - A type only needs to be stated when the property does not have or inherit a <>. - Otherwise, amend syntax (`pigeon { ... }`) or shorthand instantiation syntax (`pigeon = new { ... }`) should be used. +Its value is a typed object constructed by instantiating class `Bird`. +A type only needs to be stated when the property does not have or inherit a <>. +Otherwise, amend syntax (`pigeon { ... }`) or shorthand instantiation syntax (`pigeon = new { ... }`) should be used. Congratulations, you have constructed your first typed objectfootnote:[Not counting that every module is a typed object.]! How does it differ from a dynamic object? @@ -919,6 +940,7 @@ pigeon = new Dynamic { // <1> lifespan = 8 }.toTyped(Bird) // <2> ---- + <1> Instead of a `new Bird`, `pigeon` can be defined with a `new Dynamic`. <2> That `Dynamic` is then converted to a `Bird`. ==== @@ -955,6 +977,7 @@ favoritePigeon = (pigeon) { samePigeon = pigeon == favoritePigeon // <6> ---- + <1> Properties defined as `hidden` are accessible on any `Bird` instance, but not output by default. <2> Non-hidden properties can refer to hidden properties as usual. <3> `pigeon` is an object with _four_ properties, but is rendered with _three_ properties. @@ -986,6 +1009,7 @@ samePigeon = true ---- ==== Local properties + A property with the modifier `local` can only be referenced in the lexical scope of its definition. [source,{pkl}] @@ -1005,6 +1029,7 @@ pigeon = new Bird { pigeonInIndex = pigeon.nameAndLifespanInIndex // <3> pigeonSeparator = pigeon.separator // Error <4> ---- + <1> This property can only be accessed from inside this _class definition_. <2> Non-local properties can refer to the local property as usual. <3> The _value_ of `separator` occurs in `nameAndLifespanInIndex`. @@ -1052,6 +1077,7 @@ birds { // <2> "Giraffe" } ---- + <1> Error: cannot assign to fixed property `laysEggs` <2> Error: cannot amend fixed property `birds` @@ -1071,12 +1097,12 @@ class Penguin extends Bird { fixed name = "Penguin" // <2> } ---- + <1> Error: missing modifier `fixed`. <2> Error: modifier `fixed` cannot be applied to property `name`. The `fixed` modifier is useful for defining properties that are meant to be derived from other properties. -In the following snippet, the property `wingspanWeightRatio` is not meant to be assigned to, because it is derived -from other properties. +In the following snippet, the property `wingspanWeightRatio` is not meant to be assigned to, because it is derived from other properties. [source%parsed,{pkl}] ---- @@ -1105,14 +1131,14 @@ class Osprey extends Bird { fixed species: "Pandion haliaetus" // <2> } ---- + <1> No explicit default because the property is overridden by a child class. <2> Overrides the type from `String` to the <> `"Pandion haliaetus"`. + Assigning an explicit default would be redundant, therefore it is omitted. ==== Const properties -A property with the `const` modifier behaves like the <> modifier, -with the additional rule that it cannot reference non-const properties or methods. +A property with the `const` modifier behaves like the <> modifier, with the additional rule that it cannot reference non-const properties or methods. .Bird.pkl [source%tested,{pkl}] @@ -1145,6 +1171,7 @@ const bird: Bird = new { lifespan = birdLifespan(24) // <2> } ---- + <1> Error: cannot reference non-const property `pigeonName` from a const property. <2> Allowed: `birdLifespan` is const. @@ -1164,6 +1191,7 @@ const bird: Bird = new { description = "species: \(speciesName), lifespan: \(lifespan)" // <1> } ---- + <1> `lifespan` is declared within property `bird`. `speciesName` resolves to `this.speciesName`, where `this` is a value within property `bird`. NOTE: Because `const` members can only reference themselves and other `const` members, they are not <>. @@ -1197,13 +1225,12 @@ oldPigeonName: String typealias IsPigeonName = String(pigeonName) // <3> ---- + <1> Error: cannot reference non-const property `pigeonName` from a class. <2> Error: cannot reference non-const property `pigeonName` from an annotation. <3> Error: cannot reference non-const property `pigeonname` from a typealias. -This rule exists because classes, annotations, and typealiases are not <>; -it is not possible to change the definition of these members by amending the module -where it is defined. +This rule exists because classes, annotations, and typealiases are not <>; it is not possible to change the definition of these members by amending the module where it is defined. Generally, there are two strategies for referencing such properties: @@ -1238,6 +1265,7 @@ This solution makes sense if `pigeonName` does not get assigned/amended when ame + name: String = Birds.pigeonName } ---- + <1> module `Birds` imports itself This solution works because an import clause implicitly defines a `const local` property and amending this module does not affect a self-import. @@ -1255,8 +1283,7 @@ Listings combine qualities of lists and objects: * Like lists, listings can contain arbitrary elements. * Like objects, listings excel at defining and amending nested literal data structures. -* Like objects, listings can only be directly manipulated through amendment, - but converting them to a list (and, if necessary, back to a listing) opens the door to arbitrary transformations. +* Like objects, listings can only be directly manipulated through amendment, but converting them to a list (and, if necessary, back to a listing) opens the door to arbitrary transformations. * Like object properties, listing elements are evaluated lazily, can be defined in terms of each other, and are late-bound. [TIP] @@ -1288,12 +1315,13 @@ birds = new Listing { // <1> } } ---- + <1> Defines a module property named `birds` with a value of type `Listing`. - A type only needs to be stated when the property does not have or inherit a <>. - Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used. +A type only needs to be stated when the property does not have or inherit a <>. +Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used. <2> Defines a listing element of type `Dynamic`. <3> Defines another listing element of type `Dynamic`. - The order of definitions is relevant. +The order of definitions is relevant. To access an element by index, use the `[]` (subscript) operator: @@ -1302,6 +1330,7 @@ To access an element by index, use the `[]` (subscript) operator: firstBirdName = birds[0].name // <1> secondBirdDiet = birds[1].diet // <2> ---- + <1> result: `"Pigeon"` <2> result: `"Berries"` @@ -1317,6 +1346,7 @@ listing = new Listing { } } ---- + <1> Defines a listing element of type `String`. <2> Defines a listing element of type `Duration`. <3> Defines a listing element of type `Listing`. @@ -1331,8 +1361,9 @@ listing = new Listing { "A " + pigeon + " is a bird" // <3> } ---- + <1> Defines a local property with the value `"Pigeon"`. - Local properties can have a type annotation, as in `pigeon: String = "Pigeon"`. +Local properties can have a type annotation, as in `pigeon: String = "Pigeon"`. <2> Defines a listing element that references the local property. <3> Defines another listing element that references the local property. @@ -1373,7 +1404,9 @@ birds2 = (birds) { // <1> } } ---- -<1> Defines a module property named `birds2`. Its value is a listing that amends `birds`. + +<1> Defines a module property named `birds2`. +Its value is a listing that amends `birds`. <2> Defines a listing element of type `Dynamic`. <3> Amends the listing element at index 0 (whose name is `"Pigeon"`) and overrides property `diet`. <4> Overrides the listing element at index 1 (whose name is `"Parrot"`) with an entirely new dynamic object. @@ -1395,6 +1428,7 @@ birds = new Listing { } } ---- + <1> Defines a listing element of type `Dynamic`. <2> Defines a listing element that amends the element at index 0 and overrides `name`. @@ -1410,6 +1444,7 @@ newBirds = (birds) { // <1> secondBirdDiet = newBirds[1].diet // <2> ---- + <1> Amends listing `birds` and overrides property `diet` of element 0 (whose name is "Pigeon"`) to have the value `"Worms"`. <2> Because element 1 is defined in terms of element 0, its `diet` property also changes to `"Worms"`. @@ -1483,9 +1518,11 @@ birds = new Listing { } } ---- + <1> Amends the `default` element and sets property `lifespan`. <2> Defines a new listing element that implicitly amends the default element. -<3> Defines a new property called `name`. Property `lifespan` is inherited from the default element. +<3> Defines a new property called `name`. +Property `lifespan` is inherited from the default element. <4> Defines a new listing element that implicitly amends the default element. <5> Overrides the default for property `lifespan`. @@ -1544,8 +1581,8 @@ This declaration has the following effects: * `x` is initialized with an empty listing. * If `ElementType` has a <>, that value becomes the listing's default element. * The first time `x` is read, - ** its value is checked to have type `Listing`. - ** the listing's elements are checked to have the type `ElementType`. +** its value is checked to have type `Listing`. +** the listing's elements are checked to have the type `ElementType`. Here is an example: @@ -1577,9 +1614,7 @@ birds { } ---- -Thanks to ``birds``'s default element, which was inferred from its type, -it is not necessary to state the type of each list element -(`new Bird { ... }`, `new Bird { ... }`, etc.). +Thanks to ``birds``'s default element, which was inferred from its type, it is not necessary to state the type of each list element (`new Bird { ... }`, `new Bird { ... }`, etc.). ==== Distinct Elements @@ -1619,8 +1654,7 @@ Mappings combine qualities of maps and objects: * Like maps, mappings can contain arbitrary key-value pairs. * Like objects, mappings excel at defining and amending nested literal data structures. -* Like objects, mappings can only be directly manipulated through amendment, - but converting them to a map (and, if necessary, back to a mapping) opens the door to arbitrary transformations. +* Like objects, mappings can only be directly manipulated through amendment, but converting them to a map (and, if necessary, back to a mapping) opens the door to arbitrary transformations. * Like object properties, a mapping's values (but not its keys) are evaluated lazily, can be defined in terms of each other, and are late-bound. [TIP] @@ -1652,9 +1686,10 @@ birds = new Mapping { // <1> } } ---- + <1> Defines a module property named `birds` with a value of type `Mapping`. - A type only needs to be stated when the property does not have or inherit a <>. - Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used. +A type only needs to be stated when the property does not have or inherit a <>. +Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used. <2> Defines a mapping entry with the key `"Pigeon"` and a value of type `Dynamic`. <3> Defines a mapping entry with the key `"Parrot"` and a value of type `Dynamic`. @@ -1712,10 +1747,11 @@ mapping = new Mapping { } } ---- + <1> Defines a local property name `parrot` with the value `"Parrot"`. - Local properties can have a type annotation, as in `parrot: String = "Parrot"`. +Local properties can have a type annotation, as in `parrot: String = "Parrot"`. <2> Defines a mapping entry whose value references `parrot`. - The local property is visible to values but not keys. +The local property is visible to values but not keys. [[amending-mappings]] === Amending Mappings @@ -1754,7 +1790,9 @@ birds2 = (birds) { // <1> } } ---- -<1> Defines a module property named `birds2`. Its value is a mapping that amends `birds`. + +<1> Defines a module property named `birds2`. +Its value is a mapping that amends `birds`. <2> Defines a mapping entry with the key `"Barn owl"` and a value of type `Dynamic`. <3> Amends mapping entry `"Pigeon"` and overrides property `diet`. <4> Overrides mapping entry `"Parrot"` with an entirely new value of type `Dynamic`. @@ -1776,6 +1814,7 @@ birds = new Mapping { } } ---- + <1> Defines a mapping entry with the key `"Pigeon"` and a value of type `Dynamic`. <2> Defines a mapping entry with the key `"Parrot"` and a value that amends `"Pigeon"`. @@ -1791,6 +1830,7 @@ birds2 = (birds) { // <1> parrotDiet = birds2["Parrot"].diet // <2> ---- + <1> Amends mapping `birds` and overrides ``"Pigeon"``'s `diet` property to have value `"Seeds"`. <2> Because `"Parrot"` is defined in terms of `"Pigeon"`, its `diet` property also changes to `"Seeds"`. @@ -1863,9 +1903,11 @@ birds = new Mapping { } } ---- + <1> Amends the `default` value and sets property `lifespan`. <2> Defines a mapping entry with the key `"Pigeon"` that implicitly amends the default value. -<3> Defines a new property called `diet`. Property `lifespan` is inherited from the default value. +<3> Defines a new property called `diet`. +Property `lifespan` is inherited from the default value. <4> Defines a mapping entry with the key `"Parrot"` that implicitly amends the default value. <5> Overrides the default for property `lifespan`. @@ -1922,9 +1964,9 @@ This declaration has the following effects: * `x` is initialized with an empty mapping. * If `ValueType` has a <>, that value becomes the mapping's default value. * The first time `x` is read, - ** its value is checked to have type `Mapping`. - ** the mapping's keys are checked to have type `KeyType`. - ** the mapping's values are checked to have type `ValueType`. +** its value is checked to have type `Mapping`. +** the mapping's keys are checked to have type `KeyType`. +** the mapping's values are checked to have type `ValueType`. Here is an example: @@ -1953,9 +1995,7 @@ birds { } ---- -Thanks to ``birds``'s default value, which was inferred from its type, -it is not necessary to state the type of each mapping value -(`["Pigeon"] = new Bird { ... }`, `["Parrot"] = new Bird { ... }`, etc.). +Thanks to ``birds``'s default value, which was inferred from its type, it is not necessary to state the type of each mapping value (`["Pigeon"] = new Bird { ... }`, `["Parrot"] = new Bird { ... }`, etc.). [[classes]] == Classes @@ -1987,7 +2027,7 @@ pigeon: Bird = new { pigeonClass = pigeon.getClass() ---- -Declaration of new class instances will fail when property names are misspelled: +Declaration of new class instances will fail when property names are misspelled: [source%tested%error,{pkl}] ---- @@ -2045,6 +2085,7 @@ parrot: Bird = new { greeting1 = pigeon.greet(parrot) // <3> greeting2 = greetPigeon(parrot) // <4> ---- + <1> Instance method of class `Bird`. <2> Module method. <3> Call instance method on `pigeon`. @@ -2116,12 +2157,14 @@ Modules are loaded from _module URIs_. By default, the following URI types are available for import: ==== File URI: -Example: `+file:///path/to/my_module.pkl+` + +Example: `+file:///path/to/my_module.pkl+` Represents a module located on a file system. ==== HTTP(S) URI: -Example: `+https://example.com/my_module.pkl+` + +Example: `+https://example.com/my_module.pkl+` Represents a module imported via an HTTP(S) GET request. @@ -2129,6 +2172,7 @@ NOTE: Modules loaded from HTTP(S) URIs are only cached until the `pkl` command e [[module-path-uri]] ==== Module path URI: + Example: `+modulepath:/path/to/my_module.pkl+` Module path URIs are resolved relative to the _module path_, a search path for modules similar to Java's class path (see the `--module-path` CLI option). @@ -2143,6 +2187,7 @@ In a typical Java project, this corresponds to file path `src/main/resources/pat [[package-asset-uri]] ==== Package asset URI: + Example: `+package://example.com/mypackage@1.0.0#/my_module.pkl+` Represent a module within a _package_. @@ -2193,7 +2238,8 @@ with `import "parrot.pkl"` or `import "/animals/birds/parrot.pkl"`. ==== Relative paths use the `/` character as the directory separator on all platforms, including Windows. -Paths that contain drive letters (e.g. `C:`) must be declared as an absolute file URI, for example: `import "file:///C:/path/to/my/module.pkl"`. Otherwise, they are interpreted as a URI scheme. +Paths that contain drive letters (e.g. `C:`) must be declared as an absolute file URI, for example: `import "file:///C:/path/to/my/module.pkl"`. +Otherwise, they are interpreted as a URI scheme. ==== NOTE: When importing a relative directory or file that starts with `@`, the import string must be prefixed with `./`. @@ -2229,8 +2275,7 @@ $ pkl eval pkl:math [[triple-dot-module-uris]] ==== Triple-dot Module URIs -To simplify referencing ancestor modules in a hierarchical module structure, -relative file and module path URIs may start with `++...++/`, a generalization of `../`. +To simplify referencing ancestor modules in a hierarchical module structure, relative file and module path URIs may start with `++...++/`, a generalization of `../`. Module URI `++...++/foo/bar/baz.pkl` resolves to the first existing module among `../foo/bar/baz.pkl`, `../../foo/bar/baz.pkl`, `../../../foo/bar/baz.pkl`, and so on. Furthermore, module URI `++...++` is equivalent to `++...++/`. @@ -2255,6 +2300,7 @@ parrot = (pigeon) { // <1> name = "Parrot" // <2> } ---- + <1> Object `parrot` amends object `pigeon`, inheriting all of its members. <2> `parrot` overrides `name`. @@ -2274,6 +2320,7 @@ amends "pigeon.pkl" // <1> name = "Parrot" // <2> ---- + <1> Module `parrot` amends module `pigeon`, inheriting all of its members. <2> `parrot` overrides `name`. @@ -2330,6 +2377,7 @@ class Parrot extends Pigeon { // <2> function say() = "Pkl is great!" // <6> } ---- + <1> Class `Pigeon` is declared as `open` for extension. <2> Class `Parrot` extends `Pigeon`, inheriting all of its members. <3> `Parrot` overrides `name`. @@ -2347,6 +2395,7 @@ open module pigeon // <1> name = "Pigeon" diet = "Seeds" ---- + <1> Module `pigeon` is declared as `open` for extension. .parrot.pkl @@ -2360,6 +2409,7 @@ extinct = false // <4> function say() = "Pkl is great!" // <5> ---- + <1> Module `parrot` extends module `pigeon`, inheriting all of its members. <2> `parrot` overrides `name`. <3> `parrot` overrides `diet`. @@ -2419,8 +2469,7 @@ name = "Parrot" Multiple import clauses are permitted. A module import implicitly defines a new `const local` property through which the imported module can be accessed. -(Remember that modules are regular objects.) -The name of this property, called _import name_, is constructed from the module URI as follows: +(Remember that modules are regular objects.) The name of this property, called _import name_, is constructed from the module URI as follows: . Strip the URI scheme, including the colon (`:`). . Strip everything up to and including the last forward slash (`/`). @@ -2488,7 +2537,8 @@ When creating a new module, especially one intended for import into other module Less than six characters, not counting the `.pkl` file extension, is a good rule of thumb. * valid identifier + -Stick to alphanumeric characters. Use an underscore (`_`) instead of a hyphen (`-`) as a name separator. +Stick to alphanumeric characters. +Use an underscore (`_`) instead of a hyphen (`-`) as a name separator. * descriptive + An import name should make sense on its own and when used in qualified member names. @@ -2528,6 +2578,7 @@ import* "reptiles/*.pkl" <2> birds = import*("birds/*.pkl") // <3> ---- + <1> Globbed import clause <2> Globbed import clause without an explicit name (will import the name `*`) <3> Globbed import expression @@ -2573,13 +2624,13 @@ Otherwise, characters are interpreted verbatim, and not treated as glob wildcard For details on how glob patterns work, refer to <> in the Advanced Topics section. -NOTE: When globbing files, symbolic links are not followed. Additionally, the `.` and `..` entries are skipped. + +NOTE: When globbing files, symbolic links are not followed. +Additionally, the `.` and `..` entries are skipped. + This behavior is similar to the behavior of Bash with `shopt -s dotglob` enabled. === Security Checks -When attempting to directly evaluate a module, as in `pkl eval myModule.pkl`, -the following security checks are performed: +When attempting to directly evaluate a module, as in `pkl eval myModule.pkl`, the following security checks are performed: * The module URI is checked against the module allowlist (`--allowed-modules`). @@ -2587,8 +2638,7 @@ The module allowlist is a comma-separated list of regular expressions. For access to be granted, at least one regular expression must match a prefix of the module URI. For example, the allowlist `file:,https:` grants access to any module whose URI starts with `file:` or `https:`. -When a module attempts to load another module (via `amends`, `extends`, or `imports`), -the following security checks are performed: +When a module attempts to load another module (via `amends`, `extends`, or `imports`), the following security checks are performed: * The target module URI is checked against the module allowlist (`--allowed-modules`). * The source and target modules' _trust levels_ are determined and compared. @@ -2778,7 +2828,7 @@ THIS IS THE FINAL OUTPUT ==== Multiple File Output It is sometimes desirable for a single module to produce multiple output files. -This is possible by configuring a module's link:{uri-stdlib-outputFiles}[`output.files`] property +This is possible by configuring a module's link:{uri-stdlib-outputFiles}[`output.files`] property // suppress inspection "AsciiDocLinkResolve" and specifying the xref:pkl-cli:index.adoc#multiple-file-output-path[`--multiple-file-output-path`] (or `-m` for short) CLI option. @@ -2828,11 +2878,8 @@ name: Parrot diet: Berries ---- -Within `output.files`, -a key determines a file's path relative to `--multiple-file-output-path`, -and a value determines the file's contents. -If a file's path resolves to a location outside `--multiple-file-output-path`, -evaluation fails with an error. +Within `output.files`, a key determines a file's path relative to `--multiple-file-output-path`, and a value determines the file's contents. +If a file's path resolves to a location outside `--multiple-file-output-path`, evaluation fails with an error. Non-existing parent directories are created. [[aggregating-module-outputs]] @@ -2878,8 +2925,7 @@ output { [TIP] ==== -When aggregating module outputs, -the appropriate file extensions can be obtained programmatically: +When aggregating module outputs, the appropriate file extensions can be obtained programmatically: .birds.pkl [source%parsed,{pkl}] @@ -2915,6 +2961,7 @@ nameNonNull = name!! // <1> name2 = null name2NonNull = name2!! // <2> ---- + <1> result: `"Pigeon"` <2> result: _Error: Expected a non-null value, but got `null`._ @@ -2938,6 +2985,7 @@ nameOrParrot = name ?? "Parrot" // <1> name2 = null name2OrParrot = name2 ?? "Parrot" // <2> ---- + <1> result: `"Pigeon"` <2> result: `"Parrot"` @@ -2947,13 +2995,10 @@ name2OrParrot = name2 ?? "Parrot" // <2> Many languages allow `null` for (almost) every type, but Pkl does not. Any type can be extended to include `null` by appending `?` to the type. -For example, `parrot: Bird` will always be non-null, but `pigeon: Bird?` could be `null` - and _is_ by default, -if `pigeon` is never amended. This means if you try to coalesce a (non-nullable) typed variable, -the result is always that variable’s value. +For example, `parrot: Bird` will always be non-null, but `pigeon: Bird?` could be `null` - and _is_ by default, if `pigeon` is never amended. +This means if you try to coalesce a (non-nullable) typed variable, the result is always that variable’s value. -As per our example `parrot ?? pigeon == parrot` always holds, -but `pigeon ?? parrot` could either be `pigeon` or `parrot`, -depending on whether `pigeon` was ever amended with a non-null value. +As per our example `parrot ?? pigeon == parrot` always holds, but `pigeon ?? parrot` could either be `pigeon` or `parrot`, depending on whether `pigeon` was ever amended with a non-null value. ==== [[null-propagation]] @@ -2979,6 +3024,7 @@ name2 = null name2Length = name2?.length // <3> name2Upper = name2?.toUpperCase() // <4> ---- + <1> result: `6` <2> result: `"PIGEON"` <3> result: `null` @@ -2991,6 +3037,7 @@ The `?.` operator is often combined with `??`: name = null nameLength = name?.length ?? 0 // <1> ---- + <1> result: `0` === ifNonNull Method @@ -3013,6 +3060,7 @@ nameWithTitle = name.ifNonNull((it) -> "Dr." + it) // <1> name2 = null name2WithTitle = name2.ifNonNull((it) -> "Dr." + it) // <2> ---- + <1> result: `"Dr. Pigeon"` <2> result: `null` @@ -3035,6 +3083,7 @@ Every `if` expression must have an `else` branch. ---- num = if (2 + 2 == 5) 1984 else 42 // <1> ---- + <1> result: `42` [[resources]] @@ -3062,7 +3111,9 @@ Result type is link:{uri-stdlib-Resource}[Resource]. modulepath: :: Reads a resource from the module path (`--module-path`) or JVM class path. Result type is link:{uri-stdlib-Resource}[Resource]. See <> for further information. -package: :: Reads a resource from a _package_. Result type is link:{uri-stdlib-Resource}[Resource]. See <> for further information. +package: :: Reads a resource from a _package_. +Result type is link:{uri-stdlib-Resource}[Resource]. +See <> for further information. Relative resource URIs are resolved against the enclosing module's URI. @@ -3073,8 +3124,7 @@ Therefore, subsequent reads are guaranteed to return the same result. === Nullable Reads If a resource does not exist or cannot be read, `read()` fails with an error. -To recover from the absence of a resource, use `read?()` instead, -which returns `null` for absent resources: +To recover from the absence of a resource, use `read?()` instead, which returns `null` for absent resources: [source%parsed,{pkl}] ---- @@ -3129,7 +3179,8 @@ Globbing other resources results in an error. For details on how glob patterns work, reference <> in the Advanced Topics section. -NOTE: When globbing files, symbolic links are not followed. Additionally, the `.` and `..` entries are skipped. + +NOTE: When globbing files, symbolic links are not followed. +Additionally, the `.` and `..` entries are skipped. + This behavior is similar to the behavior of Bash with `shopt -s dotglob` enabled. [NOTE] @@ -3171,6 +3222,7 @@ To raise an error, use a `throw` expression: ---- myValue = throw("You won't be able to recover from this one!") // <1> ---- + <1> `myValue` never receives a value because the program exits. The error message is printed to the console and the program exits. @@ -3229,7 +3281,8 @@ This section discusses language features that are generally more relevant to tem <> + <> + <> + -<> +<> + +<> [[meaning-of-new]] === Meaning of `new` @@ -3259,6 +3312,7 @@ bird: Bird // <1> birdListing: Listing // <2> birdMapping: Mapping // <3> ---- + <1> Without an explicit default value, this property has default value `new Bird { name = "polly" }` <2> With an explicit element type argument, this property's default value is equivalent to `new Listing { default = (_) -> new Bird { name = "polly" } }` <3> With an explicit value type argument, this property's default value is equivalent to `new Mapping { default = (_) -> new Bird { name = "polly" } }` @@ -3300,6 +3354,7 @@ someMapping = new Mapping { } } ---- + <1> Assigning a `new` explicitly-typed value to a property. <2> Adding an `new` explicitly-typed `Listing` element. The value will not have property `isPredatory = true` as the `default` property of the `Listing` is not used. @@ -3378,6 +3433,7 @@ aviary: Aviary = new { swiftHatchlings = typedProperty.listHatchlings(new { "Poppy"; "Chirpy" }) // <8> ---- + <1> Assignment to a property with an explicitly declared type, amending `new Bird {}`. <2> Assignment to an undeclared property in module context, amending `new Dynamic {}`. <3> `Listing` element creation, amending implicit `default`, `new Bird {}`. @@ -3412,6 +3468,7 @@ birdDiets = let (diets = List("Seeds", "Berries", "Mice")) List(diets[2], diets[0]) // <1> ---- + <1> result: `birdDiets = List("Mice", "Seeds")` `let` expressions serve two purposes: @@ -3427,6 +3484,7 @@ birdDiets = let (diets: List = List("Seeds", "Berries", "Mice")) diets[2] + diets[0] // <1> ---- + <1> result: `birdDiets = List("Mice", "Seeds")` `let` expressions can be stacked: @@ -3438,6 +3496,7 @@ birdDiets = let (diet = List("Seeds", "Mice", "Berries")) birds.zip(diet) // <1> ---- + <1> result: `birdDiets = List(Pair("Pigeon", "Seeds"), Pair("Barn owl", "Mice"), Pair("Parrot", "Berries"))` [[type-tests]] @@ -3478,6 +3537,7 @@ test1 = email is String(contains("@")) // <1> test2 = map is Map // <2> test3 = name is "Pigeon"|"Barn owl"|"Parrot" // <3> ---- + <1> `email` is tested for being a string that contains a `@` sign <2> `map` is tested for being a map from `Int` to `Base` values <3> `name` is tested for being one of `"Pigeon"`, `"Barn owl"`, or `"Parrot"` @@ -3525,6 +3585,7 @@ list1 = List() // <1> list2 = List(1, 2, 3) // <2> list3 = List(1, "x", 5.min, List(1, 2, 3)) // <3> ---- + <1> result: empty list <2> result: list of length 3 <3> result: heterogeneous list whose last element is another list @@ -3543,6 +3604,7 @@ To access a list element by index, use the `[]` (subscript) operator: list = List(1, 2, 3, 4) listElement = list[2] // <1> ---- + <1> result: `3` Class `List` offers a link:{uri-stdlib-List}[rich API]. @@ -3558,6 +3620,7 @@ res4 = list.reverse() // <4> res5 = list.drop(1).take(2) // <5> res6 = list.map((n) -> n * 3) // <6> ---- + <1> result: `true` <2> result: `1` <3> result: `List(2, 3, 4)` @@ -3579,6 +3642,7 @@ res2 = Set(1, 2, 3) // <2> res3 = Set(1, 2, 3, 1) // <3> res4 = Set(1, "x", 5.min, List(1, 2, 3)) // <4> ---- + <1> result: empty set <2> result: set of length 3 <3> result: same set of length 3 @@ -3594,6 +3658,7 @@ res2 = res1.first // <1> res3 = res1.toListing() // <2> res4 = Set(2, 3, 4) == res1 // <3> ---- + <1> result: `4` <2> result: `new Listing { 4; 3; 2 }` <3> result: `true` @@ -3604,6 +3669,7 @@ To compute the union of sets, use the `+` operator: ---- Set(1, 2) + Set(2, 3) + Set(5, 3) // <1> ---- + <1> result: `Set(1, 2, 3, 5)` Class `Set` offers a link:{uri-stdlib-Set}[rich API]. @@ -3617,6 +3683,7 @@ res2 = set.drop(1).take(2) // <2> res3 = set.map((n) -> n * 3) // <3> res4 = set.intersect(Set(3, 9, 2)) // <4> ---- + <1> result: `true` <2> result: `Set(2, 3)` <3> result: `Set(3, 6, 9, 12)` @@ -3649,6 +3716,7 @@ map1 = Map() // <1> map2 = Map(1, "one", 2, "two", 3, "three") // <2> map3 = Map(1, "x", 2, 5.min, 3, Map(1, 2)) // <3> ---- + <1> result: empty map <2> result: set of length 3 <3> result: heterogeneous map whose last value is another map @@ -3670,6 +3738,7 @@ res2 = res1.entries.first // <1> res3 = res1.toMapping() // <2> res4 = res1 == Map(1, "world", 2, "hello") // <3> ---- + <1> result: `Pair(2, "hello")` <2> result: `new Mapping { [2] = "hello"; [1] = "world" }` <3> result: `true` @@ -3680,6 +3749,7 @@ To merge maps, use the `+` operator: ---- combinedMaps = Map(1, "one") + Map(2, "two", 1, "three") + Map(4, "four") // <1> ---- + <1> result: `Map(1, "three", 2, "two", 4, "four")` To access a value by key, use the `[]` (subscript) operator: @@ -3689,6 +3759,7 @@ To access a value by key, use the `[]` (subscript) operator: map = Map("Pigeon", 5.gb, "Parrot", 10.gb) parrotValue = map["Parrot"] // <1> ---- + <1> result: `10.gb` Class `Map` offers a link:{uri-stdlib-Map}[rich API]. @@ -3703,6 +3774,7 @@ res3 = map.isEmpty // <3> res4 = map.length // <4> res5 = map.getOrNull("Falcon") // <5> ---- + <1> result: `true` <2> result: `false` <3> result: `false` @@ -3721,6 +3793,7 @@ A value of type `Bytes` is a sequence of `UInt8` elements. bytes1 = Bytes(0xff, 0x00, 0x3f) // <1> bytes2 = Bytes() // <2> ---- + <1> Result: a `Bytes` with 3 elements <2> Result: an empty `Bytes` @@ -3730,6 +3803,7 @@ bytes2 = Bytes() // <2> ---- bytes3 = "cGFycm90".base64DecodedBytes // <1> ---- + <1> Result: `Bytes(112, 97, 114, 114, 111, 116)` ==== `Bytes` vs `List` @@ -3786,6 +3860,7 @@ list1 = matches.drop(1).map((it) -> it.start) // <1> list2 = matches.drop(1).map((it) -> it.value) // <2> list3 = matches.drop(1).map((it) -> it.groups[1].value) // <3> ---- + <1> result: `List(0, 19, 40)` (the entire match, `matches[0]`, was dropped) <2> result: `List("pigeon@example.com", "falcon@example.com", "parrot@example.com")` <3> result: `List("pigeon, falcon, parrot")` @@ -3879,8 +3954,7 @@ port: Int(isBetween(0, 1023)) = 443 NOTE: Remember that numbers are always instances of `Int` or `Float`. Type aliases such as `UInt16` only check that numbers are within a certain range. -The xref:java-binding:codegen.adoc[Java] and xref:kotlin-binding:codegen.adoc[Kotlin] code generators -map predefined type aliases to the most suitable Java and Kotlin types. +The xref:java-binding:codegen.adoc[Java] and xref:kotlin-binding:codegen.adoc[Kotlin] code generators map predefined type aliases to the most suitable Java and Kotlin types. For example, `UInt8` is mapped to `java.lang.Byte` and `kotlin.Byte`, and `Uri` is mapped to `java.net.URI`. [[type-annotations]] @@ -3889,20 +3963,16 @@ For example, `UInt8` is mapped to `java.lang.Byte` and `kotlin.Byte`, and `Uri` Property and method definitions may optionally contain type annotations. Type annotations serve the following purposes: -* Documentation -+ Type annotations help to document data models. They are included in any generated documentation. +* Documentation + Type annotations help to document data models. +They are included in any generated documentation. -* Validation -+ Type annotations are validated at runtime. +* Validation + Type annotations are validated at runtime. -* Defaults -+ Type-annotated properties have <>. +* Defaults + Type-annotated properties have <>. -* Code Generation -+ Type annotations enable statically typed access to configuration data through code generation. +* Code Generation + Type annotations enable statically typed access to configuration data through code generation. -* Tooling -+ Type annotations enable advanced tooling features such as code completion in editors. +* Tooling + Type annotations enable advanced tooling features such as code completion in editors. ==== Class Types @@ -3915,6 +3985,7 @@ class Bird { } bird: Bird // <2> ---- + <1> Declares an instance property of type `String`. <2> Declares a module property of type `Bird`. @@ -3937,6 +4008,7 @@ import "bird.pkl" pigeon: bird // <1> parrot: bird // <1> ---- + <1> Guaranteed to amend _bird.pkl_. As a special case, the `module` keyword denotes the _enclosing_ module's type: @@ -3960,6 +4032,7 @@ friends { import("falcon.pkl") // <1> } ---- + <1> _falcon.pkl_ (not shown here) is guaranteed to amend _bird.pkl_. ==== Type Aliases @@ -3974,6 +4047,7 @@ email: EmailAddress // <1> emailList: List // <2> ---- + <1> equivalent to `email: String(contains("@"))` for type checking purposes <2> equivalent to `emailList: List` for type checking purposes @@ -3988,6 +4062,7 @@ To turn them into _nullable types_, append a question mark (`?`): bird: Bird = null // <1> bird2: Bird? = null // <2> ---- + <1> throws `Type mismatch: Expected a value of type Bird, but got null` <2> succeeds @@ -4048,8 +4123,7 @@ mapping: Mapping // equivalent to `Mapping` ---- The `unknown` type is both a top and a bottom type. -When a static type analyzer encounters an expression of `unknown` type, -it backs off and trusts the user that they know what they are doing. +When a static type analyzer encounters an expression of `unknown` type, it backs off and trusts the user that they know what they are doing. [[union-types]] ==== Union Types @@ -4072,6 +4146,7 @@ foo: List|Bird ---- Union types have no implicit default values, but an explicit type can be chosen using a `*` marker: + [source%parsed,{pkl}] ---- foo: "a"|"b" // undefined. Will throw an error if not amended @@ -4092,8 +4167,7 @@ A string literal type admits a single string value: diet: "Seeds" ---- -While occasionally useful on their own, -string literal types are often combined with <> to form enumerated types: +While occasionally useful on their own, string literal types are often combined with <> to form enumerated types: [source%parsed,{pkl}] ---- @@ -4135,8 +4209,7 @@ A `nothing` return type indicates that a method never returns normally but alway The `unknown` type footnote:[Also known as _dynamic type_. We do not use that term to avoid confusion with `Dynamic`, Pkl's dynamic object type.] is ``nothing``'s even stranger cousin: it is both a top and bottom type! This makes `unknown` assignment-compatible with every other type, and every other type assignment-compatible with `unknown`. -When a static type analyzer encounters a value of `unknown` type, -it backs off and trusts the code's author to know what they are doing -- for example, whether a method called on the value exists. +When a static type analyzer encounters a value of `unknown` type, it backs off and trusts the code's author to know what they are doing -- for example, whether a method called on the value exists. ==== Progressive Disclosure @@ -4149,8 +4222,9 @@ lifespan = 42 // <1> map: Map // <2> function say(name) = name // <3> ---- + <1> shorthand for `lifespan: unknown = 42` - (As a dynamically typed language, Pkl does not try to statically infer types.) +(As a dynamically typed language, Pkl does not try to statically infer types.) <2> shorthand for `map: Map = Map()` <3> shorthand for `function say(name: unknown): unknown = name` @@ -4184,7 +4258,8 @@ nullish: Null // = null <11> <6> Properties of type `Mapping` default to an empty mapping whose default value is the default for `Y`. <7> Properties of non-external class type `X` default to `new X {}`. <8> Properties of type `X?` default to `Null(x)` where `x` is the default for `X`. -<9> Properties with a union type have no default value. By prefixing one of the types in a union with a `*`, the default of that type is chosen as the default for the union. +<9> Properties with a union type have no default value. +By prefixing one of the types in a union with a `*`, the default of that type is chosen as the default for the union. <10> Properties with a string literal type default to the type's only value. <11> Properties of type `Null` default to `null`. @@ -4230,9 +4305,11 @@ pigeon: Bird = new { parent = "Pigeon Sr." // <3> } ---- + <1> Restricts `name` to have at least three characters. <2> The name of the bird (`this`) should not be the same as the name of the `parent`. -<3> Note how `parent` is different from `name`. If they were the same, we would be thrown a constraint error. +<3> Note how `parent` is different from `name`. +If they were the same, we would be thrown a constraint error. In the following example, we define a `Bird` with a name of only two characters. @@ -4293,8 +4370,7 @@ project: Project = new { An _anonymous function_ is a function without a name. -Most modern general-purpose programming languages support anonymous functions, -under names such as _lamba expressions_, _arrow functions_, _function literals_, _closures_, or _procs_. +Most modern general-purpose programming languages support anonymous functions, under names such as _lamba expressions_, _arrow functions_, _function literals_, _closures_, or _procs_. Anonymous functions have their own literal syntax: @@ -4304,6 +4380,7 @@ Anonymous functions have their own literal syntax: (param) -> expr // <2> (param1, param2, ..., paramN) -> expr // <3> ---- + <1> Zero-parameter lambda expression <2> Single-parameter lambda expression <3> Multi-parameter lambda expression @@ -4374,8 +4451,7 @@ addToA = (b: Number) -> a + b list = List(1, 2, 3).map(addToA) // List(43, 44, 45) ---- -Single-parameter anonymous functions can also be applied with the `|>` (pipe) operator, -which expects a function argument to the left and an anonymous function to the right. +Single-parameter anonymous functions can also be applied with the `|>` (pipe) operator, which expects a function argument to the left and an anonymous function to the right. The pipe operator works especially well for chaining multiple functions: [source%tested,{pkl}] @@ -4388,6 +4464,7 @@ num = 4 |> add2 |> mul3 // <1> ---- + <1> result: `42` Like methods, anonymous functions can be recursive: @@ -4397,6 +4474,7 @@ Like methods, anonymous functions can be recursive: factor = (n: Number(isPositive)) -> if (n < 2) n else n * factor.apply(n - 1) num = factor.apply(5) // <1> ---- + <1> result: `120` [[mixins]] @@ -4449,6 +4527,7 @@ barnOwlWithDiet = barnOwl |> withDiet ---- `withDiet` can be generalized by turning it into a factory method for mixins: + [source%tested,{pkl}] ---- function withDiet(_diet: String) = new Mixin { @@ -4464,13 +4543,11 @@ Mixins can themselves be modified with <>. ==== Function Amending An anonymous function that returns an object can be amended with the same syntax as that object. -The result is a new function that accepts the same number of parameters as the original function, -applies the original function to them, and amends the returned object. +The result is a new function that accepts the same number of parameters as the original function, applies the original function to them, and amends the returned object. Function amending is a special form of function composition. Thanks to function amending, link:{uri-stdlib-Listing-default}[Listing.default] -and link:{uri-stdlib-Mapping-default}[Mapping.default] can be treated as if they were objects, -only gradually revealing their true (single-parameter function) nature: +and link:{uri-stdlib-Mapping-default}[Mapping.default] can be treated as if they were objects, only gradually revealing their true (single-parameter function) nature: [source%tested,{pkl}] ---- @@ -4483,13 +4560,13 @@ birds = new Mapping { } } ---- + <1> Amends the `default` function, which returns a default mapping value given a mapping key, and sets property `diet`. <2> Implicitly applies the amended `default` function and amends the returned object with property `lifespan`. The result is a mapping whose entry `"Pigeon"` has both `diet` and `lifespan` set. -When amending an anonymous function, it is possible to access its parameters -by declaring a comma-separated, arrow (`->`) terminated parameter list after the opening curly brace (`{`). +When amending an anonymous function, it is possible to access its parameters by declaring a comma-separated, arrow (`->`) terminated parameter list after the opening curly brace (`{`). Once again, this is especially useful to configure a listing's or mapping's `default` function: @@ -4503,9 +4580,10 @@ birds = new Mapping { ["Barn owl"] {} // <3> } ---- + <1> Amends the `default` function and sets the `name` property to the mapping entry's key. - To access the `default` function's key parameter, it is declared with `key ->`. - (Any other parameter name could be chosen, but `key` is customary for default functions.) +To access the `default` function's key parameter, it is declared with `key ->`. +(Any other parameter name could be chosen, but `key` is customary for default functions.) <2> Defines a mapping entry with key `"Pigeon"` <3> Defines a mapping entry with key `"Barn owl"` @@ -4614,6 +4692,7 @@ They come in two variants: . `when () { } else { }` The following code conditionally generates properties `hobby` and `idol`: + [source%tested,{pkl}] ---- isSinger = true @@ -4665,6 +4744,7 @@ abilitiesByBird { ["Parrot"] = "whistling" } ---- + <1> conditional element <2> conditional entry @@ -4795,6 +4875,7 @@ properties2 { ...properties1 // <3> } ---- + <1> Spreads entries `["Pigeon"] = "Piggy the Pigeon"` and `["Barn owl"] = "Barney the Barn owl"` <2> Spreads elements `1` and `2` <3> Spreads properties `name = "Pigeon"` and `diet = "Seeds"` @@ -4824,7 +4905,8 @@ The following table describes how different iterables turn into object members: These types can only be spread into enclosing objects that support that member type. For example, a `List` can be spread into a `Listing`, but cannot be spread into a `Mapping`. -In some ways, spread syntax can be thought of as a shorthand for a xref:for-generators[for generator]. One key difference is that spread syntax can generate properties, which is not possible with a for generator. +In some ways, spread syntax can be thought of as a shorthand for a xref:for-generators[for generator]. +One key difference is that spread syntax can generate properties, which is not possible with a for generator. [NOTE] ==== @@ -4845,6 +4927,7 @@ newPets { ["Pigeon"] = "Toby the Pigeon" // <1> } ---- + <1> Error: Duplicate definition of member `"Pigeon"`. ==== @@ -4892,6 +4975,7 @@ updated = (environmentVariables) { } } ---- + <1> a listing of environment variables <2> amend element(s) whose name equals "PARROT" + (`name` is shorthand for `this.name`) @@ -4907,6 +4991,7 @@ Matching members are amended (`{ ... }`) or overridden (`= `). Normally, the `this` keyword references the enclosing object's receiver. Example: + [source,pkl] ---- bird { @@ -4917,6 +5002,7 @@ bird { When used inside a <>, `this` refers to the value being tested. Example: + [source,pkl] ---- port: UInt16(this > 1000) @@ -4925,6 +5011,7 @@ port: UInt16(this > 1000) When used inside a <>, `this` refers to the value being matched against. Example: + [source,pkl] ---- animals { @@ -4941,6 +5028,7 @@ The receiver is the bottom-most object in the <>. That means that, within the context of an amending object, the receiver is the amending object. Example: + [source,pkl] ---- hidden lawyerBird { @@ -4951,6 +5039,7 @@ polly = (lawyerBird) { name = "Polly" // <1> } ---- + <1> Polly has title `"Polly, Esq."`. [[outer-keyword]] @@ -4961,6 +5050,7 @@ The `outer` keyword references the <> of the immediately oute It can be useful to disambiguate a lookup that might otherwise resolve elsewhere. Example: + [source%tested,pkl] ---- foo { @@ -4970,6 +5060,7 @@ foo { } } ---- + <1> References `bar` one level higher. Note that `outer` cannot be chained. @@ -4998,6 +5089,7 @@ When used within a class, it refers to the superclass's prototype. When used within an object, it refers to the parent object in the amends chain. Example: + [source%tested,pkl] ---- bird = new { name = "Quail" } @@ -5015,6 +5107,7 @@ class InsectavorousBird extends Bird { super.canEat(food) || food == "insect" // <2> } ---- + <1> Result: `"Ms. Quail"` <2> Calls parent class method `canEat()` @@ -5040,6 +5133,7 @@ some { } } ---- + <1> Resolves to `"Quail"` When used as a type, it is the module's class. @@ -5050,11 +5144,11 @@ module Bird friend: module // <1> ---- + <1> Is class `Bird` The `module` type is a _self type_. -If the module is extended by another module, the `module` type refers to the extending module when -in the context of that module. +If the module is extended by another module, the `module` type refers to the extending module when in the context of that module. [[glob-patterns]] === Glob Patterns @@ -5096,8 +5190,7 @@ NOTE: Unlike globs within shells, the `*` wildcard includes names that start wit [[character-classes]] ==== Character Classes -Character classes are sequences delimited by the `[` and `]` characters, and represent a single -character as described by the sequence within the enclosed brackets. +Character classes are sequences delimited by the `[` and `]` characters, and represent a single character as described by the sequence within the enclosed brackets. For example, the pattern `[abc]` means "a single character that is a, b, or c". Character classes may be negated using `!`. @@ -5119,11 +5212,13 @@ For example, the glob pattern `[]abc]` matches a single character that is either Sub-patterns are glob patterns delimited by the `{` and `}` characters, and separated by the `,` character. For example, the pattern `{pigeon,parrot}` will match either `pigeon` or `parrot`. -Sub-patterns cannot be nested. The pattern `{foo,{bar,baz}}` is not a valid glob pattern, and an error will be thrown during evaluation. +Sub-patterns cannot be nested. +The pattern `{foo,{bar,baz}}` is not a valid glob pattern, and an error will be thrown during evaluation. ==== Escapes -The escape character (`\`) can be used to remove the special meaning of a character. The following escapes are valid: +The escape character (`\`) can be used to remove the special meaning of a character. +The following escapes are valid: * `\[` * `\*` @@ -5133,9 +5228,12 @@ The escape character (`\`) can be used to remove the special meaning of a charac All other escapes are considered a syntax error and an error is thrown. -TIP: If incorporating escape characters into a glob pattern, use <> to express the glob pattern. For example, `+++import*(#"\{foo.pkl"#)+++`. This way, the backslash is interpreted as a backslash and not a string escape. +TIP: If incorporating escape characters into a glob pattern, use <> to express the glob pattern. +For example, `+++import*(#"\{foo.pkl"#)+++`. +This way, the backslash is interpreted as a backslash and not a string escape. ==== Examples + [cols="1,2"] |=== |Pattern |Description @@ -5144,7 +5242,7 @@ TIP: If incorporating escape characters into a glob pattern, use < res2 = number // <3> ---- + <1> Equivalent to `number = 42` <2> References property `{backtick}number{backtick}` <3> Also references property `{backtick}number{backtick}` @@ -5231,8 +5330,7 @@ They are programmatically accessible via the link:{uri-stdlib-reflectModule}/[pk * Start each sentence on a new line and capitalize the first letter. * End each sentence with a punctuation mark. * The first paragraph of a doc comment is its _summary_. - Keep the summary short (a single sentence is common) - and insert an empty line (`///`) before the next paragraph. +Keep the summary short (a single sentence is common) and insert an empty line (`///`) before the next paragraph. ==== Doc comments can be attached to module, class, type alias, property, and method declarations. @@ -5378,8 +5476,7 @@ The most common use cases for annotations are to add metadata to influence behav Annotations are regular Pkl objects whose class extends link:{uri-stdlib-Annotation}[`Annotation`]. Annotation instances are defined similarly to regular <>, but instead of using the `new` keyword the class name is prefixed with `@`. -The object body may be omitted if an annotation's class has no properties or the declared annotation does not override any of the class's default values -Multiple annotations may be defined on a member. +The object body may be omitted if an annotation's class has no properties or the declared annotation does not override any of the class's default values Multiple annotations may be defined on a member. If the annotated member has a <>, the annotation is defined between the comment and the member. [source%tested,{pkl}] @@ -5403,11 +5500,17 @@ class Bird { function greet(greeting: String): String = "\(greeting), \(name)!" } ---- -<1> An annotation applied to a module. The annotation(s) must precede the `module` clause and follow the doc comment. The object body is omitted. + +<1> An annotation applied to a module. +The annotation(s) must precede the `module` clause and follow the doc comment. +The object body is omitted. <2> The definition of an annotation class. -<3> An annotation appied to a class. The object body is omitted. -<4> An annotation applied to a property. The object body is included because the `description` property is overridden. -<5> An annotation applied to a method. The object body is included because the `description` property is overridden. +<3> An annotation appied to a class. +The object body is omitted. +<4> An annotation applied to a property. +The object body is included because the `description` property is overridden. +<5> An annotation applied to a method. +The object body is included because the `description` property is overridden. <6> A second annotation applied to the same method. [[name-resolution]] @@ -5421,25 +5524,22 @@ a = x + 1 ---- The call site's "variable" syntax reveals that `x` refers to a _LAMP_ -(let binding, anonymous function parameter, method parameter, or property) definition. But which one? +(let binding, anonymous function parameter, method parameter, or property) definition. +But which one? To answer this question, Pkl follows these steps: -. Search the lexically enclosing scopes of `x`, - starting with the scope in which `x` occurs and continuing outwards - up to and including the enclosing module's top-level scope, for a LAMP definition named `x`. - If a match is found, this is the answer. +. Search the lexically enclosing scopes of `x`, starting with the scope in which `x` occurs and continuing outwards up to and including the enclosing module's top-level scope, for a LAMP definition named `x`. +If a match is found, this is the answer. . Search the `pkl.base` module for a top-level definition of property `x`. - If a match is found, this is the answer. +If a match is found, this is the answer. . Search the <> of `this`, from bottom to top, for a definition of property `x`. - If a match is found, this is the answer. +If a match is found, this is the answer. . Throw a "name `x` not found" error. NOTE: Pkl's LAMP name resolution is inspired by link:{uri-newspeak}[Newspeak]. The goal is for name resolution to be stable with regard to changes in external modules. -This is why lexically enclosing scopes are searched before the prototype chain of `this`, -and why the prototype chains of lexically enclosing scopes are not searched, -which sometimes requires the use of `outer.` or `module.`. +This is why lexically enclosing scopes are searched before the prototype chain of `this`, and why the prototype chains of lexically enclosing scopes are not searched, which sometimes requires the use of `outer.` or `module.`. For name resolution to fully stabilize, the list of top-level properties defined in `pkl.base` needs to be freezed. This is tentatively planned for Pkl 1.0. @@ -5450,19 +5550,18 @@ Consider this snippet of code buried deep inside a config file: a = x("foo") + 1 ---- -The call site's method call syntax reveals that `x` refers to a method definition. But which one? +The call site's method call syntax reveals that `x` refers to a method definition. +But which one? To answer this question, Pkl follows these steps: -. Search the call sites' lexically enclosing scopes, - starting with the scope in which the call site occurs and continuing outwards - up to and including the enclosing module's top-level scope, for a definition of method `x`. - If a match is found, this is the answer. +. Search the call sites' lexically enclosing scopes, starting with the scope in which the call site occurs and continuing outwards up to and including the enclosing module's top-level scope, for a definition of method `x`. +If a match is found, this is the answer. . Search the `pkl.base` module for a top-level definition of method `x`. - If a match is found, this is the answer. +If a match is found, this is the answer. . Search the class inheritance chain of `this`, starting with the class of `this` - and continuing upwards until and including class `Any`, for a method named `x.` - If a match is found, this is the answer. +and continuing upwards until and including class `Any`, for a method named `x.` +If a match is found, this is the answer. . Throw a "method `x` not found" error. NOTE: Pkl does not support arity or type-based method overloading. @@ -5475,10 +5574,10 @@ Pkl's object model is based on link:{uri-prototypical-inheritance}[prototypical The prototype chain of objectfootnote:[An instance of `Listing`, `Mapping`, `Dynamic`, or (a subclass of) `Typed`.] `x` contains, from bottom to top: 1. The chain of objects amended to create `x`, ending in `x` itself, in reverse order.footnote:[All objects in this chain are instances of the same class, - except when a direct conversion between listing, mapping, dynamic, and typed object has occurred. - For example, `Typed.toDynamic()` returns a dynamic object that amends a typed object.] + except when a direct conversion between listing, mapping, dynamic, and typed object has occurred. + For example, `Typed.toDynamic()` returns a dynamic object that amends a typed object.] 2. The prototype of the class of the top object in (1). - If no amending took place, this is the class of `x`. +If no amending took place, this is the class of `x`. 3. The prototypes of the superclasses of (2). The prototype of class `X` is an instance of `X` that defines the defaults for properties defined in `X`. @@ -5562,8 +5661,8 @@ For a complete list of keywords, consult field `Lexer.KEYWORDS` in {uri-github-P === Blank Identifiers Blank identifiers can be used in many places to ignore parameters and variables. + -`_` is not a valid identifier. To use it as a parameter or variable name, -it needs to be enclosed in backticks: +`_`+. +`_` is not a valid identifier. +To use it as a parameter or variable name, it needs to be enclosed in backticks: +`_`+. ==== Functions and methods @@ -5640,6 +5739,7 @@ dependencies { } } ---- + <1> Declare dependency on `package://example.com/birds@1.0.0` with simple name "birds". These dependencies can then be imported by their simple name. @@ -5655,6 +5755,7 @@ pigeon: Bird = new { name = "Pigeon" } ---- + <1> Dependency notation; imports path `/Bird.pkl` within dependency `package://example.com/birds@1.0.0` NOTE: Internally, Pkl assigns URI scheme `projectpackage` to project dependencies imported using dependency notation. @@ -5697,6 +5798,7 @@ package { packageZipUrl = "https://example.com/\(name)/\(name)@\(version).zip" // <4> } ---- + <1> The display name of the package.For display purposes only. <2> The package URI, without the version part. <3> The version of the package. @@ -5735,6 +5837,7 @@ package { packageZipUrl = "https://example.com/birds@\(version).zip" } ---- + <1> Specify relative project `../fruit` as a dependency. .fruit/PklProject @@ -5773,6 +5876,7 @@ $ pkl eval --external-resource-reader = --external- ---- External readers may also be configured in a <> `PklProject` file. + [source,{pkl}] ---- evaluatorSettings { @@ -5816,6 +5920,7 @@ email = "pigeon@example.com" In this example, the external reader may provide both `ldap:` and `ldaps:` schemes. To support both schemes during evaluation, both would need to be registered explicitly: + [source,text] ---- $ pkl eval --external-resource-reader ldap=pkl-ldap --external-resource-reader ldaps=pkl-ldap @@ -5842,3 +5947,175 @@ NOTE: To effectively mirror packages from pkg.pkl-lang.org, there must be two re NOTE: Pkl does not provide any tooling to run a mirror server. To fully set up mirroring, an HTTP(s) server will need be running, and which mirrors the same assets byte-for-byte. + +[[references]] +=== References + +[WARNING] +.Experimenal Feature +==== +The API and semantics of references are subject to change in a future release. +The Pkl team is soliciting feedback from authors of libraries considering adopting references. +For questions and feedback, please reach out via https://github.com/apple/pkl/discussions[GitHub Discussions] or https://github.com/apple/pkl/issues/new[create an issue]. +==== + +pkldoc:Reference[pkl:ref]s (provided by the `pkl.ref` module) are an advanced API design tool that serves use cases where configuration must refer to values that are not known at evaluation time. +This is common in task-based execution systems like CI platforms and Infrastructure as Code (IaC) tools. +Today, many such systems encode this kind of reference using strings containing an expression language; this approach makes it difficult to write and debug such references, as target systems may not know where in the Pkl source an error occurred. +`Reference` provides this functionality in-language, so users benefit from evaluation-time type checking with accurate error messages and editor support for code completion. + +IMPORTANT: In general, a library that makes use of references should handle all creation of references as part of its API; end-users of the library should not create references directly. +In most cases, end users should never need to import the `pkl:ref` module. + +An instance of `Reference` consist of four parts: + +* Domain - An instance of a subclass of `pkl.ref#Domain`, corresponding to the `D` type argument. +A domain determines which `Reference` instances are compatible and how they are rendered as strings. +The domain of a `Reference` may be retrieved using its `getDomain()` method. +* Data - An arbitrary value that may contain domain-specific information about the referenced value. +The data of a `Reference` may be retrieved using its `getData()` method. +* Path - A `List` of `ref.Access` values indicating how the reference was accessed (by property or subscript). +The path of a `Reference` may be retrieved using its `getPath()` method. +* Referent type - The type of the value that the reference refers to, corresponding to the `T` type argument. + +The key feature of a `Reference` is that it inherits properties and subscript behavior from its referent type. +When a reference is accessed, either via qualified property access (`.`) or subscript (`[]`), a new reference is returned: + +* The new reference shares its domain and data with the original reference. +* The new reference's path extends the original reference's path, adding a new `ref.Access` instance describing the accessed property name or subscript key. +* The new reference's referent type is the type of the accessed property or subscript value of the original referent type. +Any type constraints within the new referent type are erased and type constraints are not allowed in the referent (second) type argument of any Reference type annotations. + +There are some restrictions on properties that may be referenced; attempting to reference these properties will fail: + +* Properties marked `external` or `local` +* All properties of `external` classes +* The `default` property of `Listing`, `Mapping`, and `Dynamic` +* Properties originally defined in `external` classes (this only includes `Module.output`) + +pkldoc:Reference[pkl:ref] instances are created using the constructor method pkldoc:#Reference()[pkl:ref]. +This method accepts three parameters: + +* `domain` - An instance of a `ref.Domain` subclass; its type becomes the `D` type argument of the resulting `Reference` instance. +* `class` - A `Class` instance that will be the referent type of the returned `Reference`; it is the `T` type argument of the resulting instance. +* `data` - A domain-specific value used to identify the root or context of the reference. + +[NOTE] +==== +It is only possible to construct new references with a referent type that is a simple (non-generic) class type. +To obtain a reference to values of other types (generic, typealias, nullable, union, etc.), libraries must wrap those types in a class: + +[source,{pkl}%parsed] +---- +import "pkl:ref" + +class TypeHolder { + $: *Listing? | Mapping // <1> +} + +local r: ref.Reference? | Mapping> = + ref.Reference(myDomain, TypeHolder, null).$ // <2> +---- +<1> Define a "holder" class with a property of the desired type. +<2> Access the `$` property of the reference to get a reference to the desired non-class type. +==== + +Here's a realistic example of how references might be used: + +[source,pkl] +.Workflow.pkl +---- +import "pkl:ref" + +class Domain extends ref.Domain { // <1> + function renderReference(reference: ref.Reference): String = + if (reference.getData() is Task) + let (path = reference.getPath().map((access) -> access.property ?? access.key.toString())) + "${\(reference.getData().name)/\(path.join("/"))}" + else + throw("Domain can only render Reference(getData() is Task) value") +} + +typealias Ref = ref.Reference // <2> + +abstract class Task { // <3> + name: String +} + +class TaskA extends Task { + input1: String | Ref // <4> + input2: Int | Ref + + hidden fixed outputs: Ref = ref.Reference(new Domain {}, TaskAOutputs, this) // <5> +} + +abstract class TaskAOutputs { // <6> + foo: String + bar: Int +} + +class TaskB extends Task { + inputX: String | Ref + inputY: Int | Ref +} + +tasks: Listing(isDistinctBy((it) -> it.name)) // <7> + +output { + renderer { + converters { + [ref.Reference] = (it) -> it.toString() // <8> + } + } +} +---- +<1> Define a `ref.Domain` subclass that determines how `Reference` instances are rendered as strings. +<2> For convenience, create a type alias for references of this domain. +<3> Create a new base class that can be referenced. `Task` instances will be identified by their `name` value. +<4> In a specific `Task` subclass, input values accept both literal values and references to the same type. +<5> `Task` subclasses that provide output values define an `outputs` property where the referent type is `TaskAOutputs`. +<6> The `TaskAOutputs` class defines the possible runtime outputs of `TaskA` tasks. It is `abstract` and never instantiated, only used for producing references. +<7> Users provide `Task` instances with unique `name` values. +<8> Delegate to `Domain` for rendering `Reference` values. + +[source,pkl] +.myWorkflow.pkl +---- +amends "Workflow.pkl" + +local doA: TaskA = new { // <1> + name = "doA" + input1 = "hi" + input2 = 123 +} + +local doB: TaskB = new { + name = "doB" + inputX = doA.outputs.foo // <2> + inputY = doA.outputs.bar +} + +tasks { + doA + doB +} +---- +<1> Instantiate a `TaskA` instance with only concrete values as inputs. +<2> Instantiate a `TaskB` instance with both inputs derived from the outputs of `doA`. + +The result of evaluating `myWorkflow.pkl` is +[source,pkl] +---- +tasks { + new { + name = "doA" + input1 = "hi" + input2 = 123 + } + new { + name = "doB" + inputX = "${doA/foo}" + inputY = "${doA/bar}" + } +} +---- diff --git a/pkl-core/src/main/java/org/pkl/core/PClass.java b/pkl-core/src/main/java/org/pkl/core/PClass.java index 309f2a581..5962d80e9 100644 --- a/pkl-core/src/main/java/org/pkl/core/PClass.java +++ b/pkl-core/src/main/java/org/pkl/core/PClass.java @@ -27,6 +27,7 @@ public final class PClass extends Member implements Value { private final List typeParameters; private final Map properties; private final Map methods; + private final @Nullable PClass moduleClass; private @Nullable PType supertype; private @Nullable PClass superclass; @@ -42,12 +43,14 @@ public final class PClass extends Member implements Value { PClassInfo classInfo, List typeParameters, Map properties, - Map methods) { + Map methods, + @Nullable PClass moduleClass) { super(docComment, sourceLocation, modifiers, annotations, classInfo.getSimpleName()); this.classInfo = classInfo; this.typeParameters = typeParameters; this.properties = properties; this.methods = methods; + this.moduleClass = moduleClass; } public void initSupertype(PType supertype, PClass superclass) { @@ -57,7 +60,7 @@ public final class PClass extends Member implements Value { /** * Returns the name of the module that this class is declared in. Note that a module name is not - * guaranteed to be unique, especially if it not declared but inferred from the module URI. + * guaranteed to be unique, especially if it is not declared but inferred from the module URI. */ public String getModuleName() { return classInfo.getModuleName(); @@ -119,6 +122,11 @@ public final class PClass extends Member implements Value { return allMethods; } + /** Returns the class's containing module's class, or this class if it is a module class. */ + public PClass getModuleClass() { + return moduleClass != null ? moduleClass : this; + } + @Override public void accept(ValueVisitor visitor) { visitor.visitClass(this); @@ -138,6 +146,10 @@ public final class PClass extends Member implements Value { return getDisplayName(); } + public boolean isSubclassOf(PClass other) { + return this == other || getSuperclass() != null && getSuperclass().isSubclassOf(other); + } + public abstract static class ClassMember extends Member { @Serial private static final long serialVersionUID = 0L; diff --git a/pkl-core/src/main/java/org/pkl/core/PClassInfo.java b/pkl-core/src/main/java/org/pkl/core/PClassInfo.java index 4ef1420bb..abc9b40dd 100644 --- a/pkl-core/src/main/java/org/pkl/core/PClassInfo.java +++ b/pkl-core/src/main/java/org/pkl/core/PClassInfo.java @@ -39,6 +39,7 @@ public final class PClassInfo implements Serializable { public static final URI pklSemverUri = URI.create("pkl:semver"); public static final URI pklSettingsUri = URI.create("pkl:settings"); public static final URI pklProjectUri = URI.create("pkl:Project"); + public static final URI pklRefUri = URI.create("pkl:ref"); public static final PClassInfo Any = pklBaseClassInfo("Any", Void.class); public static final PClassInfo Null = pklBaseClassInfo("Null", PNull.class); @@ -82,9 +83,11 @@ public final class PClassInfo implements Serializable { public static final PClassInfo Version = new PClassInfo<>("pkl.semver", "Version", PObject.class, pklSemverUri); public static final PClassInfo Project = - new PClassInfo<>("pkl.Project", "ModuleClass", PObject.class, pklProjectUri); + new PClassInfo<>("pkl.Project", MODULE_CLASS_NAME, PObject.class, pklProjectUri); public static final PClassInfo Settings = - new PClassInfo<>("pkl.settings", "ModuleClass", PObject.class, pklSettingsUri); + new PClassInfo<>("pkl.settings", MODULE_CLASS_NAME, PObject.class, pklSettingsUri); + public static final PClassInfo Reference = + new PClassInfo<>("pkl.ref", "Reference", Reference.class, pklRefUri); public static final PClassInfo Unavailable = new PClassInfo<>("unavailable", "unavailable", Object.class, URI.create("pkl:unavailable")); diff --git a/pkl-core/src/main/java/org/pkl/core/PType.java b/pkl-core/src/main/java/org/pkl/core/PType.java index a69dc3321..4a912156c 100644 --- a/pkl-core/src/main/java/org/pkl/core/PType.java +++ b/pkl-core/src/main/java/org/pkl/core/PType.java @@ -1,5 +1,5 @@ /* - * Copyright Β© 2024 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. @@ -18,6 +18,7 @@ package org.pkl.core; import java.io.Serial; import java.io.Serializable; import java.util.*; +import java.util.stream.Collectors; /** A Pkl type as used in type annotations. */ public abstract class PType implements Serializable { @@ -27,18 +28,33 @@ public abstract class PType implements Serializable { public static final PType UNKNOWN = new PType() { @Serial private static final long serialVersionUID = 0L; + + @Override + public String toString() { + return "unknown"; + } }; /** The bottom type. */ public static final PType NOTHING = new PType() { @Serial private static final long serialVersionUID = 0L; + + @Override + public String toString() { + return "nothing"; + } }; /** The type of the enclosing module. */ public static final PType MODULE = new PType() { @Serial private static final long serialVersionUID = 0L; + + @Override + public String toString() { + return "module"; + } }; private PType() {} @@ -59,6 +75,11 @@ public abstract class PType implements Serializable { public String getLiteral() { return literal; } + + @Override + public String toString() { + return ValueFormatter.basic().formatStringValue(literal, ""); + } } public static final class Class extends PType { @@ -92,6 +113,18 @@ public abstract class PType implements Serializable { public List getTypeArguments() { return typeArguments; } + + @Override + public String toString() { + var result = pClass.getDisplayName(); + if (!typeArguments.isEmpty()) { + result += + "<" + + typeArguments.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ">"; + } + return result; + } } public static final class Nullable extends PType { @@ -106,6 +139,13 @@ public abstract class PType implements Serializable { public PType getBaseType() { return baseType; } + + @Override + public String toString() { + return baseType instanceof Function || baseType instanceof Union + ? "(" + baseType + ")?" + : baseType + "?"; + } } public static final class Constrained extends PType { @@ -126,6 +166,16 @@ public abstract class PType implements Serializable { public List getConstraints() { return constraints; } + + @Override + public String toString() { + return (baseType instanceof Function || baseType instanceof Union + ? "(" + baseType + ")" + : baseType) + + "(" + + String.join(", ", constraints) + + ")"; + } } public static final class Alias extends PType { @@ -161,6 +211,18 @@ public abstract class PType implements Serializable { public PType getAliasedType() { return aliasedType; } + + @Override + public String toString() { + var result = typeAlias.getDisplayName(); + if (!typeArguments.isEmpty()) { + result += + "<" + + typeArguments.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ">"; + } + return result; + } } public static final class Function extends PType { @@ -181,6 +243,14 @@ public abstract class PType implements Serializable { public PType getReturnType() { return returnType; } + + @Override + public String toString() { + return "(" + + parameterTypes.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ") -> " + + returnType; + } } public static final class Union extends PType { @@ -195,6 +265,11 @@ public abstract class PType implements Serializable { public List getElementTypes() { return elementTypes; } + + @Override + public String toString() { + return elementTypes.stream().map(Object::toString).collect(Collectors.joining(" | ")); + } } public static final class TypeVariable extends PType { @@ -213,5 +288,10 @@ public abstract class PType implements Serializable { public TypeParameter getTypeParameter() { return typeParameter; } + + @Override + public String toString() { + return typeParameter.getName(); + } } } diff --git a/pkl-core/src/main/java/org/pkl/core/PropertiesRenderer.java b/pkl-core/src/main/java/org/pkl/core/PropertiesRenderer.java index 23fa1fe82..52e29d0fd 100644 --- a/pkl-core/src/main/java/org/pkl/core/PropertiesRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/PropertiesRenderer.java @@ -177,6 +177,13 @@ final class PropertiesRenderer implements ValueRenderer { "Values of type `Regex` cannot be rendered as Properties. Value: %s", value)); } + @Override + public String convertReference(Reference value) { + throw new RendererException( + String.format( + "Values of type `Reference` cannot be rendered as Properties. Value: %s", value)); + } + private void doVisitMap(@Nullable String keyPrefix, Map map) { for (Map.Entry entry : map.entrySet()) { doVisitKeyAndValue(keyPrefix, entry.getKey(), entry.getValue()); diff --git a/pkl-core/src/main/java/org/pkl/core/Reference.java b/pkl-core/src/main/java/org/pkl/core/Reference.java new file mode 100644 index 000000000..f0c3b5d42 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Reference.java @@ -0,0 +1,101 @@ +/* + * Copyright Β© 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. + * You may obtain a copy of the License at + * + * https://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 org.pkl.core; + +import java.io.Serial; +import java.util.List; + +public class Reference implements Value { + @Serial private static final long serialVersionUID = 0L; + + private final Composite domain; + private final Object data; + private final List path; + private final PType referentType; + + /** Constructs a reference. */ + public Reference(Composite domain, Object data, List path, PType referentType) { + this.domain = domain; + this.data = data; + this.path = path; + this.referentType = referentType; + } + + /** Returns the domain object of this reference. */ + public Composite getDomain() { + return domain; + } + + /** Returns the data object of this reference. */ + public Object getData() { + return data; + } + + /** + * Returns the access path of this reference. + * + *

All elements are exported {@code pkl.ref#Access} instances. + */ + public List getPath() { + return path; + } + + /** Returns the referent type of this reference. */ + public PType getReferentType() { + return referentType; + } + + @Override + public void accept(ValueVisitor visitor) { + visitor.visitReference(this); + } + + @Override + public T accept(ValueConverter converter) { + return converter.convertReference(this); + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Reference reference)) { + return false; + } + + return domain.equals(reference.domain) + && data.equals(reference.data) + && path.equals(reference.path) + && referentType.equals(reference.referentType); + } + + @Override + public PClassInfo getClassInfo() { + return PClassInfo.Reference; + } + + @Override + public int hashCode() { + int result = domain.hashCode(); + result = 31 * result + data.hashCode(); + result = 31 * result + path.hashCode(); + result = 31 * result + referentType.hashCode(); + return result; + } + + @Override + public String toString() { + return super.toString(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/TypeAlias.java b/pkl-core/src/main/java/org/pkl/core/TypeAlias.java index 77f7a4044..e0171da69 100644 --- a/pkl-core/src/main/java/org/pkl/core/TypeAlias.java +++ b/pkl-core/src/main/java/org/pkl/core/TypeAlias.java @@ -28,6 +28,7 @@ public final class TypeAlias extends Member implements Value { private final String moduleName; private final String qualifiedName; private final List typeParameters; + private final PClass moduleClass; @LateInit private PType aliasedType; @@ -39,11 +40,13 @@ public final class TypeAlias extends Member implements Value { String simpleName, String moduleName, String qualifiedName, - List typeParameters) { + List typeParameters, + PClass moduleClass) { super(docComment, sourceLocation, modifiers, annotations, simpleName); this.moduleName = moduleName; this.qualifiedName = qualifiedName; this.typeParameters = typeParameters; + this.moduleClass = moduleClass; } public void initAliasedType(PType type) { @@ -79,6 +82,10 @@ public final class TypeAlias extends Member implements Value { return typeParameters; } + public PClass getModuleClass() { + return moduleClass; + } + /** Returns the type that this type alias stands for. */ public PType getAliasedType() { //noinspection ConstantValue diff --git a/pkl-core/src/main/java/org/pkl/core/ValueConverter.java b/pkl-core/src/main/java/org/pkl/core/ValueConverter.java index 5a4c310b6..faf129cc6 100644 --- a/pkl-core/src/main/java/org/pkl/core/ValueConverter.java +++ b/pkl-core/src/main/java/org/pkl/core/ValueConverter.java @@ -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. @@ -57,6 +57,8 @@ public interface ValueConverter { T convertRegex(Pattern value); + T convertReference(Reference value); + default T convert(Object value) { if (value instanceof Value v) { return (v.accept(this)); diff --git a/pkl-core/src/main/java/org/pkl/core/ValueVisitor.java b/pkl-core/src/main/java/org/pkl/core/ValueVisitor.java index 90d98af76..2c5bc0318 100644 --- a/pkl-core/src/main/java/org/pkl/core/ValueVisitor.java +++ b/pkl-core/src/main/java/org/pkl/core/ValueVisitor.java @@ -93,6 +93,10 @@ public interface ValueVisitor { visitDefault(value); } + default void visitReference(Reference value) { + visitDefault(value); + } + default void visit(Object value) { if (value instanceof Value v) { v.accept(this); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java index 87dcf2fba..f0259f573 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java @@ -100,6 +100,20 @@ public abstract class SubscriptNode extends BinaryExpressionNode { return readMember(dynamic, key, callNode); } + @Specialization + protected VmReference eval(VmReference reference, Object key) { + var result = reference.withSubscriptAccess(key); + if (result != null) return result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError( + "operatorNotDefined2", getShortName(), reference.exportType(), VmUtils.getClass(key)) + .withProgramValue("Left operand", reference) + .withProgramValue("Right operand", key) + .build(); + } + @Specialization protected long eval(VmBytes receiver, long index) { if (index < 0 || index >= receiver.getLength()) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java index 6ecc0d22b..db8014103 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java @@ -61,6 +61,18 @@ public abstract class ReadPropertyNode extends ExpressionNode { this(sourceSection, propertyName, MemberLookupMode.EXPLICIT_RECEIVER, false); } + @Specialization + protected VmReference evalReference(VmReference receiver) { + assert lookupMode == MemberLookupMode.EXPLICIT_RECEIVER; + var result = receiver.withPropertyAccess(propertyName); + if (result != null) return result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("cannotFindPropertyInReference", propertyName, receiver.exportType()) + .build(); + } + // This method effectively covers `VmObject receiver` but is implemented in a more // efficient way. See: // https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/ToStringNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/ToStringNode.java index 60038979e..16c92de6d 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/internal/ToStringNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/ToStringNode.java @@ -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. @@ -20,6 +20,7 @@ import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Fallback; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.MemberLookupMode; @@ -62,10 +63,18 @@ public abstract class ToStringNode extends UnaryExpressionNode { VmTyped value, @Cached(value = "createInvokeNode()", neverDefault = true) InvokeMethodVirtualNode invokeNode) { - return (String) invokeNode.executeWith(frame, value, value.getVmClass()); } + @Specialization + protected String evalReference( + VirtualFrame frame, + VmReference value, + @Cached(value = "createReferenceCallNode(value)", neverDefault = true) + DirectCallNode callNode) { + return (String) callNode.call(value, value.getVmClass().getPrototype()); + } + @Fallback @Override @TruffleBoundary @@ -74,7 +83,6 @@ public abstract class ToStringNode extends UnaryExpressionNode { } protected InvokeMethodVirtualNode createInvokeNode() { - //noinspection ConstantConditions return InvokeMethodVirtualNodeGen.create( sourceSection, Identifier.TO_STRING, @@ -83,4 +91,10 @@ public abstract class ToStringNode extends UnaryExpressionNode { null, null); } + + protected DirectCallNode createReferenceCallNode(VmReference reference) { + var toStringMethod = reference.getVmClass().getDeclaredMethod(Identifier.TO_STRING); + assert toStringMethod != null; + return DirectCallNode.create(toStringMethod.getCallTarget()); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java index 442b82ba3..c823074bd 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java @@ -2127,6 +2127,125 @@ public abstract class TypeNode extends PklNode { } } + public abstract static class ReferenceTypeNode extends ObjectSlotTypeNode { + @Child private TypeNode domainTypeNode; + @Child private TypeNode referentTypeNode; + @Child private ExpressionNode getModuleNode; + + public ReferenceTypeNode( + SourceSection sourceSection, TypeNode domainTypeNode, TypeNode referentTypeNode) { + super(sourceSection); + this.domainTypeNode = domainTypeNode; + this.referentTypeNode = referentTypeNode; + this.getModuleNode = new GetModuleNode(sourceSection); + validateTypeArguments(sourceSection); + } + + @Specialization + protected Object eval(VirtualFrame frame, VmReference value) { + if (domainTypeNode.isNoopTypeCheck() && referentTypeNode.isNoopTypeCheck()) { + return value; + } + + try { + domainTypeNode.execute(frame, value.getDomain()); + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw new VmTypeMismatchException.Reference( + sourceSection, + value, + TypeNode.export(domainTypeNode), + TypeNode.export(referentTypeNode)); + } + + var module = (VmTyped) getModuleNode.executeGeneric(frame); + return doEval(value, module); + } + + @TruffleBoundary + private Object doEval(VmReference value, VmTyped module) { + var referentType = TypeNode.export(referentTypeNode); + if (value.referentTypeIsSubtypeOf(referentType, module.getVmClass().export())) { + return value; + } + + throw new VmTypeMismatchException.Reference( + sourceSection, value, TypeNode.export(domainTypeNode), referentType); + } + + public void validateTypeArguments(@Nullable SourceSection aliasSourceSection) { + // constraints may not be used in Reference type annotation referents + // walk the type and throw if any part of the referent is constrained + + // TODO improve error message when this type node and/or referent constraint are behind type + // aliases + referentTypeNode.acceptTypeNode( + true, + (typeNode) -> { + if (typeNode instanceof ConstrainedTypeNode) { + CompilerDirectives.transferToInterpreter(); + var err = + exceptionBuilder().evalError("invalidReferenceTypeAnnotationWithConstraint"); + if (aliasSourceSection != null) { + err.withSourceSection(aliasSourceSection); + } + throw err.build(); + } + return true; + }); + } + + @Fallback + protected Object fallback(Object value) { + throw typeMismatch(value, RefModule.getReferenceClass()); + } + + @Override + protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) { + if (visitTypeArguments) + return consumer.accept(this) + && consumer.accept(domainTypeNode) + && consumer.accept(referentTypeNode); + return consumer.accept(this); + } + + @Override + public VmClass getVmClass() { + return RefModule.getReferenceClass(); + } + + @Override + public VmList getTypeArgumentMirrors() { + return VmList.of(domainTypeNode.getMirror(), referentTypeNode.getMirror()); + } + + @Override + protected boolean doIsEquivalentTo(TypeNode other) { + if (!(other instanceof ReferenceTypeNode referenceTypeNode)) { + return false; + } + return referentTypeNode.isEquivalentTo(referenceTypeNode.referentTypeNode); + } + + @Override + public boolean isNoopTypeCheck() { + return domainTypeNode.isNoopTypeCheck() && referentTypeNode.isNoopTypeCheck(); + } + + @Override + protected PType doExport() { + return new PType.Class( + RefModule.getReferenceClass().export(), + domainTypeNode.doExport(), + referentTypeNode.doExport()); + } + + @Override + protected boolean isParametric() { + return true; + } + } + public static final class PairTypeNode extends ObjectSlotTypeNode { @Child private TypeNode firstTypeNode; @Child private TypeNode secondTypeNode; @@ -2584,7 +2703,7 @@ public abstract class TypeNode extends PklNode { this.typeAlias = typeAlias; this.typeArgumentNodes = typeArgumentNodes; - aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes); + aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes, sourceSection); } public TypeNode getAliasedTypeNode() { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java index cb1877fb7..1665cad49 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java @@ -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. @@ -287,6 +287,13 @@ public abstract class UnresolvedTypeNode extends PklNode { return new VarArgsTypeNode(sourceSection, typeArgumentNodes[0].execute(frame)); } + if (clazz.isReferenceClass()) { + return ReferenceTypeNodeGen.create( + sourceSection, + typeArgumentNodes[0].execute(frame), + typeArgumentNodes[1].execute(frame)); + } + throw exceptionBuilder() .evalError("notAParameterizableClass", clazz.getDisplayName()) .withSourceSection(typeArgumentNodes[0].sourceSection) diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/VmTypeMismatchException.java b/pkl-core/src/main/java/org/pkl/core/ast/type/VmTypeMismatchException.java index 59ff49640..4443ea261 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/VmTypeMismatchException.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/VmTypeMismatchException.java @@ -23,6 +23,7 @@ import com.oracle.truffle.api.source.SourceSection; import java.util.*; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; +import org.pkl.core.PType; import org.pkl.core.StackFrame; import org.pkl.core.ValueFormatter; import org.pkl.core.ast.type.TypeNode.UnionTypeNode; @@ -333,4 +334,44 @@ public abstract class VmTypeMismatchException extends ControlFlowException { return false; } } + + public static final class Reference extends VmTypeMismatchException { + + private final PType expectedDomainType; + private final PType expectedReferentType; + + public Reference( + SourceSection sourceSection, + VmReference actualValue, + PType expectedDomainType, + PType expectedReferentType) { + super(sourceSection, actualValue); + this.expectedDomainType = expectedDomainType; + this.expectedReferentType = expectedReferentType; + } + + @Override + public void buildMessage( + AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { + builder + .append( + ErrorMessages.createIndented( + "typeMismatch", + indent, + new PType.Class( + RefModule.getReferenceClass().export(), + expectedDomainType, + expectedReferentType), + ((VmReference) actualValue).exportType())) + .append("\n") + .append(indent) + .append("Value: ") + .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); + } + + @Override + protected Boolean hasHint() { + return false; + } + } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java index 78cafda13..9009ccf17 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java @@ -165,6 +165,10 @@ public final class Identifier implements Comparable { public static final Identifier ILLEGAL = get("`"); + // members of pkl.ref + public static final Identifier PROPERTY = get("property"); + public static final Identifier KEY = get("key"); + // common in lambdas etc public static final Identifier IT = get("it"); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java index 590ecb580..3467a381c 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java @@ -104,6 +104,8 @@ public final class ModuleCache { return PlatformModule.getModule(); case "project": return ProjectModule.getModule(); + case "ref": + return RefModule.getModule(); case "reflect": return ReflectModule.getModule(); case "release": diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/RefModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/RefModule.java new file mode 100644 index 000000000..915b8fb12 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/RefModule.java @@ -0,0 +1,53 @@ +/* + * Copyright Β© 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. + * You may obtain a copy of the License at + * + * https://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 org.pkl.core.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.net.URI; + +public class RefModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:ref"), instance); + } + + public static VmClass getReferenceClass() { + return ReferenceClass.instance; + } + + public static VmClass getAccessClass() { + return AccessClass.instance; + } + + private static final class ReferenceClass { + static final VmClass instance = loadClass("Reference"); + } + + private static final class AccessClass { + static final VmClass instance = loadClass("Access"); + } + + @TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } + + public static VmTyped getModule() { + return instance; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java index 2cad2146e..68614aa86 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java @@ -445,6 +445,11 @@ public final class VmClass extends VmValue { return isClass(BaseModule.getVarArgsClass(), "pkl.base#VarArgs"); } + @Idempotent + public boolean isReferenceClass() { + return isClass(RefModule.getReferenceClass(), "pkl.ref#Reference"); + } + private boolean isClass(@Nullable VmClass clazz, String qualifiedClassName) { // may be null during evaluation of base module return clazz != null ? this == clazz : getQualifiedName().equals(qualifiedClassName); @@ -621,49 +626,62 @@ public final class VmClass extends VmValue { public PClass export() { synchronized (pClassLock) { //noinspection ConstantValue - if (__pClass == null) { - var exportedAnnotations = new ArrayList(); - var properties = - CollectionUtils.newLinkedHashMap( - EconomicMaps.size(declaredProperties)); - var methods = - CollectionUtils.newLinkedHashMap( - EconomicMaps.size(declaredMethods)); + if (__pClass != null) { + return __pClass; + } - // set pClass before exporting class members to prevent - // infinite recursion in case of cyclic references - __pClass = - new PClass( - VmUtils.exportDocComment(docComment), - new SourceLocation(headerSection.getStartLine(), sourceSection.getEndLine()), - VmModifier.export(modifiers, true), - exportedAnnotations, - classInfo, - typeParameters, - properties, - methods); + // if this is not a module class, export this class's module's class first to break the cycle + PClass moduleClass = null; + if (!classInfo.isModuleClass()) { + moduleClass = getModule().getVmClass().export(); + } + // then if the cached value is still null, initialize it + if (__pClass != null) { + return __pClass; + } - for (var parameter : typeParameters) { - parameter.initOwner(__pClass); + var exportedAnnotations = new ArrayList(); + var properties = + CollectionUtils.newLinkedHashMap( + EconomicMaps.size(declaredProperties)); + var methods = + CollectionUtils.newLinkedHashMap( + EconomicMaps.size(declaredMethods)); + + // set pClass before exporting class members to prevent + // infinite recursion in case of cyclic references + __pClass = + new PClass( + VmUtils.exportDocComment(docComment), + new SourceLocation(headerSection.getStartLine(), sourceSection.getEndLine()), + VmModifier.export(modifiers, true), + exportedAnnotations, + classInfo, + typeParameters, + properties, + methods, + moduleClass); + + for (var parameter : typeParameters) { + parameter.initOwner(__pClass); + } + + if (supertypeNode != null) { + assert superclass != null; + __pClass.initSupertype(TypeNode.export(supertypeNode), superclass.export()); + } + + VmUtils.exportAnnotations(annotations, exportedAnnotations); + + for (var property : EconomicMaps.getValues(declaredProperties)) { + if (isClassPropertyDefinition(property)) { + properties.put(property.getName().toString(), property.export(__pClass)); } + } - if (supertypeNode != null) { - assert superclass != null; - __pClass.initSupertype(TypeNode.export(supertypeNode), superclass.export()); - } - - VmUtils.exportAnnotations(annotations, exportedAnnotations); - - for (var property : EconomicMaps.getValues(declaredProperties)) { - if (isClassPropertyDefinition(property)) { - properties.put(property.getName().toString(), property.export(__pClass)); - } - } - - for (var method : EconomicMaps.getValues(declaredMethods)) { - if (method.isLocal()) continue; - methods.put(method.getName().toString(), method.export(__pClass)); - } + for (var method : EconomicMaps.getValues(declaredMethods)) { + if (method.isLocal()) continue; + methods.put(method.getName().toString(), method.export(__pClass)); } return __pClass; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectBuilder.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectBuilder.java index f2cc06d92..3b3b45049 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright Β© 2024 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. @@ -87,4 +87,9 @@ public final class VmObjectBuilder { members, elementCount); } + + public VmTyped toTyped(VmClass clazz) { + return new VmTyped( + VmUtils.createEmptyMaterializedFrame(), clazz.getPrototype(), clazz, members); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java index 0c8cbde4e..5818bcd22 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java @@ -324,6 +324,22 @@ public class VmPklBinaryEncoder extends AbstractRenderer { } } + @Override + public void visitReference(VmReference value) { + try { + packer.packArrayHeader(4); + packCode(PklBinaryCode.REFERENCE); + visit(value.getDomain()); + visit(value.getData()); + packer.packArrayHeader(value.getPath().size()); + for (var access : value.getPath()) { + visit(access); + } + } catch (IOException e) { + throw PklBugException.unreachableCode(); + } + } + @Override protected void visitEntryKey(Object key, boolean isFirst) { visit(key); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmReference.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmReference.java new file mode 100644 index 000000000..ca15dac25 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmReference.java @@ -0,0 +1,473 @@ +/* + * Copyright Β© 2025-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. + * You may obtain a copy of the License at + * + * https://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 org.pkl.core.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; +import org.pkl.core.Composite; +import org.pkl.core.PClass; +import org.pkl.core.PClassInfo; +import org.pkl.core.PType; +import org.pkl.core.Reference; +import org.pkl.core.TypeAlias; +import org.pkl.core.util.paguro.RrbTree; +import org.pkl.core.util.paguro.RrbTree.ImRrbt; + +public final class VmReference extends VmValue { + + private final VmTyped domain; + private final Object data; + private final ImRrbt path; + // candidate types can only be: PType.Class, PType.Alias (only preservedAliasTypes), + // PType.StringLiteral, PType.UNKNOWN, or PType.Union (containing only the previous; flattened) + private final PType referentType; + + private boolean forced = false; + + private static VmTyped newAccess(@Nullable String property, @Nullable Object key) { + return new VmObjectBuilder() + .addProperty(Identifier.PROPERTY, property == null ? VmNull.withoutDefault() : property) + .addProperty(Identifier.KEY, key == null ? VmNull.withoutDefault() : key) + .toTyped(RefModule.getAccessClass()); + } + + @TruffleBoundary + public VmReference(VmTyped domain, VmClass clazz, Object data) { + this( + domain, + data, + RrbTree.empty(), + normalizeTypes(new PType.Class(clazz.export()), clazz.getModule().getVmClass().export())); + } + + public VmReference(VmTyped domain, Object data, ImRrbt path, PType referentType) { + this.domain = domain; + this.data = data; + this.referentType = referentType; + this.path = path; + } + + public VmTyped getDomain() { + return domain; + } + + public Object getData() { + return data; + } + + public List getPath() { + return path; + } + + public PType getReferentType() { + return referentType; + } + + // simplifies a type by: + // * erasing constraints + // * transforming T? into T|Null + // * dereferencing aliases (except for well-known stdlib alias types) + // * flattening unions + // * when moduleClass is supplied, replace PType.MODULE with appropriate PType.Class + // * drop PType.Function and PType.TypeVariable + private static PType normalizeTypes(PType type, PClass moduleClass) { + var types = new HashSet(); + normalizeTypes(type, moduleClass, types); + return minimizeTypes(types); + } + + private static PType minimizeTypes(Set types) { + if (types.size() == 1) return types.iterator().next(); + // optimization: unknown allows all references, erase all candidates to only unknown + if (types.contains(PType.UNKNOWN)) return PType.UNKNOWN; + // optimization: All allows all references, erase all candidates to only All + if (containsClass(types, BaseModule.getAnyClass().export())) + return new PType.Class(BaseModule.getAnyClass().export()); + var typesList = new ArrayList<>(types); + typesList.sort(Comparator.comparing(Object::toString)); + return new PType.Union(typesList); + } + + private static void normalizeTypes(PType type, PClass moduleClass, Set result) { + if (type == PType.UNKNOWN || type == PType.NOTHING || type instanceof PType.StringLiteral) { + result.add(type); + } else if (type instanceof PType.Class clazz) { + if (clazz.getTypeArguments().isEmpty()) { + // if a generic type is used without type arguments, it needs to be normalized so all args + // are unknown; i.e. with bare List/Map/etc. type annotations (via FinalClassTypeNode). + var typeParameterCount = clazz.getPClass().getTypeParameters().size(); + result.add( + typeParameterCount == 0 + ? clazz + : new PType.Class( + clazz.getPClass(), Collections.nCopies(typeParameterCount, PType.UNKNOWN))); + } else { + var typeArgs = new ArrayList(clazz.getTypeArguments().size()); + for (var arg : clazz.getTypeArguments()) { + typeArgs.add(normalizeTypes(arg, moduleClass)); + } + result.add(new PType.Class(clazz.getPClass(), typeArgs)); + } + } else if (type instanceof PType.Nullable nullable) { + normalizeTypes(nullable.getBaseType(), moduleClass, result); + result.add(new PType.Class(BaseModule.getNullClass().export())); + } else if (type instanceof PType.Constrained constrained) { + normalizeTypes(constrained.getBaseType(), moduleClass, result); + } else if (type instanceof PType.Alias alias) { + if (isPreservedTypeAlias(alias.getTypeAlias())) { + result.add(alias); + } else { + normalizeTypes(alias.getAliasedType(), alias.getTypeAlias().getModuleClass(), result); + } + } else if (type instanceof PType.Union union) { + for (var t : union.getElementTypes()) { + normalizeTypes(t, moduleClass, result); + } + } else if (type == PType.MODULE) { + result.add(new PType.Class(moduleClass)); + } + } + + private static Iterable iterateTypes(PType t) { + if (t instanceof PType.Union union) return union.getElementTypes(); + return Collections.singleton(t); + } + + public @Nullable VmReference withPropertyAccess(Identifier property) { + var propString = property.toString(); + return withAccess( + (t, candidates) -> getCandidatePropertyType(t, propString, candidates), + () -> newAccess(property.toString(), null)); + } + + public @Nullable VmReference withSubscriptAccess(Object key) { + return withAccess( + (t, candidates) -> getCandidateSubscriptType(t, key, candidates), + () -> newAccess(null, key)); + } + + @TruffleBoundary + private @Nullable VmReference withAccess( + BiConsumer> checkCandidate, Supplier makeAccess) { + Set candidates = new HashSet<>(); + for (var t : iterateTypes(referentType)) { + checkCandidate.accept(t, candidates); + } + if (candidates.isEmpty()) { + return null; // no valid access found + } + return new VmReference(domain, data, path.append(makeAccess.get()), minimizeTypes(candidates)); + } + + @SuppressWarnings("DuplicatedCode") + private static void getCandidatePropertyType(PType type, String property, Set result) { + if (type == PType.UNKNOWN) { + result.add(type); + return; + } + // restriction: only class types can have their properties referenced + if (!(type instanceof PType.Class clazz)) return; + // restriction: cannot reference properties of external classes + if (clazz.getPClass().isExternal()) return; + if (clazz.getPClass().getInfo() == PClassInfo.Dynamic) { + // restriction: cannot reference Dynamic.default + if (!property.equals("default")) result.add(PType.UNKNOWN); + return; + } + // restriction: cannot reference Listing/Mapping.default + if (clazz.getPClass().getInfo() == PClassInfo.Listing + || clazz.getPClass().getInfo() == PClassInfo.Mapping) { + return; + } + // restriction: cannot reference Module.output. + // generalized: properties originally defined in external classes; the only extant example. + // This is implemented specifically because this is the only case where an external class + // containing a property can be subclassed. + // And this can't check prop.getOwner().isExternal() because fully overriding the property with + // a new type annotation means the owner isn't Module. + if (clazz.getPClass().isSubclassOf(BaseModule.getModuleClass().export()) + && property.equals("output")) return; + + var prop = clazz.getPClass().getAllProperties().get(property); + // restriction: cannot reference external properties + if (prop == null || prop.isExternal()) { + return; + } + normalizeTypes(prop.getType(), clazz.getPClass().getModuleClass(), result); + } + + @SuppressWarnings("DuplicatedCode") + private static void getCandidateSubscriptType(PType type, Object key, Set result) { + if (type == PType.UNKNOWN) { + result.add(type); + return; + } + if (!(type instanceof PType.Class clazz)) { + return; + } + if (clazz.getPClass().getInfo() == PClassInfo.Dynamic) { + result.add(PType.UNKNOWN); + return; + } + if (clazz.getPClass().getInfo() == PClassInfo.Listing + || clazz.getPClass().getInfo() == PClassInfo.List) { + if (key instanceof Long) { + normalizeTypes(clazz.getTypeArguments().get(0), clazz.getPClass().getModuleClass(), result); + } + return; + } + if (clazz.getPClass().getInfo() == PClassInfo.Mapping + || clazz.getPClass().getInfo() == PClassInfo.Map) { + var typeArgs = clazz.getTypeArguments(); + var keyTypes = normalizeTypes(typeArgs.get(0), clazz.getPClass().getModuleClass()); + for (var kt : iterateTypes(keyTypes)) { + if (kt == PType.UNKNOWN + || (kt instanceof PType.Class klazz + && klazz.getPClass().getInfo() == PClassInfo.forValue(VmValue.export(key))) + || (kt instanceof PType.StringLiteral stringLiteral + && stringLiteral.getLiteral().equals(key))) { + normalizeTypes(typeArgs.get(1), clazz.getPClass().getModuleClass(), result); + return; + } + } + } + } + + /** + * Tells if this reference's referent type is a subtype of {@code type}. Does not check domain. + */ + public boolean referentTypeIsSubtypeOf(PType type, PClass moduleClass) { + // fast path: if referent is unknown it can match any type check + if (referentType == PType.UNKNOWN) { + return true; + } + + var checkType = normalizeTypes(type, moduleClass); + // fast path: short circuit if any referent is accepted + if (checkType == PType.UNKNOWN || isClass(checkType, BaseModule.getAnyClass().export())) { + return true; + } + // fast path: short circuit if nothing is accepted + if (checkType == PType.NOTHING) { + return false; + } + + return isSubtype(referentType, checkType); + } + + private static boolean containsClass(Set types, PClass pClass) { + for (var t : types) { + if (isClass(t, pClass)) return true; + } + return false; + } + + private static boolean isClass(PType t, PClass pClass) { + return t instanceof PType.Class clazz && clazz.getPClass() == pClass; + } + + private static boolean isSubtype(PType a, PType b) { + // checks if A is a subtype of B + // cases (A -> B) + // * A == B + // * StringLiteral -> StringLiteral: if literals are the same + // * StringLiteral -> Class: B is String + // * Char Alias -> Char Alias, StringLiteral (known single character) + // * Int Alias -> Class: B is a subtype of Number (Int|Float|Number) + // * Int Alias -> Alias + // * same alias + // * Int8 is Int16|Int32 + // * Int16 is Int32 + // * UInt8 is Int16|Int32|Uint16|UInt32|UInt + // * UInt16 is Int32|UInt32|UInt + // * UInt32 is UInt + // * Class -> Class: if same class or A is a subclass of B + // * if type args are present, must have equal number of them + // * for each pair of type args, check variance + // * invariant: A_i must be identical to B_i + // * covariant: A_i must be a subtype of B_i + // * contravariant: B_i must be a subtype of A_i + // * Union -> Union: Each elem of A must be a subtype of at least one elem of B + // * Non-union -> Union: A must be a subtype of at least one elem of B + if (a == b) return true; + + if (a instanceof PType.StringLiteral aStr) { + if (b instanceof PType.StringLiteral bStr) { + return aStr.getLiteral().equals(bStr.getLiteral()); + } else if (b instanceof PType.Class bClass) { + return bClass.getPClass() == BaseModule.getStringClass().export(); + } + } else if (a instanceof PType.Alias aAlias) { + var aa = aAlias.getTypeAlias(); + if (isIntTypeAlias(aa)) { + // special casing for stdlib Int typealiases + if (b instanceof PType.Class bClass) { + // A is an int alias, B is a Number (sub)class + return bClass.getPClass().isSubclassOf(BaseModule.getNumberClass().export()); + } else if (b instanceof PType.Alias bAlias) { + var bb = bAlias.getTypeAlias(); + if (aa == bb) { + return true; + } + if (aa == BaseModule.getInt8TypeAlias().export()) { + return bb == BaseModule.getInt16TypeAlias().export() + || bb == BaseModule.getInt32TypeAlias().export(); + } else if (aa == BaseModule.getInt16TypeAlias().export()) { + return bb == BaseModule.getInt32TypeAlias().export(); + } else if (aa == BaseModule.getUInt8TypeAlias().export()) { + return bb == BaseModule.getInt16TypeAlias().export() + || bb == BaseModule.getInt32TypeAlias().export() + || bb == BaseModule.getUInt16TypeAlias().export() + || bb == BaseModule.getUInt32TypeAlias().export() + || bb == BaseModule.getUIntTypeAlias().export(); + } else if (aa == BaseModule.getUInt16TypeAlias().export()) { + return bb == BaseModule.getInt32TypeAlias().export() + || bb == BaseModule.getUInt32TypeAlias().export() + || bb == BaseModule.getUIntTypeAlias().export(); + } else if (aa == BaseModule.getUInt32TypeAlias().export()) { + return bb == BaseModule.getUIntTypeAlias().export(); + } + } + } + } else if (a instanceof PType.Class aClass && b instanceof PType.Class bClass) { + if (!aClass.getPClass().isSubclassOf(bClass.getPClass())) { + return false; + } + var aArgs = aClass.getTypeArguments(); + var bArgs = bClass.getTypeArguments(); + var bParams = bClass.getPClass().getTypeParameters(); + if (aArgs.size() != bArgs.size()) { + return false; + } + // check variance of type args pairwise + for (var i = 0; i < aArgs.size(); i++) { + if (!switch (bParams.get(i).getVariance()) { + case INVARIANT -> aArgs.get(i) == bArgs.get(i); + case COVARIANT -> isSubtype(aArgs.get(i), bArgs.get(i)); + case CONTRAVARIANT -> isSubtype(bArgs.get(i), aArgs.get(i)); + }) { + return false; + } + } + return true; + } else if (b instanceof PType.Union bUnion) { + if (a instanceof PType.Union aUnion) { + a: + for (var aElem : aUnion.getElementTypes()) { + for (var bElem : bUnion.getElementTypes()) { + if (isSubtype(aElem, bElem)) continue a; + } + return false; + } + return true; + } else { + for (var bElem : bUnion.getElementTypes()) { + if (isSubtype(a, bElem)) return true; + } + } + } + return false; + } + + private static boolean isIntTypeAlias(TypeAlias t) { + return t == BaseModule.getInt8TypeAlias().export() + || t == BaseModule.getInt16TypeAlias().export() + || t == BaseModule.getInt32TypeAlias().export() + || t == BaseModule.getUInt8TypeAlias().export() + || t == BaseModule.getUInt16TypeAlias().export() + || t == BaseModule.getUInt32TypeAlias().export() + || t == BaseModule.getUIntTypeAlias().export(); + } + + private static boolean isPreservedTypeAlias(TypeAlias t) { + return isIntTypeAlias(t); + } + + @Override + public VmClass getVmClass() { + return RefModule.getReferenceClass(); + } + + @Override + public void force(boolean allowUndefinedValues) { + if (forced) return; + + forced = true; + + domain.force(allowUndefinedValues); + VmValue.force(data, allowUndefinedValues); + for (var elem : path) { + elem.force(allowUndefinedValues); + } + } + + @Override + public Reference export() { + var pathList = new ArrayList(path.size()); + for (var elem : path) { + pathList.add(elem.export()); + } + + return new Reference(domain.export(), VmValue.export(data), pathList, getReferentType()); + } + + public PType exportType() { + return new PType.Class( + RefModule.getReferenceClass().export(), + new PType.Class(domain.getVmClass().export()), + getReferentType()); + } + + @Override + public void accept(VmValueVisitor visitor) { + visitor.visitReference(this); + } + + @Override + public T accept(VmValueConverter converter, Iterable path) { + return converter.convertReference(this, path); + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) return true; + if (!(o instanceof VmReference that)) { + return false; + } + + return domain.equals(that.domain) + && data.equals(that.data) + && path.equals(that.path) + && referentType.equals(that.referentType); + } + + @Override + public int hashCode() { + int result = domain.hashCode(); + result = 31 * result + data.hashCode(); + result = 31 * result + path.hashCode(); + result = 31 * result + referentType.hashCode(); + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java index 3de9ffad6..26bfb48d5 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypeAlias.java @@ -32,6 +32,7 @@ import org.pkl.core.TypeParameter; import org.pkl.core.ast.VmModifier; import org.pkl.core.ast.type.TypeNode; import org.pkl.core.ast.type.TypeNode.ConstrainedTypeNode; +import org.pkl.core.ast.type.TypeNode.ReferenceTypeNode; import org.pkl.core.ast.type.TypeNode.TypeVariableNode; import org.pkl.core.ast.type.TypeNode.UnknownTypeNode; import org.pkl.core.util.LateInit; @@ -177,7 +178,8 @@ public final class VmTypeAlias extends VmValue { } @TruffleBoundary - public TypeNode instantiate(TypeNode[] typeArgumentNodes) { + public TypeNode instantiate( + TypeNode[] typeArgumentNodes, SourceSection typeAliasTypeNodeSourceSection) { // Cloning the type node means that the entire type check remains within a single root node, // which should be good for interpreted and compiled performance alike: // * Fewer root nodes to call @@ -199,6 +201,13 @@ public final class VmTypeAlias extends VmValue { } return true; }); + clone.accept( + node -> { + if (node instanceof ReferenceTypeNode referenceTypeNode) { + referenceTypeNode.validateTypeArguments(typeAliasTypeNodeSourceSection); + } + return true; + }); return clone; } @@ -228,7 +237,8 @@ public final class VmTypeAlias extends VmValue { simpleName, getModuleName(), qualifiedName, - typeParameters); + typeParameters, + module.getVmClass().export()); for (var parameter : typeParameters) { parameter.initOwner(__pTypeAlias); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmTypes.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypes.java index 69e809018..01927b795 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmTypes.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmTypes.java @@ -1,5 +1,5 @@ /* - * Copyright Β© 2024 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. @@ -40,6 +40,7 @@ import com.oracle.truffle.api.dsl.TypeSystem; VmRegex.class, VmTypeAlias.class, VmObjectLike.class, - VmValue.class + VmReference.class, + VmValue.class, }) public class VmTypes {} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java index 62f2696f3..d18f3259e 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java @@ -85,6 +85,8 @@ public interface VmValueConverter { T convertFunction(VmFunction value, Iterable path); + T convertReference(VmReference value, Iterable path); + /** Returns with an empty identifier if the second value is a RenderDirective */ Pair convertProperty(ClassProperty property, Object value, Iterable path); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java index 63c9ce825..4abef27cb 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java @@ -271,6 +271,30 @@ public final class VmValueRenderer { append("null"); } + @Override + public void visitReference(VmReference value) { + contexts.push(Context.EXPLICIT); + append("Reference("); + visit(value.getDomain()); + append(", "); + append(value.getReferentType()); + append(", "); + visit(value.getData()); + append(")"); + for (var elem : value.getPath()) { + var property = VmUtils.readMember(elem, Identifier.PROPERTY); + if (property instanceof String propName) { + append("."); + writeIdentifier(propName); + } else { + append("["); + visit(VmUtils.readMember(elem, Identifier.KEY)); + append("]"); + } + } + contexts.pop(); + } + private void append(Object value) { builder.append(value); checkLengthLimit(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueVisitor.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueVisitor.java index 29b7cff7b..190634206 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueVisitor.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueVisitor.java @@ -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. @@ -60,6 +60,8 @@ public interface VmValueVisitor { void visitFunction(VmFunction value); + void visitReference(VmReference value); + default void visit(Object value) { Objects.requireNonNull(value, "Value to be visited must be non-null."); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java b/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java index 2cfd69519..17c24416f 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java @@ -34,6 +34,7 @@ import org.pkl.core.runtime.VmListing; import org.pkl.core.runtime.VmMap; import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmReference; import org.pkl.core.runtime.VmSet; import org.pkl.core.runtime.VmTypeAlias; import org.pkl.core.runtime.VmTyped; @@ -397,6 +398,11 @@ public abstract class AbstractRenderer implements VmValueVisitor { currSourceSection = prevSourceSection; } + @Override + public void visitReference(VmReference value) { + cannotRenderTypeAddConverter(value); + } + protected void cannotRenderTypeAddConverter(VmValue value) { var builder = new VmExceptionBuilder() diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java b/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java index 036d23b05..9bf04ed44 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java @@ -46,6 +46,7 @@ public final class PklConverter implements VmValueConverter { private final @Nullable VmFunction nullConverter; private final @Nullable VmFunction classConverter; private final @Nullable VmFunction typeAliasConverter; + private final @Nullable VmFunction referenceConverter; private PklConverter( VmMapping converters, VmMapping convertPropertyTransformers, Object rendererOrParser) { @@ -76,6 +77,7 @@ public final class PklConverter implements VmValueConverter { nullConverter = typeConverters.get(BaseModule.getNullClass()); classConverter = typeConverters.get(BaseModule.getClassClass()); typeAliasConverter = typeConverters.get(BaseModule.getTypeAliasClass()); + referenceConverter = typeConverters.get(RefModule.getReferenceClass()); } public static final PklConverter NOOP = @@ -199,6 +201,11 @@ public final class PklConverter implements VmValueConverter { return doConvert(value, path, nullConverter); } + @Override + public Object convertReference(VmReference value, Iterable path) { + return doConvert(value, path, referenceConverter); + } + @Override public Pair convertProperty( ClassProperty property, Object value, Iterable path) { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java index 6906324ad..ad1868270 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java @@ -59,6 +59,7 @@ import org.pkl.core.runtime.VmMap; import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmNull; import org.pkl.core.runtime.VmPair; +import org.pkl.core.runtime.VmReference; import org.pkl.core.runtime.VmRegex; import org.pkl.core.runtime.VmSet; import org.pkl.core.runtime.VmTyped; @@ -554,6 +555,12 @@ public final class RendererNodes { builder.append(value); } + @Override + public void visitReference(VmReference value) { + writePropertyName(); + builder.append(value); + } + /** * Resolves types for the purpose of protobuf rendering. "Sees through" nullable types and type * aliases, simplifies variations of {@code Int} and {@code String} types (literate string diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/ref/RefNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/ref/RefNodes.java new file mode 100644 index 000000000..c8f6b2649 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/ref/RefNodes.java @@ -0,0 +1,33 @@ +/* + * Copyright Β© 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. + * You may obtain a copy of the License at + * + * https://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 org.pkl.core.stdlib.ref; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmReference; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.stdlib.ExternalMethod3Node; + +public class RefNodes { + public abstract static class Reference extends ExternalMethod3Node { + @Specialization + protected VmReference eval( + VirtualFrame frame, VmTyped self, VmTyped domain, VmClass clazz, Object data) { + return new VmReference(domain, clazz, data); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/ref/ReferenceNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/ref/ReferenceNodes.java new file mode 100644 index 000000000..7f129f9f1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/ref/ReferenceNodes.java @@ -0,0 +1,46 @@ +/* + * Copyright Β© 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. + * You may obtain a copy of the License at + * + * https://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 org.pkl.core.stdlib.ref; + +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.runtime.VmList; +import org.pkl.core.runtime.VmReference; +import org.pkl.core.stdlib.ExternalMethod0Node; + +public class ReferenceNodes { + private ReferenceNodes() {} + + public abstract static class getDomain extends ExternalMethod0Node { + @Specialization + protected Object eval(VmReference self) { + return self.getDomain(); + } + } + + public abstract static class getData extends ExternalMethod0Node { + @Specialization + protected Object eval(VmReference self) { + return self.getData(); + } + } + + public abstract static class getPath extends ExternalMethod0Node { + @Specialization + protected VmList eval(VmReference self) { + return VmList.create(self.getPath()); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/ref/package-info.java b/pkl-core/src/main/java/org/pkl/core/stdlib/ref/package-info.java new file mode 100644 index 000000000..09f331911 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/ref/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.pkl.core.stdlib.ref; + +import org.jspecify.annotations.NullMarked; diff --git a/pkl-core/src/main/java/org/pkl/core/util/pklbinary/PklBinaryCode.java b/pkl-core/src/main/java/org/pkl/core/util/pklbinary/PklBinaryCode.java index 6a969d1ef..21af46c87 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/pklbinary/PklBinaryCode.java +++ b/pkl-core/src/main/java/org/pkl/core/util/pklbinary/PklBinaryCode.java @@ -33,6 +33,7 @@ public enum PklBinaryCode { TYPEALIAS((byte) 0x0D), FUNCTION((byte) 0x0E), BYTES((byte) 0x0F), + REFERENCE((byte) 0x20), PROPERTY((byte) 0x10), ENTRY((byte) 0x11), @@ -65,6 +66,7 @@ public enum PklBinaryCode { case 0x0D -> PklBinaryCode.TYPEALIAS; case 0x0E -> PklBinaryCode.FUNCTION; case 0x0F -> PklBinaryCode.BYTES; + case 0x20 -> PklBinaryCode.REFERENCE; case 0x10 -> PklBinaryCode.PROPERTY; case 0x11 -> PklBinaryCode.ENTRY; diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index a8f365ad5..fb96eabb3 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -390,6 +390,9 @@ Cannot find property `{0}` in object of type `{1}`.\n\ Did you mean any of the following?\n\ {2} +cannotFindPropertyInReference=\ +Cannot find property `{0}` in object of type `{1}`. + cannotFindMethodInScope=\ Cannot find method `{0}`. @@ -1179,3 +1182,6 @@ Cannot follow redirect from ''https:'' URL to ''http:'' URL.\ \n\ HTTP Request: `GET {0}`\n\ Redirected to: `{1}` + +invalidReferenceTypeAnnotationWithConstraint=\ +`Reference` referent type argument may not include type constraints. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModule.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModule.pkl new file mode 100644 index 000000000..f9781db09 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModule.pkl @@ -0,0 +1 @@ +foo: String diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModuleWithOutputOverride.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModuleWithOutputOverride.pkl new file mode 100644 index 000000000..308c004aa --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/errors/ReferencedModuleWithOutputOverride.pkl @@ -0,0 +1,7 @@ +open module ReferencedModuleWithOutputOverride + +foo: String + +hidden output: ModuleOutput = new { + text = foo +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl new file mode 100644 index 000000000..51ab221fe --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl @@ -0,0 +1,134 @@ +import "pkl:math" +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = + let (data = reference.getData()) + let (root = if (data is Resource) data.name else data.toString()) + let ( + path = + reference + .getPath() + .map((elem) -> if (elem.isProperty) ".\(elem.property)" else "[\(elem.key)]") + ) + "${\(root)\(path.join(""))}" +} + +local const d: D = new {} +typealias Ref = ref.Reference + +abstract class Resource { + name: String + hidden fixed $: Ref +} + +class A extends Resource { + id: String + hidden outputs: AProperties + hidden fixed $: Ref = ref.Reference(d, A, this) +} + +/// Test doc comment +class AProperties { + foo: Int + someMapping: Mapping + someMap: Map + someListing: Listing + someList: List + nonInt: String +} + +class B extends Resource { + id: String + hidden outputs: BProperties + hidden fixed $: Ref = ref.Reference(d, B, this) +} + +class BProperties { + foo: String + someMapping: Mapping + someMap: Map + someListing: Listing + someList: List + nonString: Int +} + +class MapKey { + k: Int + + function toString(): String = "key:\(k)" +} + +class K { + aId: String | *Ref? + bId: String | *Ref? + aProperties: AProperties | *Ref? + bProperties: BProperties | *Ref? + aValues: Listing>? + bValues: Listing>? + splitUnion: Ref | Listing>? +} + +a: A = new { + name = "a" + id = "some-a-value" +} + +b: B = new { + name = "b" + id = "some-b-value" +} + +aOrB: A | B = a +aRef: Ref = a.$ +bRef: Ref = b.$ +unknownRef: Ref = aRef +unknownRef2: Ref | Ref = aRef +unknownRef3: Ref = aOrB.$ + +k: K = new { + aId = aRef.id + bId = bRef.id + aProperties = aRef.outputs + bProperties = bRef.outputs + aValues { + aRef.outputs.foo + aRef.outputs.someMapping["key"] + aRef.outputs.someMap[new MapKey { k = 123 }] + aRef.outputs.someListing[0] + aRef.outputs.someList[math.maxInt] + bRef.outputs.nonString + } + bValues { + bRef.outputs.foo + bRef.outputs.someMapping["key"] + bRef.outputs.someMap[new MapKey { k = 123 }] + bRef.outputs.someListing[0] + bRef.outputs.someList[math.maxInt] + aRef.outputs.nonInt + } +} + +j: K = new { + local aRef2 = unknownRef as Ref + aProperties = aRef2.outputs + splitUnion = unknownRef.outputs.someListing +} + +refInterpolation = "\(aRef.outputs.someListing[1])" +kInterpolation = "\(k)" +aValuesJoined = k.aValues.join("\n").replaceAll(Regex("@[a-z0-9]+"), "@") + +// ensure that type arguments that are unions are handled correctly +typeArgs = ref.Reference(d, TypeHolder, null).prop as Ref> +class TypeHolder { + prop: Listing +} + +output { + renderer { + converters { + [ref.Reference] = (it) -> it.toString() + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl new file mode 100644 index 000000000..0465c5bfe --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl @@ -0,0 +1,9 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} +typealias Ref = ref.Reference + +test = ref.Reference(d, String, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference10.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference10.pkl new file mode 100644 index 000000000..bb145f685 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference10.pkl @@ -0,0 +1,9 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +typealias Ref = ref.Reference +local d: D = new {} + +test = ref.Reference(d, String, "") as Ref> diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference11.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference11.pkl new file mode 100644 index 000000000..8bb1c9141 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference11.pkl @@ -0,0 +1,12 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +typealias Ref = ref.Reference +local d: D = new {} + +test = ref.Reference(d, String, "") as Ref + +typealias Alias1 = Int | Alias2? +typealias Alias2 = String(length < 5) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference12.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference12.pkl new file mode 100644 index 000000000..576d7d287 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference12.pkl @@ -0,0 +1,12 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +typealias RefAlias1 = ref.Reference +local d: D = new {} + +test = ref.Reference(d, String, "") as RefAlias1 + +typealias Alias1 = Int | Alias2? +typealias Alias2 = String(length < 5) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl new file mode 100644 index 000000000..489f46661 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl @@ -0,0 +1,8 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +test = ref.Reference(d, Mapping, "").default diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl new file mode 100644 index 000000000..47bbe6628 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl @@ -0,0 +1,8 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +test = ref.Reference(d, Listing, "").default diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl new file mode 100644 index 000000000..4f82ef2af --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl @@ -0,0 +1,8 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +test = ref.Reference(d, Dynamic, "").default diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl new file mode 100644 index 000000000..1519c25db --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl @@ -0,0 +1,10 @@ +import "pkl:ref" + +import ".../input-helper/errors/ReferencedModule.pkl" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +test = ref.Reference(d, ReferencedModule.getClass(), "").output diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl new file mode 100644 index 000000000..81d697e2a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl @@ -0,0 +1,10 @@ +import "pkl:ref" + +import ".../input-helper/errors/ReferencedModuleWithOutputOverride.pkl" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +test = ref.Reference(d, ReferencedModuleWithOutputOverride.getClass(), "").output diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl new file mode 100644 index 000000000..7a916c524 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl @@ -0,0 +1,12 @@ +import "pkl:ref" + +import ".../input-helper/errors/ReferencedModuleWithOutputOverride.pkl" + +class ModuleSubclass extends ReferencedModuleWithOutputOverride + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +test = ref.Reference(d, ModuleSubclass, "").output diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl new file mode 100644 index 000000000..2e4ff0a74 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl @@ -0,0 +1,12 @@ +import "pkl:ref" + +class A { + foo: String | Int | Boolean +} + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +test = ref.Reference(d, A, "").foo as ref.Reference diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl new file mode 100644 index 000000000..a942c2034 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl @@ -0,0 +1,9 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} +typealias Ref = ref.Reference + +test = ref.Reference(d, String, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl new file mode 100644 index 000000000..417c9f3a0 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl @@ -0,0 +1,15 @@ +import "pkl:ref" + +class A { + foo: String | Int | Boolean +} + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} + +local test = ref.Reference(d, A, "").foo +testInterpolation = "test:\(test)" + +// this tests that the interpolation appears in the output when referenceToString throws diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl new file mode 100644 index 000000000..182046e4c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl @@ -0,0 +1,9 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} +typealias Ref = ref.Reference + +test = ref.Reference(d, String, "") as Ref> diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl new file mode 100644 index 000000000..d264d65f5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl @@ -0,0 +1,12 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} +typealias Ref = ref.Reference + +test = ref.Reference(d, String, "") as Ref + +typealias Alias1 = Int | Alias2? +typealias Alias2 = Boolean diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl new file mode 100644 index 000000000..1a7365af4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl @@ -0,0 +1,14 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} +typealias Ref = ref.Reference + +class D2 extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d2: D2 = new {} + +test = ref.Reference(d2, String, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl new file mode 100644 index 000000000..7ab3faed2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl @@ -0,0 +1,13 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} +typealias Ref = ref.Reference + +class A { + b: String +} + +test = ref.Reference(d, String, new A {}).c diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl new file mode 100644 index 000000000..649ddd426 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl @@ -0,0 +1,9 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +local d: D = new {} +typealias Ref = ref.Reference + +test = ref.Reference(d, List, List())["hi"] diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference8.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference8.pkl new file mode 100644 index 000000000..0dff11cbe --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference8.pkl @@ -0,0 +1,9 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +typealias Ref = ref.Reference +local d: D = new {} + +test = ref.Reference(d, String, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference9.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference9.pkl new file mode 100644 index 000000000..2fa7bf21f --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference9.pkl @@ -0,0 +1,9 @@ +import "pkl:ref" + +class D extends ref.Domain { + function renderReference(reference: ref.Reference): String = throw("not supported") +} +typealias Ref = ref.Reference +local d: D = new {} + +test = ref.Reference(d, String, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/reference.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/reference.pcf new file mode 100644 index 000000000..a59f18ba6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/reference.pcf @@ -0,0 +1,60 @@ +a { + name = "a" + id = "some-a-value" +} +b { + name = "b" + id = "some-b-value" +} +aOrB { + name = "a" + id = "some-a-value" +} +aRef = "${a}" +bRef = "${b}" +unknownRef = "${a}" +unknownRef2 = "${a}" +unknownRef3 = "${a}" +k { + aId = "${a.id}" + bId = "${b.id}" + aProperties = "${a.outputs}" + bProperties = "${b.outputs}" + aValues { + "${a.outputs.foo}" + "${a.outputs.someMapping[key]}" + "${a.outputs.someMap[key:123]}" + "${a.outputs.someListing[0]}" + "${a.outputs.someList[9223372036854775807]}" + "${b.outputs.nonString}" + } + bValues { + "${b.outputs.foo}" + "${b.outputs.someMapping[key]}" + "${b.outputs.someMap[key:123]}" + "${b.outputs.someListing[0]}" + "${b.outputs.someList[9223372036854775807]}" + "${a.outputs.nonInt}" + } + splitUnion = null +} +j { + aId = null + bId = null + aProperties = "${a.outputs}" + bProperties = null + aValues = null + bValues = null + splitUnion = "${a.outputs.someListing}" +} +refInterpolation = "${a.outputs.someListing[1]}" +kInterpolation = "new K { aId = Reference(new D {}, String, new A { name = \"a\"; id = \"some-a-value\" }).id; bId = Reference(new D {}, String, new B { name = \"b\"; id = \"some-b-value\" }).id; aProperties = Reference(new D {}, reference#AProperties, new A { name = \"a\"; id = \"some-a-value\" }).outputs; bProperties = Reference(new D {}, reference#BProperties, new B { name = \"b\"; id = \"some-b-value\" }).outputs; aValues { Reference(new D {}, Int, new A { name = \"a\"; id = \"some-a-value\" }).outputs.foo; Reference(new D {}, Int | Null, new A { name = \"a\"; id = \"some-a-value\" }).outputs.someMapping[\"key\"]; Reference(new D {}, Int, new A { name = \"a\"; id = \"some-a-value\" }).outputs.someMap[new MapKey { k = 123 }]; Reference(new D {}, Int, new A { name = \"a\"; id = \"some-a-value\" }).outputs.someListing[0]; Reference(new D {}, Int, new A { name = \"a\"; id = \"some-a-value\" }).outputs.someList[9223372036854775807]; Reference(new D {}, Int, new B { name = \"b\"; id = \"some-b-value\" }).outputs.nonString }; bValues { Reference(new D {}, String, new B { name = \"b\"; id = \"some-b-value\" }).outputs.foo; Reference(new D {}, Null | String, new B { name = \"b\"; id = \"some-b-value\" }).outputs.someMapping[\"key\"]; Reference(new D {}, String, new B { name = \"b\"; id = \"some-b-value\" }).outputs.someMap[new MapKey { k = 123 }]; Reference(new D {}, String, new B { name = \"b\"; id = \"some-b-value\" }).outputs.someListing[0]; Reference(new D {}, String, new B { name = \"b\"; id = \"some-b-value\" }).outputs.someList[9223372036854775807]; Reference(new D {}, String, new A { name = \"a\"; id = \"some-a-value\" }).outputs.nonInt }; splitUnion = null }" +aValuesJoined = """ + org.pkl.core.runtime.VmReference@ + org.pkl.core.runtime.VmReference@ + org.pkl.core.runtime.VmReference@ + org.pkl.core.runtime.VmReference@ + org.pkl.core.runtime.VmReference@ + org.pkl.core.runtime.VmReference@ + """ +typeArgs = "${null.prop}" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err index f2bc6644d..8e835110d 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err @@ -20,6 +20,7 @@ pkl:pklbinary pkl:platform pkl:Project pkl:protobuf +pkl:ref pkl:reflect pkl:release pkl:semver diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err new file mode 100644 index 000000000..ebc5ea327 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `pkl.ref#Reference`, but got type `pkl.ref#Reference`. +Value: Reference(new D {}, String, "") + +x | typealias Ref = ref.Reference + ^^^^^^^^^^^^^^^^^^^ +at reference1#test (file:///$snippetsDir/input/errors/reference1.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference10.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference10.err new file mode 100644 index 000000000..461d5811a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference10.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +`Reference` referent type argument may not include type constraints. + +x | test = ref.Reference(d, String, "") as Ref> + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference10#test (file:///$snippetsDir/input/errors/reference10.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference11.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference11.err new file mode 100644 index 000000000..ac3607b2a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference11.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +`Reference` referent type argument may not include type constraints. + +x | test = ref.Reference(d, String, "") as Ref + ^^^^^^^^^^^^ +at reference11#test (file:///$snippetsDir/input/errors/reference11.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference12.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference12.err new file mode 100644 index 000000000..cde60de77 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference12.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +`Reference` referent type argument may not include type constraints. + +x | typealias RefAlias1 = ref.Reference + ^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference12#RefAlias1 (file:///$snippetsDir/input/errors/reference12.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err new file mode 100644 index 000000000..e689a38a9 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Cannot find property `default` in object of type `pkl.ref#Reference>`. + +x | test = ref.Reference(d, Mapping, "").default + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference13#test (file:///$snippetsDir/input/errors/reference13.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err new file mode 100644 index 000000000..bebf392d1 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Cannot find property `default` in object of type `pkl.ref#Reference>`. + +x | test = ref.Reference(d, Listing, "").default + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference14#test (file:///$snippetsDir/input/errors/reference14.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err new file mode 100644 index 000000000..7c833d88c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Cannot find property `default` in object of type `pkl.ref#Reference`. + +x | test = ref.Reference(d, Dynamic, "").default + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference15#test (file:///$snippetsDir/input/errors/reference15.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err new file mode 100644 index 000000000..13ea3afe3 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Cannot find property `output` in object of type `pkl.ref#Reference`. + +xx | test = ref.Reference(d, ReferencedModule.getClass(), "").output + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference16#test (file:///$snippetsDir/input/errors/reference16.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err new file mode 100644 index 000000000..e68974a6e --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Cannot find property `output` in object of type `pkl.ref#Reference`. + +xx | test = ref.Reference(d, ReferencedModuleWithOutputOverride.getClass(), "").output + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference17#test (file:///$snippetsDir/input/errors/reference17.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err new file mode 100644 index 000000000..05087f5ae --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Cannot find property `output` in object of type `pkl.ref#Reference`. + +xx | test = ref.Reference(d, ModuleSubclass, "").output + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference18#test (file:///$snippetsDir/input/errors/reference18.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err new file mode 100644 index 000000000..cafd2a0e0 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `pkl.ref#Reference`, but got type `pkl.ref#Reference`. +Value: Reference(new D {}, Boolean | Int | String, "").foo + +xx | test = ref.Reference(d, A, "").foo as ref.Reference + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference19#test (file:///$snippetsDir/input/errors/reference19.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err new file mode 100644 index 000000000..ce1a58dbc --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `pkl.ref#Reference`, but got type `pkl.ref#Reference`. +Value: Reference(new D {}, String, "") + +x | typealias Ref = ref.Reference + ^^^^^^^^^^^^^^^^^^^ +at reference2#test (file:///$snippetsDir/input/errors/reference2.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference20.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference20.err new file mode 100644 index 000000000..08663a8a8 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference20.err @@ -0,0 +1,22 @@ +–– Pkl Error –– +not supported + +x | function renderReference(reference: ref.Reference): String = throw("not supported") + ^^^^^^^^^^^^^^^^^^^^^^ +at reference20#D.renderReference (file:///$snippetsDir/input/errors/reference20.pkl) + +xxx | function toString(): String = getDomain().renderReference(this) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.ref#Reference.toString (pkl:ref) + +xx | testInterpolation = "test:\(test)" + ^^^^^^^ +at reference20#testInterpolation (file:///$snippetsDir/input/errors/reference20.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err new file mode 100644 index 000000000..7151afaa7 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `pkl.ref#Reference>`, but got type `pkl.ref#Reference`. +Value: Reference(new D {}, String, "") + +x | typealias Ref = ref.Reference + ^^^^^^^^^^^^^^^^^^^ +at reference3#test (file:///$snippetsDir/input/errors/reference3.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err new file mode 100644 index 000000000..7d90d28ac --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `pkl.ref#Reference`, but got type `pkl.ref#Reference`. +Value: Reference(new D {}, String, "") + +x | typealias Ref = ref.Reference + ^^^^^^^^^^^^^^^^^^^ +at reference4#test (file:///$snippetsDir/input/errors/reference4.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err new file mode 100644 index 000000000..3749aa331 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `pkl.ref#Reference`, but got type `pkl.ref#Reference`. +Value: Reference(new D2 {}, String, "") + +x | typealias Ref = ref.Reference + ^^^^^^^^^^^^^^^^^^^ +at reference5#test (file:///$snippetsDir/input/errors/reference5.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err new file mode 100644 index 000000000..1dd136837 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Cannot find property `c` in object of type `pkl.ref#Reference`. + +xx | test = ref.Reference(d, String, new A {}).c + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference6#test (file:///$snippetsDir/input/errors/reference6.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err new file mode 100644 index 000000000..426b680ec --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Operator `[]` is not defined for operand types `pkl.ref#Reference>` and `String`. +Left operand : Reference(new D {}, List, List()) +Right operand: "hi" + +x | test = ref.Reference(d, List, List())["hi"] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at reference7#test (file:///$snippetsDir/input/errors/reference7.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference8.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference8.err new file mode 100644 index 000000000..206779e8c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference8.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +`Reference` referent type argument may not include type constraints. + +x | test = ref.Reference(d, String, "") as Ref + ^^^^^^^^^^^^^^^^^ +at reference8#test (file:///$snippetsDir/input/errors/reference8.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference9.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference9.err new file mode 100644 index 000000000..fa24b00ea --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference9.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +`Reference` referent type argument may not include type constraints. + +x | test = ref.Reference(d, String, "") as Ref + ^^^^^^^^^^^^^^^^^^^^^^^ +at reference9#test (file:///$snippetsDir/input/errors/reference9.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/stdlib/ref.pkl b/stdlib/ref.pkl new file mode 100644 index 000000000..5ea25d5b3 --- /dev/null +++ b/stdlib/ref.pkl @@ -0,0 +1,207 @@ +//===----------------------------------------------------------------------===// +// Copyright Β© 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. +// You may obtain a copy of the License at +// +// https://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. +//===----------------------------------------------------------------------===// + +/// Type-safe deferred references to values not known at evaluation time. +/// +/// References are an advanced API design tool that enables library authors to express +/// domain-specific references to values that may not be known during evaluation. +/// They are particularly suited for configuring execution systems where tasks have well-typed +/// inputs and output. +/// +/// WARNING: The API and semantics of references are subject to change in a future release. +/// The Pkl team is soliciting feedback from authors of libraries considering adopting references. +/// For questions and feedback, please reach out via +/// [GitHub Discussions](https://github.com/apple/pkl/discussions) or +/// [create an issue](https://github.com/apple/pkl/issues/new). +@ModuleInfo { minPklVersion = "0.32.0" } +module pkl.ref + +/// Creates a deferred [Reference] to a value of type [class] in the given [domain]. +/// +/// References may only be constructed for single, non-generic class types. +/// To create a reference to other types (generic classes, union, nullable, constrained, +/// typealiases, etc.), it may be necessary to create a wrapper class with a property of the desired +/// type. +/// Example: +/// ``` +/// referenceListingString: Reference> = Reference(domain, Holder, data).$ +/// +/// class Holder { +/// $: Listing +/// } +/// ``` +external const function Reference( + domain: D(this is Domain), + `class`: Class, + data: Any, +): Reference + +/// A reference to a value that may not exist at evaluation time. +/// +/// References are an advanced API design tool that enables library authors to express +/// domain-specific references to values that may not be known during evaluation. +/// +/// Instances are created with the [Reference()] constructor method. +/// References consist of four parts: +/// * [Domain]: determines which references are compatible and how references are rendered +/// as strings. +/// * Data: an arbitrary value that may contain domain-specific information about the +/// referenced value. +/// * Path: a [List] of [Access] values indicating how the reference was accessed (by property or +/// subscript). +/// * Referent type: the type of the value that the reference refers to. +/// +/// Every time a reference is accessed, either via qualified property access +/// (`.`) or subscript (`[]`), a new reference is returned. +/// The new reference shares the same domain and data as the original reference. +/// The new reference's path extends the original reference's with an [Access] instance describing +/// the accessed property name or subscript key. +/// The new reference's referent type is the type of the accessed property or subscript value of the +/// original referent type. +/// Properties with the `external` modifier may not be referenced. +/// Any type constraints within the referent type are erased and type constraints are not allowed +/// in the referent (second) type argument of any Reference type annotations. +/// +/// Example: +/// ``` +/// import "pkl:ref" +/// +/// /// Define a domain class for this configuration system: +/// class Domain extends ref.Domain { +/// /// Define how this domain renders references to strings: +/// function renderReference(reference: ref.Reference): String = +/// ( +/// List(reference.getData().toString()) +/// + reference.getPath().map((it) -> it.property ?? it.key.toString()) +/// ).join("/") +/// } +/// +/// class Resource { +/// name: String +/// /// Types that provide references will often vend a "root" Reference via a `fixed` property. +/// /// Here, this references [Outputs] and the `data` identifies the enclosing [Resource]. +/// fixed outputs: ref.Reference = ref.Reference(new Domain {}, Outputs, name) +/// } +/// +/// class Outputs { +/// a: String +/// b: String(length < 5) +/// c: Listing +/// d: Mapping +/// e: Foo | Bar +/// } +/// +/// class Foo { +/// x: Int +/// +/// function toString(): String = "foo:\(x)" +/// } +/// +/// class Bar { +/// x: Float +/// +/// function toString(): String = "bar:\(x)" +/// } +/// +/// local outputs: ref.Reference = new Resource { name = "root" }.outputs +/// +/// /// Simple property access. +/// /// getPath() == List(new ref.Access { property = "a" }) +/// aRef: ref.Reference = outputs.a +/// +/// /// Type constraint erasure. +/// /// getPath() == List(new ref.Access { property = "b" }) +/// bRef: ref.Reference = outputs.b +/// +/// /// Simple subscript access. +/// /// getPath() == List(new ref.Access { property = "c" }, new ref.Access { key = 10 }) +/// cRef: ref.Reference = outputs.c[10] +/// +/// /// Simple property access: +/// /// getPath() == List(new ref.Access { property = "d" }, new ref.Access { key = new Foo { x = 1 } }) +/// /// The type constraint on the Mapping value type argument is also erased. +/// dRef: ref.Reference = outputs.d[new Foo { x = 1 }] +/// +/// /// Simple property access: +/// /// getPath() == List(new ref.Access { property = "e" }, new ref.Access { property = "x" }) +/// eRef: ref.Reference = outputs.e.x +/// +/// // Render references as strings: +/// output { +/// renderer { +/// converters { +/// [ref.Reference] = (it) -> it.toString() +/// } +/// } +/// } +/// ``` +/// +/// Output: +/// ``` +/// aRef = "root/a" +/// bRef = "root/b" +/// cRef = "root/c/10" +/// dRef = "root/d/foo:1" +/// eRef = "root/e/x" +/// ``` +external class Reference { + /// The domain the reference belongs to. + external function getDomain(): D + + /// Arbitrary data attached to the reference during creation. + /// + /// Used for domain-specific purposes such as controlling how the reference is rendered. + external function getData(): Any + + /// The path of property and subscript access applied to the reference. + external function getPath(): List + + /// Render the reference to a string using its domain's ([getDomain()]) + /// [Domain.referencetoString()] method. + function toString(): String = getDomain().renderReference(this) +} + +/// Represents a configuration domain that a [Reference] may exist within. +/// +/// Library authors supporting references should declare a subclass of [Domain] to be shared by all +/// inter-compatible [Reference] instances. +/// +/// A domain also determines how [Reference] values are transformed into strings; see +/// [renderReference()]. +abstract class Domain { + /// Must be overridden by domain classes to determine how [Reference] instances are transformed to + /// strings by [Reference.toString()] and string interpolation. + abstract function renderReference(reference: Reference): String +} + +/// Represents a property or subscript access as part of a [Reference]'s path. +class Access { + /// Indicates if this represents a property access. + fixed isProperty: Boolean = property != null + + /// Indicates if this represents a subscript access. + fixed isSubscript: Boolean = property == null + + /// The property that was accessed. + /// + /// If non-null, this is a property access. If `null` this is a subscript access. + property: String(key == null)? + + /// The subscript key that was accessed. + /// + /// May be null even when [property] is null, which represents a subscript access with key `null`. + key: Any +}