From cb01d6275bc97ef1fd97024a1ce32802a103b5e3 Mon Sep 17 00:00:00 2001 From: Jen Basch Date: Tue, 25 Aug 2026 22:29:34 -0700 Subject: [PATCH] Correctly type check `Class` (#1698) --- docs/modules/release-notes/pages/0.33.adoc | 10 ++ .../java/org/pkl/core/ast/type/TypeNode.java | 106 ++++++++++++++++-- .../pkl/core/ast/type/UnresolvedTypeNode.java | 3 +- .../ast/type/VmTypeMismatchException.java | 63 +++++++++++ .../pkl/core/runtime/CommandSpecParser.java | 2 +- .../org/pkl/core/errorMessages.properties | 3 + .../input-helper/types/classTypeA.pkl | 2 + .../input-helper/types/classTypeB.pkl | 2 + .../input/types/classType.pkl | 36 ++++++ .../input/types/classTypeErr1.pkl | 3 + .../input/types/classTypeErr10.pkl | 3 + .../input/types/classTypeErr11.pkl | 3 + .../input/types/classTypeErr12.pkl | 5 + .../input/types/classTypeErr13.pkl | 5 + .../input/types/classTypeErr2.pkl | 6 + .../input/types/classTypeErr3.pkl | 6 + .../input/types/classTypeErr4.pkl | 3 + .../input/types/classTypeErr5.pkl | 3 + .../input/types/classTypeErr6.pkl | 3 + .../input/types/classTypeErr7.pkl | 3 + .../input/types/classTypeErr8.pkl | 3 + .../input/types/classTypeErr9.pkl | 3 + .../output/types/classType.pcf | 16 +++ .../output/types/classTypeErr1.err | 14 +++ .../output/types/classTypeErr10.err | 14 +++ .../output/types/classTypeErr11.err | 16 +++ .../output/types/classTypeErr12.err | 16 +++ .../output/types/classTypeErr13.err | 16 +++ .../output/types/classTypeErr2.err | 15 +++ .../output/types/classTypeErr3.err | 15 +++ .../output/types/classTypeErr4.err | 16 +++ .../output/types/classTypeErr5.err | 16 +++ .../output/types/classTypeErr6.err | 16 +++ .../output/types/classTypeErr7.err | 16 +++ .../output/types/classTypeErr8.err | 16 +++ .../output/types/classTypeErr9.err | 14 +++ .../test/kotlin/org/pkl/core/EvaluatorTest.kt | 18 +++ stdlib/base.pkl | 2 +- stdlib/settings.pkl | 4 +- 39 files changed, 503 insertions(+), 13 deletions(-) create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeA.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeB.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classType.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr1.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr10.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr11.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr12.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr13.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr2.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr3.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr4.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr5.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr6.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr7.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr8.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr9.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classType.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr1.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr10.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr11.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr12.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr13.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr2.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr3.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr4.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr5.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr6.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr7.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr8.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr9.err diff --git a/docs/modules/release-notes/pages/0.33.adoc b/docs/modules/release-notes/pages/0.33.adoc index 903b9c188..32337e92a 100644 --- a/docs/modules/release-notes/pages/0.33.adoc +++ b/docs/modules/release-notes/pages/0.33.adoc @@ -59,6 +59,16 @@ Native `pkl` and `pkldoc` binaries for Intel Mac systems are no longer published To continue running new Pkl releases on these systems, use an appropriate Java runtime and the `jpkl` and `jpkldoc` Java executables. +=== Type check changes for `Class` + +In prior versions of Pkl, type arguments to the `Class` type were erased. +Any `Class` value would typecheck against `Class` for any value of `T`. + +In Pkl 0.33, this erasure has been removed. +A `Class` value typechecked against `Class` must be a subclass of `T`. + +If `T` does not resolve to a class type (i.e. it is a union type, nullable type, string literal type, parameterized type, or `nothing`), the type check will always fail. + === XXX == Bug Fixes [small]#🐜# 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 933188582..1fa9157e2 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 @@ -33,6 +33,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import org.jspecify.annotations.NonNull; @@ -61,7 +62,8 @@ import org.pkl.core.util.MutableReference; public abstract class TypeNode extends PklNode { - public interface ClassTypeNode { + /** Type node that corresponds to a user-defined class (or module class). */ + public interface UserClassTypeNode { VmClass getVmClass(); } @@ -410,8 +412,7 @@ public abstract class TypeNode extends PklNode { } /** The `module` type for a final module. */ - public static final class FinalModuleTypeNode extends ObjectSlotTypeNode - implements ClassTypeNode { + public static final class FinalModuleTypeNode extends ObjectSlotTypeNode { private final VmClass moduleClass; public FinalModuleTypeNode(SourceSection sourceSection, VmClass moduleClass) { @@ -465,8 +466,7 @@ public abstract class TypeNode extends PklNode { } /** The `module` type for an open module. */ - public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode - implements ClassTypeNode { + public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode { private final VmClass moduleClass; // only used by getVmClass() @Child private ExpressionNode getModuleNode; @@ -651,7 +651,8 @@ public abstract class TypeNode extends PklNode { * String/Boolean/Int/Float and their supertypes, only `VmValue`s can possibly pass its type * check. */ - public static final class FinalClassTypeNode extends ObjectSlotTypeNode implements ClassTypeNode { + public static final class FinalClassTypeNode extends ObjectSlotTypeNode + implements UserClassTypeNode { private final VmClass clazz; public FinalClassTypeNode(SourceSection sourceSection, VmClass clazz) { @@ -708,7 +709,7 @@ public abstract class TypeNode extends PklNode { * check. */ public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode - implements ClassTypeNode { + implements UserClassTypeNode { protected final VmClass clazz; public NonFinalClassTypeNode(SourceSection sourceSection, VmClass clazz) { @@ -3164,6 +3165,97 @@ public abstract class TypeNode extends PklNode { } } + public abstract static class ClassClassTypeNode extends ObjectSlotTypeNode { + + @Child private TypeNode typeNode; + @CompilationFinal private boolean initialized = false; + @CompilationFinal private @Nullable VmClass clazz = null; + + public ClassClassTypeNode(SourceSection sourceSection, TypeNode typeNode) { + super(sourceSection); + this.typeNode = typeNode; + } + + private void initVmClass() { + if (initialized) return; + + CompilerDirectives.transferToInterpreterAndInvalidate(); + initialized = true; + + var node = typeNode; + while (node instanceof TypeAliasTypeNode typeAliasTypeNode) { + node = typeAliasTypeNode.getAliasedTypeNode(); + } + + if (node instanceof UnknownTypeNode || node instanceof TypeVariableNode) { + clazz = BaseModule.getAnyClass(); + } else if (!node.isParametric()) { + clazz = node.getVmClass(); + } + } + + @Override + public VmClass getVmClass() { + return BaseModule.getClassClass(); + } + + @Specialization + protected Object eval(VmClass value) { + // safe to init clazz here (instead of on init and typealias instantiate) + // because in the typealias case this node will never execute prior to instantiation + initVmClass(); + + // Fast path: all classes match Class / Class / Class. + // In this case, skip the subclass check and behave like a bare `Class` type annotation. + if (clazz == BaseModule.getAnyClass()) { + return value; + } + + // clazz will be null iff the type arg is a not a valid class type + if (clazz == null) { + throw new VmTypeMismatchException.ClassType(sourceSection, value, typeNode.doExport()); + } + + if (!value.isSubclassOf(clazz)) { + throw new VmTypeMismatchException.ClassType(sourceSection, value, clazz); + } + + return value; + } + + @Fallback + protected Object fallback(Object value) { + throw typeMismatch(value, BaseModule.getClassClass()); + } + + @Override + protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) { + if (visitTypeArguments) { + return consumer.accept(this) && typeNode.acceptTypeNode(visitTypeArguments, consumer); + } + return consumer.accept(this); + } + + @Override + protected boolean doIsEquivalentTo(TypeNode other) { + if (!(other instanceof ClassClassTypeNode classClassTypeNode)) { + return false; + } + + return Objects.equals(clazz, classClassTypeNode.clazz); + } + + @Override + public VmList getTypeArgumentMirrors() { + return VmList.of(typeNode.getMirror()); + } + + @Override + protected PType doExport() { + return new PType.Class(BaseModule.getClassClass().export(), typeNode.doExport()); + } + } + public abstract static class ValidatingObjectSlotTypeNode extends ObjectSlotTypeNode { protected ValidatingObjectSlotTypeNode(SourceSection sourceSection) { 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 22544d37b..16cbaf407 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 @@ -278,9 +278,8 @@ public abstract class UnresolvedTypeNode extends PklNode { return FunctionNClassTypeNodeGen.create(sourceSection, resolvedTypeArgumentNodes); } - // erase `x: Class` to `x: Class` for now (cf. function types) if (clazz.isClassClass()) { - return new FinalClassTypeNode(sourceSection, clazz); + return ClassClassTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); } if (clazz.isVarArgsClass()) { 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 4443ea261..deeab0f34 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 @@ -169,6 +169,69 @@ public abstract class VmTypeMismatchException extends ControlFlowException { } } + public static final class ClassType extends VmTypeMismatchException { + + private final String renderedExpected; + private final @Nullable VmClass expectedClass; + + public ClassType(SourceSection sourceSection, VmClass actualClass, VmClass expectedClass) { + super(sourceSection, actualClass); + this.expectedClass = expectedClass; + renderedExpected = "Class<" + expectedClass + ">"; + } + + public ClassType(SourceSection sourceSection, VmClass actualClass, PType expectedType) { + super(sourceSection, actualClass); + this.expectedClass = null; + renderedExpected = "Class<" + expectedType + ">"; + } + + @Override + @TruffleBoundary + public void buildMessage( + AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { + var actualClass = (VmClass) actualValue; + var renderedActualClass = "Class<" + actualClass + ">"; + + // give better error than "expected Class, but got Class" in case of naming + // conflict + if (expectedClass != null + && actualClass.getQualifiedName().equals(expectedClass.getQualifiedName())) { + var actualModuleUri = actualClass.getModule().getModuleInfo().getModuleKey().getUri(); + var expectedModuleUri = expectedClass.getModule().getModuleInfo().getModuleKey().getUri(); + + builder + .append( + ErrorMessages.createIndented( + actualClass.getPClassInfo().isModuleClass() + ? "typeMismatchVersionConflict1" + : "typeMismatchVersionConflict2", + indent, + renderedExpected, + expectedModuleUri, + actualModuleUri)) + .append("\n"); + return; + } + + builder.append( + ErrorMessages.createIndented( + "typeMismatch", indent, renderedExpected, renderedActualClass)); + } + + @Override + protected Boolean hasHint() { + return expectedClass == null; + } + + @Override + @TruffleBoundary + public void buildHint(AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { + if (expectedClass != null) return; + builder.append(ErrorMessages.createIndented("classTypeMismatchHint", indent)); + } + } + public static final class Constraint extends VmTypeMismatchException { private final SourceSection constraintBodySourceSection; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java index 720dc8944..915cf8043 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java @@ -173,7 +173,7 @@ public final class CommandSpecParser { if (optionsTypeNode instanceof TypeNode.TypedTypeNode) { return BaseModule.getTypedClass(); } - if (!(optionsTypeNode instanceof TypeNode.ClassTypeNode node)) { + if (!(optionsTypeNode instanceof TypeNode.UserClassTypeNode node)) { throw exceptionBuilder() .withSourceSection(optionsTypeNode.getSourceSection()) .evalError( 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 7d335d4e0..fea731288 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -358,6 +358,9 @@ Expected value of type `{0}`, but got a different `{1}`. typeMismatchBecause=\ * Value is not of type `{0}` because: +classTypeMismatchHint=\ +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + typeConstraintViolated=\ Type constraint `{0}` violated. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeA.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeA.pkl new file mode 100644 index 000000000..162589ac9 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeA.pkl @@ -0,0 +1,2 @@ +module classType +class Foo diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeB.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeB.pkl new file mode 100644 index 000000000..162589ac9 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/types/classTypeB.pkl @@ -0,0 +1,2 @@ +module classType +class Foo diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classType.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classType.pkl new file mode 100644 index 000000000..a231a5c81 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classType.pkl @@ -0,0 +1,36 @@ +open module classType + +import "pkl:reflect" + +open class A +open class B +class C extends A +class D extends module +typealias E = A +typealias BB = B + +res0 = C is Class +res0a = C is Class<(C)> +res0b = C is Class<((C))> +res1 = C is Class +res2 = C is Class + +res3 = C is Class +res4 = C is Class +res5 = C is Class + +res15 = D is Class +res16 = D is Class +res17 = D is Class +res18 = D is Class +res19 = D is Class + +typealias F = List> +res20 = List(A, C) is F +res21 = List(new A {}, new B {}, new C {}).filterIsInstance(A).length + +hidden $res22: Class = A +res22 = + let (t = reflect.Module(module).moduleClass.properties["$res22"].type as reflect.DeclaredType) + let (arg = t.typeArguments.single as reflect.DeclaredType) + "\(t.referent.enclosingDeclaration.uri)#\(t.referent.name)<\(arg.referent.enclosingDeclaration.uri)#\(arg.referent.name)>" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr1.pkl new file mode 100644 index 000000000..916938b92 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr1.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res2 = C as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr10.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr10.pkl new file mode 100644 index 000000000..8bc812c23 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr10.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res14 = C as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr11.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr11.pkl new file mode 100644 index 000000000..99384be22 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr11.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res22 = List(A, C) as F diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr12.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr12.pkl new file mode 100644 index 000000000..68612b000 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr12.pkl @@ -0,0 +1,5 @@ +extends "classType.pkl" + +typealias G = F + +res22 = List(A, C) as G diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr13.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr13.pkl new file mode 100644 index 000000000..59d40d60c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr13.pkl @@ -0,0 +1,5 @@ +extends "classType.pkl" + +typealias G = F + +res22 = List(A, C) as G diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr2.pkl new file mode 100644 index 000000000..f04ec56fb --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr2.pkl @@ -0,0 +1,6 @@ +extends "classType.pkl" + +import ".../input-helper/types/classTypeA.pkl" +import ".../input-helper/types/classTypeB.pkl" + +res6 = classTypeA.getClass() as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr3.pkl new file mode 100644 index 000000000..73d613993 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr3.pkl @@ -0,0 +1,6 @@ +extends "classType.pkl" + +import ".../input-helper/types/classTypeA.pkl" +import ".../input-helper/types/classTypeB.pkl" + +res7 = classTypeA.Foo as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr4.pkl new file mode 100644 index 000000000..6c0b69561 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr4.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res8 = C as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr5.pkl new file mode 100644 index 000000000..df58a6b4c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr5.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res9 = C as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr6.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr6.pkl new file mode 100644 index 000000000..2ddef66fd --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr6.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res10 = C as Class<"foo"> diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr7.pkl new file mode 100644 index 000000000..3f11ed90c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr7.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res11 = C as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr8.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr8.pkl new file mode 100644 index 000000000..b9761d53a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr8.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res12 = C as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr9.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr9.pkl new file mode 100644 index 000000000..9a60c56b7 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/types/classTypeErr9.pkl @@ -0,0 +1,3 @@ +extends "classType.pkl" + +res13 = C as Class diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/types/classType.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classType.pcf new file mode 100644 index 000000000..45970320b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classType.pcf @@ -0,0 +1,16 @@ +res0 = true +res0a = true +res0b = true +res1 = true +res2 = true +res3 = true +res4 = true +res5 = true +res15 = true +res16 = true +res17 = true +res18 = false +res19 = false +res20 = true +res21 = 2 +res22 = "pkl:base#Class" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr1.err new file mode 100644 index 000000000..ab8934b38 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr1.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +x | res2 = C as Class + ^^^^^^^^ +at classTypeErr1#res2 (file:///$snippetsDir/input/types/classTypeErr1.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/types/classTypeErr10.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr10.err new file mode 100644 index 000000000..d90642c6d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr10.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +x | res14 = C as Class + ^^^^^^^^^^^^^ +at classTypeErr10#res14 (file:///$snippetsDir/input/types/classTypeErr10.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/types/classTypeErr11.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr11.err new file mode 100644 index 000000000..c4ec17457 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr11.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +xx | typealias F = List> + ^^^^^^^^ +at classTypeErr11#res22 (file:///$snippetsDir/input/types/classType.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr12.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr12.err new file mode 100644 index 000000000..2d13d368f --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr12.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +xx | typealias F = List> + ^^^^^^^^ +at classTypeErr12#res22 (file:///$snippetsDir/input/types/classType.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr13.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr13.err new file mode 100644 index 000000000..752eb5e05 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr13.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +xx | typealias F = List> + ^^^^^^^^ +at classTypeErr13#res22 (file:///$snippetsDir/input/types/classType.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr2.err new file mode 100644 index 000000000..e4a895b24 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr2.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Module version conflict: Expected value of type `Class` defined by module `file:///$snippetsDir/input-helper/types/classTypeB.pkl`, but got type `Class` defined by module `file:///$snippetsDir/input-helper/types/classTypeA.pkl`. + + +x | res6 = classTypeA.getClass() as Class + ^^^^^^^^^^^^^^^^^ +at classTypeErr2#res6 (file:///$snippetsDir/input/types/classTypeErr2.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/types/classTypeErr3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr3.err new file mode 100644 index 000000000..d1bca6603 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr3.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Module version conflict: Expected value of type `Class` defined in module `file:///$snippetsDir/input-helper/types/classTypeB.pkl`, but got type `Class` defined in module `file:///$snippetsDir/input-helper/types/classTypeA.pkl`. + + +x | res7 = classTypeA.Foo as Class + ^^^^^^^^^^^^^^^^^^^^^ +at classTypeErr3#res7 (file:///$snippetsDir/input/types/classTypeErr3.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/types/classTypeErr4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr4.err new file mode 100644 index 000000000..be44eeac5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr4.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +x | res8 = C as Class + ^^^^^^^^^^^^ +at classTypeErr4#res8 (file:///$snippetsDir/input/types/classTypeErr4.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr5.err new file mode 100644 index 000000000..859aaa1e6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr5.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +x | res9 = C as Class + ^^^^^^^^^ +at classTypeErr5#res9 (file:///$snippetsDir/input/types/classTypeErr5.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr6.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr6.err new file mode 100644 index 000000000..0f5d16afa --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr6.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class<"foo">`, but got type `Class`. + +x | res10 = C as Class<"foo"> + ^^^^^^^^^^^^ +at classTypeErr6#res10 (file:///$snippetsDir/input/types/classTypeErr6.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr7.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr7.err new file mode 100644 index 000000000..c77d2978a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr7.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +x | res11 = C as Class + ^^^^^^^^^^^^^^ +at classTypeErr7#res11 (file:///$snippetsDir/input/types/classTypeErr7.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr8.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr8.err new file mode 100644 index 000000000..e60e1b6d5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr8.err @@ -0,0 +1,16 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +x | res12 = C as Class + ^^^^^^^^^^^^^^ +at classTypeErr8#res12 (file:///$snippetsDir/input/types/classTypeErr8.pkl) + +A `Class` type check can only succeed when its type argument is an un-parameterized class, `unknown`, `module`, or an alias to one of those types. + +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/types/classTypeErr9.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr9.err new file mode 100644 index 000000000..f2dd49574 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/classTypeErr9.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Expected value of type `Class`, but got type `Class`. + +x | res13 = C as Class + ^^^^^^^^^ +at classTypeErr9#res13 (file:///$snippetsDir/input/types/classTypeErr9.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/kotlin/org/pkl/core/EvaluatorTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorTest.kt index 6679603d8..783dbc975 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorTest.kt @@ -772,6 +772,24 @@ class EvaluatorTest { .doesNotThrowAnyException() } + @Test + fun `eval schema containing a parameterized Class type`() { + val evaluator = Evaluator.preconfigured() + val schema = + evaluator.evaluateSchema( + text( + """ + foo: Class + """ + .trimIndent() + ) + ) + val fooType = schema.moduleClass.properties["foo"]?.type as? PType.Class + assertThat(fooType?.pClass?.info).isEqualTo(PClassInfo.Class) + val argType = fooType?.typeArguments?.single() as? PType.Class + assertThat(argType?.pClass?.info).isEqualTo(PClassInfo.Int) + } + @Test fun `concurrent evals`() { val exceptions = mutableListOf() diff --git a/stdlib/base.pkl b/stdlib/base.pkl index 1008ebec5..231ad5d26 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -361,7 +361,7 @@ abstract class BaseValueRenderer { /// /// See [ConvertProperty] for detailed information. @Since { version = "0.31.0" } - convertPropertyTransformers: Mapping> + convertPropertyTransformers: Mapping, Mixin> /// The file extension associated with this output format, /// or [null] if this format does not have an extension. diff --git a/stdlib/settings.pkl b/stdlib/settings.pkl index 8cac8aead..5f4c9a5d4 100644 --- a/stdlib/settings.pkl +++ b/stdlib/settings.pkl @@ -26,8 +26,8 @@ /// 3. `~/.config/pkl/settings.pkl` /// 4. Path `pkl/settings.pkl` within the `$XDG_CONFIG_DIRS` search path /// (dirs separated by `:` on Unix, `;` on Windows). -/// 4. `/etc/xdg/pkl/settings.pkl` -/// 5. `~/.pkl/settings.pkl` (legacy location used by Pkl 0.32 and lower) +/// 5. `/etc/xdg/pkl/settings.pkl` +/// 6. `~/.pkl/settings.pkl` (legacy location used by Pkl 0.32 and lower) @ModuleInfo { minPklVersion = "0.33.0" } module pkl.settings