diff --git a/docs/modules/ROOT/partials/component-attributes.adoc b/docs/modules/ROOT/partials/component-attributes.adoc index 2ff24650d..54fadf10b 100644 --- a/docs/modules/ROOT/partials/component-attributes.adoc +++ b/docs/modules/ROOT/partials/component-attributes.adoc @@ -147,6 +147,8 @@ endif::[] :uri-stdlib-Function4: {uri-stdlib-baseModule}/Function4 :uri-stdlib-Function5: {uri-stdlib-baseModule}/Function5 :uri-stdlib-Bytes: {uri-stdlib-baseModule}/Bytes +:uri-stdlib-Reference: {uri-stdlib-baseModule}/Reference +:uri-stdlib-ReferenceAccess: {uri-stdlib-baseModule}/ReferenceAccess :uri-stdlib-Resource: {uri-stdlib-baseModule}/Resource :uri-stdlib-outputFiles: {uri-stdlib-baseModule}/ModuleOutput#files :uri-stdlib-FileOutput: {uri-stdlib-baseModule}/FileOutput diff --git a/docs/modules/bindings-specification/pages/binary-encoding.adoc b/docs/modules/bindings-specification/pages/binary-encoding.adoc index 637d2530f..6402b4a3e 100644 --- a/docs/modules/bindings-specification/pages/binary-encoding.adoc +++ b/docs/modules/bindings-specification/pages/binary-encoding.adoc @@ -187,6 +187,24 @@ The array's length is the number of slots that are filled. For example, xref:{ur | | | + +|link:{uri-stdlib-Reference}[Reference] +|`0x20` +|`` +|Root value +|link:{uri-messagepack-array}[array] +|Array of link:{uri-stdlib-ReferenceAccess}[property access] values +| +| + +|link:{uri-stdlib-ReferenceAccess}[ReferenceAccess] +|`0x21` +|link:{uri-messagepack-str}[str] (when a property access) or link:{uri-messagepack-str}[nil] (when a subscript access) +|Property name +|`` or link:{uri-messagepack-str}[nil] (when a property access) +|Key value +| +| |=== [[type-name-encoding]] 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 02a6beac2..96751c6a4 100644 --- a/pkl-core/src/main/java/org/pkl/core/PClass.java +++ b/pkl-core/src/main/java/org/pkl/core/PClass.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -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 PClass( 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) { @@ -119,6 +122,11 @@ public Map getAllMethods() { 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 String toString() { 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/TypeAlias.java b/pkl-core/src/main/java/org/pkl/core/TypeAlias.java index 70d253981..a679c0353 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 TypeAlias( 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) { @@ -78,6 +81,10 @@ public List getTypeParameters() { return typeParameters; } + public PClass getModuleClass() { + return moduleClass; + } + /** Returns the type that this type alias stands for. */ public PType getAliasedType() { assert aliasedType != null; 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 204ebcb69..312a40a6e 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 @@ -98,6 +98,18 @@ protected Object eval( 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(); + // TODO + throw exceptionBuilder() + .adhocEvalError("unabled to index reference with key {0}", 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 e9cda6af3..2215f21c4 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -61,6 +61,19 @@ protected ReadPropertyNode(SourceSection sourceSection, Identifier propertyName) 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(); + // TODO + throw exceptionBuilder() + .adhocEvalError("Cannot find property `{0}` on Reference", propertyName) + .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/type/TypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java index 8bf986117..a23f86fa6 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 @@ -20,6 +20,7 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.Frame; import com.oracle.truffle.api.frame.FrameDescriptor; @@ -2057,6 +2058,81 @@ protected boolean isParametric() { } } + @ImportStatic(BaseModule.class) + public abstract static class ReferenceTypeNode extends ObjectSlotTypeNode { + private TypeNode referentTypeNode; + @Child private ExpressionNode getModuleNode; + + public ReferenceTypeNode(SourceSection sourceSection, TypeNode referentTypeNode) { + super(sourceSection); + this.referentTypeNode = referentTypeNode; + this.getModuleNode = new GetModuleNode(sourceSection); + } + + @SuppressWarnings("unused") + @Specialization(guards = "value.getVmClass() == getReferenceClass()") + protected Object eval(VirtualFrame frame, VmReference value) { + if (referentTypeNode.isNoopTypeCheck()) { + return value; + } + + var module = (VmTyped) getModuleNode.executeGeneric(frame); + if (value.checkType(TypeNode.export(referentTypeNode), module.getVmClass().export())) { + return value; + } + // TODO better exceptions? + throw new VmExceptionBuilder() + .adhocEvalError( + "reference type mismatch Reference<%s> and %s", + value.getCandidateTypes(), TypeNode.export(referentTypeNode)) + .build(); + } + + @Fallback + protected Object fallback(Object value) { + throw typeMismatch(value, BaseModule.getReferenceClass()); + } + + @Override + protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) { + if (visitTypeArguments) return consumer.accept(this) && consumer.accept(referentTypeNode); + return consumer.accept(this); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getReferenceClass(); + } + + @Override + public VmList getTypeArgumentMirrors() { + return VmList.of(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 referentTypeNode.isNoopTypeCheck(); + } + + @Override + protected PType doExport() { + return new PType.Class(BaseModule.getReferenceClass().export(), referentTypeNode.doExport()); + } + + @Override + protected boolean isParametric() { + return true; + } + } + public static final class PairTypeNode extends ObjectSlotTypeNode { @Child private TypeNode firstTypeNode; @Child private TypeNode secondTypeNode; 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..5d843df0d 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 @@ -287,6 +287,10 @@ public TypeNode execute(VirtualFrame frame) { return new VarArgsTypeNode(sourceSection, typeArgumentNodes[0].execute(frame)); } + if (clazz.isReferenceClass()) { + return ReferenceTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + } + throw exceptionBuilder() .evalError("notAParameterizableClass", clazz.getDisplayName()) .withSourceSection(typeArgumentNodes[0].sourceSection) diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java index 597cca148..e0faa2c28 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java @@ -19,6 +19,7 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.Set; public final class BaseModule extends StdLibModule { static final VmTyped instance = VmUtils.createEmptyModule(); @@ -207,6 +208,14 @@ public static VmClass getResourceClass() { return ResourceClass.instance; } + public static VmClass getReferenceClass() { + return ReferenceClass.instance; + } + + public static VmClass getReferenceAccessClass() { + return ReferenceAccessClass.instance; + } + public static VmTypeAlias getNonNullTypeAlias() { return NonNullTypeAlias.instance; } @@ -231,6 +240,29 @@ public static VmTypeAlias getUInt8TypeAlias() { return UInt8TypeAlias.instance; } + public static VmTypeAlias getUInt16TypeAlias() { + return UInt16TypeAlias.instance; + } + + public static VmTypeAlias getUInt32TypeAlias() { + return UInt32TypeAlias.instance; + } + + public static VmTypeAlias getUIntTypeAlias() { + return UIntTypeAlias.instance; + } + + public static Set getIntTypeAliases() { + return Set.of( + getInt8TypeAlias(), + getInt16TypeAlias(), + getInt32TypeAlias(), + getUInt8TypeAlias(), + getUInt16TypeAlias(), + getUInt32TypeAlias(), + getUIntTypeAlias()); + } + private static final class AnyClass { static final VmClass instance = loadClass("Any"); } @@ -359,6 +391,14 @@ private static final class ResourceClass { static final VmClass instance = loadClass("Resource"); } + private static final class ReferenceClass { + static final VmClass instance = loadClass("Reference"); + } + + private static final class ReferenceAccessClass { + static final VmClass instance = loadClass("ReferenceAccess"); + } + private static final class FunctionClass { static final VmClass instance = loadClass("Function"); } @@ -407,6 +447,18 @@ private static final class UInt8TypeAlias { static final VmTypeAlias instance = loadTypeAlias("UInt8"); } + private static final class UInt16TypeAlias { + static final VmTypeAlias instance = loadTypeAlias("UInt16"); + } + + private static final class UInt32TypeAlias { + static final VmTypeAlias instance = loadTypeAlias("UInt32"); + } + + private static final class UIntTypeAlias { + static final VmTypeAlias instance = loadTypeAlias("UInt"); + } + private static final class MixinTypeAlias { static final VmTypeAlias instance = loadTypeAlias("Mixin"); } 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 7d1e3880b..cfea98a9f 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 @@ -440,6 +440,11 @@ public boolean isVarArgsClass() { return isClass(BaseModule.getVarArgsClass(), "pkl.base#VarArgs"); } + @Idempotent + public boolean isReferenceClass() { + return isClass(BaseModule.getReferenceClass(), "pkl.base#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); @@ -611,53 +616,66 @@ public VmMap getAllMethodMirrors() { @TruffleBoundary public PClass export() { synchronized (pClassLock) { - if (__pClass == null) { - 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); - - for (var parameter : typeParameters) { - parameter.initOwner(__pClass); - } + if (__pClass != null) { + return __pClass; + } - if (supertypeNode != null) { - assert superclass != null; - __pClass.initSupertype(TypeNode.export(supertypeNode), superclass.export()); - } + // 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; + } - VmUtils.exportAnnotations(annotations, exportedAnnotations); + 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); + } - 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()); + } - for (var method : EconomicMaps.getValues(declaredMethods)) { - if (method.isLocal()) continue; - methods.put(method.getName().toString(), method.export(__pClass)); + VmUtils.exportAnnotations(annotations, exportedAnnotations); + + for (var property : EconomicMaps.getValues(declaredProperties)) { + if (isClassPropertyDefinition(property)) { + properties.put(property.getName().toString(), property.export(__pClass)); } } - return __pClass; + for (var method : EconomicMaps.getValues(declaredMethods)) { + if (method.isLocal()) continue; + methods.put(method.getName().toString(), method.export(__pClass)); + } } + + return __pClass; } @Override 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..5f16659c5 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 @@ -19,6 +19,7 @@ import java.util.Deque; import org.msgpack.core.MessageBufferPacker; import org.pkl.core.PklBugException; +import org.pkl.core.runtime.VmReference.Access; import org.pkl.core.stdlib.AbstractRenderer; import org.pkl.core.stdlib.PklConverter; import org.pkl.core.util.pklbinary.PklBinaryCode; @@ -324,6 +325,38 @@ public void visitFunction(VmFunction value) { } } + @Override + public void visitReference(VmReference value) { + try { + packer.packArrayHeader(3); + packCode(PklBinaryCode.REFERENCE); + visit(value.getRootValue()); + packer.packArrayHeader(value.getPath().size()); + for (var access : value.getPath()) { + visit(access); + } + } catch (IOException e) { + throw PklBugException.unreachableCode(); + } + } + + @Override + public void visitReferenceAccess(Access value) { + try { + packer.packArrayHeader(3); + packCode(PklBinaryCode.REFERENCE_ACCESS); + if (value.isProperty()) { + packer.packString(value.getProperty()); + packer.packNil(); + } else { + packer.packNil(); + visit(value.getKey()); + } + } 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..a1c278f9b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmReference.java @@ -0,0 +1,446 @@ +/* + * Copyright © 2025 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 java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.organicdesign.fp.collections.RrbTree; +import org.organicdesign.fp.collections.RrbTree.ImRrbt; +import org.pkl.core.Composite; +import org.pkl.core.Modifier; +import org.pkl.core.PClass; +import org.pkl.core.PClassInfo; +import org.pkl.core.PNull; +import org.pkl.core.PObject; +import org.pkl.core.PType; +import org.pkl.core.TypeAlias; +import org.pkl.core.util.Nullable; + +public final class VmReference extends VmValue { + + // candidate types can only be: PType.Class, PType.Alias (only preservedAliasTypes), + // PType.StringLiteral, or PType.UNKNOWN + private final Set candidateTypes; + // TODO figure out what to do with constraints + // maybe: start w/ errors and open up to analyzable constraints later + private final VmValue rootValue; + private final ImRrbt path; + private boolean forced = false; + + private static final PType nullType = new PType.Class(BaseModule.getNullClass().export()); + private static final Set intAliasTypes = getIntAliasTypes(); + private static final Set preservedAliasTypes = intAliasTypes; + + private static Set getIntAliasTypes() { + var types = new HashSet(); + for (var t : BaseModule.getIntTypeAliases()) { + types.add(t.export()); + } + return types; + } + + public VmReference(VmValue rootValue) { + this(Set.of(new PType.Class(rootValue.getVmClass().export())), rootValue, RrbTree.empty()); + } + + public VmReference(Set candidateTypes, VmValue rootValue, ImRrbt path) { + this.candidateTypes = candidateTypes; + this.rootValue = rootValue; + this.path = path; + } + + public Set getCandidateTypes() { + return candidateTypes; + } + + public VmValue getRootValue() { + return rootValue; + } + + public List getPath() { + return path; + } + + // 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.NOTHING, PType.Function, and PType.TypeVariable + private static Set simplifyType(PType type, @Nullable PClass moduleClass) { + var types = new HashSet(); + simplifyType(type, moduleClass, types); + return types; + } + + private static void simplifyType(PType type, @Nullable PClass moduleClass, Set result) { + if (type == PType.UNKNOWN || type instanceof PType.StringLiteral) { + result.add(type); + } else if (type instanceof PType.Class klass) { + if (klass.getTypeArguments().isEmpty()) { + result.add(klass); + } else { + var typeArgs = new ArrayList(klass.getTypeArguments().size()); + for (var arg : klass.getTypeArguments()) { + var tt = new ArrayList<>(simplifyType(arg, moduleClass)); + typeArgs.add(tt.size() == 1 ? tt.get(0) : new PType.Union(tt)); + } + result.add(new PType.Class(klass.getPClass(), typeArgs)); + } + } else if (type instanceof PType.Nullable nullable) { + simplifyType(nullable.getBaseType(), moduleClass, result); + result.add(nullType); + } else if (type instanceof PType.Constrained constrained) { + simplifyType(constrained.getBaseType(), moduleClass, result); + } else if (type instanceof PType.Alias alias) { + if (preservedAliasTypes.contains(alias.getTypeAlias())) { + result.add(alias); + } else { + simplifyType(alias.getAliasedType(), alias.getTypeAlias().getModuleClass(), result); + } + } else if (type instanceof PType.Union union) { + for (var t : union.getElementTypes()) { + simplifyType(t, moduleClass, result); + } + } else if (type == PType.MODULE && moduleClass != null) { + result.add(new PType.Class(moduleClass)); + } + } + + public @Nullable VmReference withPropertyAccess(Identifier property) { + Set candidates = new HashSet<>(); + for (var t : candidateTypes) { + getCandidatePropertyType(t, property.toString(), candidates); + } + if (candidates.isEmpty()) { + return null; // no valid property found + } else if (candidates.contains(PType.UNKNOWN)) { + // optimization: unknown allows all references, erase all candidates to only unknown + candidates = Set.of(PType.UNKNOWN); + } + return new VmReference( + candidates, rootValue, path.append(Access.property(property.toString()))); + } + + public @Nullable VmReference withSubscriptAccess(Object key) { + Set candidates = new HashSet<>(); + for (var t : candidateTypes) { + getCandidateSubscriptType(t, key, candidates); + } + if (candidates.isEmpty()) { + return null; // no valid subscript found + } else if (candidates.contains(PType.UNKNOWN)) { + // optimization: unknown allows all references, erase all candidates to only unknown + candidates = Set.of(PType.UNKNOWN); + } + return new VmReference(candidates, rootValue, path.append(Access.subscript(key))); + } + + @SuppressWarnings("DuplicatedCode") + private static void getCandidatePropertyType(PType type, String property, Set result) { + if (type == PType.UNKNOWN) { + result.add(type); + return; + } + if (!(type instanceof PType.Class klass)) { + return; + } + if (klass.getPClass().getInfo() == PClassInfo.Dynamic) { + result.add(PType.UNKNOWN); + return; + } + if (klass.getPClass().getInfo() == PClassInfo.Listing + || klass.getPClass().getInfo() == PClassInfo.List + || klass.getPClass().getInfo() == PClassInfo.Mapping + || klass.getPClass().getInfo() == PClassInfo.Map) { + return; + } + // Typed + var prop = klass.getPClass().getAllProperties().get(property); + if (prop == null || prop.isExternal()) { + return; + } + simplifyType(prop.getType(), klass.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 klass)) { + return; + } + if (klass.getPClass().getInfo() == PClassInfo.Dynamic) { + result.add(PType.UNKNOWN); + return; + } + if (klass.getPClass().getInfo() == PClassInfo.Listing + || klass.getPClass().getInfo() == PClassInfo.List) { + if (key instanceof Long) { + simplifyType(klass.getTypeArguments().get(0), klass.getPClass().getModuleClass(), result); + } + return; + } + if (klass.getPClass().getInfo() == PClassInfo.Mapping + || klass.getPClass().getInfo() == PClassInfo.Map) { + var typeArgs = klass.getTypeArguments(); + var keyTypes = simplifyType(typeArgs.get(0), klass.getPClass().getModuleClass()); + for (var kt : keyTypes) { + if (kt == PType.UNKNOWN + || (kt instanceof PType.Class klazz + && klazz.getPClass().getInfo() == PClassInfo.forValue(key)) + || (kt instanceof PType.StringLiteral stringLiteral + && stringLiteral.getLiteral().equals(key))) { + simplifyType(typeArgs.get(1), klass.getPClass().getModuleClass(), result); + return; + } + } + } + } + + public boolean checkType(PType type, @Nullable PClass moduleClass) { + // fast path: if this could be unknown, any type is accepted + if (candidateTypes.contains(PType.UNKNOWN)) { + return true; + } + + // check if any candidate type is a subtype of and check type + for (var t : simplifyType(type, moduleClass)) { + for (var c : candidateTypes) { + if (!isSubtype(c, t)) return false; + } + } + return true; + } + + private static boolean isSubtype(PType a, PType b) { + // checks if A is a subtype of B + // cases (A -> B) + // * StringLiteral -> StringLiteral: if literals are the same + // * StringLiteral -> Class: B is String + // * 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 + + 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 (intAliasTypes.contains(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; + } + return false; + } + + @Override + public VmClass getVmClass() { + return BaseModule.getReferenceClass(); + } + + @Override + public void force(boolean allowUndefinedValues) { + if (forced) return; + + forced = true; + + rootValue.force(allowUndefinedValues); + for (var elem : path) { + VmValue.force(elem, allowUndefinedValues); + } + } + + @Override + public Composite export() { + var pathList = new ArrayList<>(path.size()); + for (Access elem : path) { + pathList.add(elem.export()); + } + + return new PObject( + getVmClass().getPClassInfo(), + Map.of( + "candidateTypes", candidateTypes, + "rootValue", rootValue.export(), + "path", pathList)); + } + + @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(Object obj) { + if (this == obj) return true; + if (!(obj instanceof VmReference other)) return false; + + return Objects.equals(candidateTypes, other.getCandidateTypes()) + && rootValue.equals(other.getRootValue()) + && path.equals(other.getPath()); + } + + public static class Access extends VmValue { + private final @Nullable String property; + private final @Nullable Object key; + + public static Access property(String property) { + return new Access(property, null); + } + + public static Access subscript(Object key) { + return new Access(null, key); + } + + private Access(@Nullable String property, @Nullable Object key) { + this.property = property; + this.key = key; + } + + public String getProperty() { + assert property != null; + return property; + } + + public Object getKey() { + assert key != null; + return key; + } + + public boolean isProperty() { + return property != null; + } + + public boolean isSubscript() { + return key != null; + } + + @Override + public VmClass getVmClass() { + return BaseModule.getReferenceAccessClass(); + } + + @Override + public void force(boolean allowUndefinedValues) { + if (key != null) { + VmValue.force(key, allowUndefinedValues); + } + } + + @Override + public Object export() { + return new PObject( + getVmClass().getPClassInfo(), + Map.of( + "property", + property == null ? PNull.getInstance() : property, + "key", + key == null ? PNull.getInstance() : VmValue.export(key))); + } + + @Override + public void accept(VmValueVisitor visitor) { + visitor.visitReferenceAccess(this); + } + + @Override + public T accept(VmValueConverter converter, Iterable path) { + return converter.convertReferenceAccess(this, path); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Access other)) return false; + return Objects.equals(property, other.getProperty()) && Objects.equals(key, other.getKey()); + } + } +} 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 8ed4889c5..b6083251b 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 @@ -228,7 +228,8 @@ public TypeAlias export() { 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..ddc70e96f 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-2025 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,8 @@ VmRegex.class, VmTypeAlias.class, VmObjectLike.class, - VmValue.class + VmReference.class, + VmReference.Access.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..5d218f589 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,8 +85,9 @@ public String toString() { T convertFunction(VmFunction value, Iterable path); - /** Returns with an empty identifier if the second value is a RenderDirective */ - Pair convertProperty(ClassProperty property, Object value, Iterable path); + T convertReference(VmReference value, Iterable path); + + T convertReferenceAccess(VmReference.Access value, Iterable path); default T convert(Object value, Iterable path) { if (value instanceof VmValue vmValue) { 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..370bc50dc 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 void visitNull(VmNull value) { append("null"); } + @Override + public void visitReference(VmReference value) { + contexts.push(Context.EXPLICIT); + append("Reference("); + visit(value.getRootValue()); + append(")"); + for (var elem : value.getPath()) { + visit(elem); + } + contexts.pop(); + } + + @Override + public void visitReferenceAccess(VmReference.Access value) { + if (value.isProperty()) { + append("."); + writeIdentifier(value.getProperty()); + } else { + append("["); + visit(value.getKey()); + append("]"); + } + } + 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..0b2336204 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 @@ -60,6 +60,10 @@ public interface VmValueVisitor { void visitFunction(VmFunction value); + void visitReference(VmReference value); + + void visitReferenceAccess(VmReference.Access 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 4fedf8ffa..8e4abc7f9 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 @@ -33,6 +33,7 @@ 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,16 @@ private void doVisitElement( currSourceSection = prevSourceSection; } + @Override + public void visitReference(VmReference value) { + cannotRenderTypeAddConverter(value); + } + + @Override + public void visitReferenceAccess(VmReference.Access 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 53a225a96..0371b61d2 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,8 @@ 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 final @Nullable VmFunction referenceAccessConverter; private PklConverter( VmMapping converters, VmMapping convertPropertyTransformers, Object rendererOrParser) { @@ -76,6 +78,8 @@ private PklConverter( nullConverter = typeConverters.get(BaseModule.getNullClass()); classConverter = typeConverters.get(BaseModule.getClassClass()); typeAliasConverter = typeConverters.get(BaseModule.getTypeAliasClass()); + referenceConverter = typeConverters.get(BaseModule.getReferenceClass()); + referenceAccessConverter = typeConverters.get(BaseModule.getReferenceAccessClass()); } public static final PklConverter NOOP = @@ -198,10 +202,18 @@ public Object convertTypeAlias(VmTypeAlias value, Iterable path) { public Object convertNull(VmNull value, Iterable path) { return doConvert(value, path, nullConverter); } + + public Object convertReference(VmReference value, Iterable path) { + return doConvert(value, path, referenceConverter); + } @Override + public Object convertReferenceAccess(VmReference.Access value, Iterable path) { + return doConvert(value, path, referenceAccessConverter); + } + public Pair convertProperty( - ClassProperty property, Object value, Iterable path) { + ClassProperty property, Object value, Iterable path) { var name = property.getName(); var annotations = property.getAllAnnotations(false); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/BaseNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/BaseNodes.java index 84cb28af7..fa6362bcb 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/BaseNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/BaseNodes.java @@ -24,9 +24,11 @@ import org.pkl.core.runtime.VmList; 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.VmTyped; import org.pkl.core.runtime.VmUtils; +import org.pkl.core.runtime.VmValue; import org.pkl.core.stdlib.ExternalMethod0Node; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.ExternalMethod2Node; @@ -151,4 +153,11 @@ protected VmList eval(VirtualFrame frame, VmTyped self, Object args) { throw exceptionBuilder().bug("Node `BaseNodes.Bytes` should never be executed.").build(); } } + + public abstract static class Reference extends ExternalMethod1Node { + @Specialization + protected VmReference eval(VirtualFrame frame, VmTyped self, VmValue rootValue) { + return new VmReference(rootValue); + } + } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ReferenceAccessNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ReferenceAccessNodes.java new file mode 100644 index 000000000..8adf4b8d4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ReferenceAccessNodes.java @@ -0,0 +1,61 @@ +/* + * Copyright © 2025 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.base; + +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmReference; +import org.pkl.core.stdlib.ExternalPropertyNode; + +public class ReferenceAccessNodes { + private ReferenceAccessNodes() {} + + public abstract static class isProperty extends ExternalPropertyNode { + + @Specialization + protected boolean eval(VmReference.Access self) { + return self.isProperty(); + } + } + + public abstract static class property extends ExternalPropertyNode { + @Specialization + protected Object eval(VmReference.Access self) { + if (!self.isProperty()) { + return VmNull.withoutDefault(); + } + return self.getProperty(); + } + } + + public abstract static class isSubscript extends ExternalPropertyNode { + + @Specialization + protected boolean eval(VmReference.Access self) { + return self.isSubscript(); + } + } + + public abstract static class key extends ExternalPropertyNode { + @Specialization + protected Object eval(VmReference.Access self) { + if (!self.isSubscript()) { + return VmNull.withoutDefault(); + } + return self.getKey(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ReferenceNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ReferenceNodes.java new file mode 100644 index 000000000..a7bcf98df --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ReferenceNodes.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2025 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.base; + +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.runtime.VmList; +import org.pkl.core.runtime.VmReference; +import org.pkl.core.runtime.VmValue; +import org.pkl.core.stdlib.ExternalMethod0Node; + +public class ReferenceNodes { + private ReferenceNodes() {} + + public abstract static class getRootValue extends ExternalMethod0Node { + @Specialization + protected VmValue eval(VmReference self) { + return self.getRootValue(); + } + } + + 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/protobuf/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java index 06b7b76ea..c27dff234 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 @@ -58,6 +58,7 @@ 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,18 @@ public void visitNull(VmNull value) { builder.append(value); } + @Override + public void visitReference(VmReference value) { + writePropertyName(); + builder.append(value); + } + + @Override + public void visitReferenceAccess(VmReference.Access 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/util/pklbinary/PklBinaryCode.java b/pkl-core/src/main/java/org/pkl/core/util/pklbinary/PklBinaryCode.java index a4004085d..d89627bc0 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,8 @@ public enum PklBinaryCode { TYPEALIAS((byte) 0x0D), FUNCTION((byte) 0x0E), BYTES((byte) 0x0F), + REFERENCE((byte) 0x20), + REFERENCE_ACCESS((byte) 0x21), PROPERTY((byte) 0x10), ENTRY((byte) 0x11), 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..6482e49b4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl @@ -0,0 +1,66 @@ +import "pkl:math" + +abstract class Resource { + name: String +} + +class A extends Resource { + id: String + hidden outputs: AProperties +} + +class AProperties { + foo: Int + someMapping: Mapping + someMap: Map + someListing: Listing + someList: List +} + +class B extends Resource { + aId: String | Reference + aProperties: AProperties | Reference + aValues: Listing> +} + +a: A = new { + name = "a" + id = "some-a-value" +} + +local aRef: Reference = Reference(a) + +b: B = new { + name = "b" + aId = aRef.id + aProperties = aRef.outputs + aValues { + aRef.outputs.foo + aRef.outputs.someMapping["key"] + aRef.outputs.someMap[123] + aRef.outputs.someListing[0] + aRef.outputs.someList[math.maxInt] + } +} + +function renderReference(ref: Reference): String = + if (ref.getRootValue() is Resource) + let (resource = ref.getRootValue() as Resource) + let ( + path = + ref + .getPath() + .toList() + .map((elem) -> if (elem.isProperty) ".\(elem.property)" else "[\(elem.key)]") + ) + "${\(resource.name)\(path.join(""))}" + else + throw("can only render references rooted to Resource instances") + +output { + renderer { + converters { + [Reference] = (it) -> renderReference(it) + } + } +} 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..6d2229aa4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/reference.pcf @@ -0,0 +1,16 @@ +a { + name = "a" + id = "some-a-value" +} +b { + name = "b" + aId = "${a.id}" + aProperties = "${a.outputs}" + aValues { + "${a.outputs.foo}" + "${a.outputs.someMapping[key]}" + "${a.outputs.someMap[123]}" + "${a.outputs.someListing[0]}" + "${a.outputs.someList[9223372036854775807]}" + } +} diff --git a/stdlib/base.pkl b/stdlib/base.pkl index 23dd37c3f..02149ff6f 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -3770,3 +3770,47 @@ external class Bytes extends Any { /// Converts these bytes into a [List]. external function toList(): List } + +/// Creates a reference to the given value. +@Since { version = "0.31.0" } +external const function Reference(rootValue: T): Reference + +/// A reference to a value that may not exist at evaluation time. +/// +/// Reference provides a type-checked way to produce references to data that is not known at time of +/// evaluation. Different configuration systems may ascribe different meanings to references and +/// require that textuat rendering is formatted in specific ways. +/// +/// Limitations: +/// * Most type constraints are ignored (erased) when referenced. +/// * Properties marked `external` may not be referenced. +@Since { version = "0.31.0" } +external class Reference extends Object { + // these are methods instead of properties to avoid collisions with referenced properties + + /// The value from which the Reference is rooted. + external function getRootValue(): Any + + /// The path of access from [rootValue] to the referent. + external function getPath(): List +} + +/// Represents a property or subscript access as part of a [Reference]. +@Since { version = "0.31.0" } +external class ReferenceAccess { + /// If the access is access to a property. + external isProperty: Boolean + + /// The property that was accessed. + /// + /// If non-null, this is a property access. If `null` this is a subscript access. + external property: String(key == null)? + + /// If the access is access via subscript. + external isSubscript: Boolean + + /// The subscript key that was accessed. + /// + /// May be null even when [property] is null, which represents a subscript access with key `null`. + external key: Any +}