diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt index 79435e93..bac968d2 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt @@ -107,6 +107,140 @@ class CliTestRunnerTest { assertThat(err.toString()).isEqualTo("") } + @Test + fun `CliTestRunner with thrown error in facts`(@TempDir tempDir: Path) { + val code = + """ + amends "pkl:test" + + facts { + ["fail"] { + throw("uh oh") + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(code).toString() + val out = StringWriter() + val err = StringWriter() + val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val testOpts = CliTestOptions() + val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) + assertThatCode { runner.run() }.hasMessage("Tests failed.") + + assertThat(out.toString().stripFileAndLines(tempDir)) + .isEqualToNormalizingNewlines( + """ + module test + fail ❌ + Error: + –– Pkl Error –– + uh oh + + 5 | throw("uh oh") + ^^^^^^^^^^^^^^ + at test#facts["fail"][#1] + + """ + .trimIndent() + ) + assertThat(err.toString()).isEqualTo("") + } + + @Test + fun `CliTestRunner with thrown error in examples -- no expected output`(@TempDir tempDir: Path) { + val code = + """ + amends "pkl:test" + + examples { + ["fail"] { + throw("uh oh") + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(code).toString() + val out = StringWriter() + val err = StringWriter() + val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val testOpts = CliTestOptions() + val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) + assertThatCode { runner.run() }.hasMessage("Tests failed.") + + assertThat(out.toString().stripFileAndLines(tempDir)) + .isEqualTo( + """ + module test + fail ❌ + Error: + –– Pkl Error –– + uh oh + + 5 | throw("uh oh") + ^^^^^^^^^^^^^^ + at test#examples["fail"][#1] + + """ + .trimIndent() + ) + assertThat(err.toString()).isEqualTo("") + } + + @Test + fun `CliTestRunner with thrown error in examples -- existing expected output`( + @TempDir tempDir: Path + ) { + val code = + """ + amends "pkl:test" + + examples { + ["fail"] { + throw("uh oh") + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(code).toString() + tempDir + .resolve("test.pkl-expected.pcf") + .writeString( + """ + examples { + ["fail"] { + "never compared to" + } + } + """ + .trimIndent() + ) + val out = StringWriter() + val err = StringWriter() + val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val testOpts = CliTestOptions() + val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) + assertThatCode { runner.run() }.hasMessage("Tests failed.") + + assertThat(out.toString().stripFileAndLines(tempDir)) + .isEqualToNormalizingNewlines( + """ + module test + fail ❌ + Error: + –– Pkl Error –– + uh oh + + 5 | throw("uh oh") + ^^^^^^^^^^^^^^ + at test#examples["fail"][#1] + + """ + .trimIndent() + ) + assertThat(err.toString()).isEqualTo("") + } + @Test fun `CliTestRunner JUnit reports`(@TempDir tempDir: Path) { val code = @@ -149,6 +283,54 @@ class CliTestRunnerTest { ) } + @Test + fun `CliTestRunner JUnit reports, with thrown error`(@TempDir tempDir: Path) { + val code = + """ + amends "pkl:test" + + facts { + ["foo"] { + 9 == trace(9) + "foo" == "foo" + } + ["fail"] { + throw("uh oh") + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(code).toString() + val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val testOpts = CliTestOptions(junitDir = tempDir) + val runner = CliTestRunner(opts, testOpts) + assertThatCode { runner.run() }.hasMessageContaining("failed") + + val junitReport = tempDir.resolve("test.xml").readString().stripFileAndLines(tempDir) + assertThat(junitReport) + .isEqualTo( + """ + + + + + –– Pkl Error –– + uh oh + + 9 | throw("uh oh") + ^^^^^^^^^^^^^^ + at test#facts["fail"][#1] + + + + + + """ + .trimIndent() + ) + } + @Test fun `CliTestRunner duplicated JUnit reports`(@TempDir tempDir: Path) { val foo = diff --git a/pkl-core/src/main/java/org/pkl/core/ast/MemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/MemberNode.java index 548b542c..fb7c659a 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/MemberNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/MemberNode.java @@ -22,7 +22,6 @@ import java.util.function.Function; import org.pkl.core.ast.member.DefaultPropertyBodyNode; import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmLanguage; -import org.pkl.core.runtime.VmUtils; import org.pkl.core.util.Nullable; public abstract class MemberNode extends PklRootNode { @@ -57,19 +56,6 @@ public abstract class MemberNode extends PklRootNode { return new VmExceptionBuilder().withSourceSection(getHeaderSection()); } - /** - * If true, the property value computed by this node is not the final value exposed to user code - * but will still be amended. - * - *

Used to disable type check for to-be-amended properties. See {@link - * org.pkl.core.runtime.VmUtils#SKIP_TYPECHECK_MARKER}. IDEA: might be more appropriate to only - * skip constraints check - */ - protected final boolean shouldRunTypeCheck(VirtualFrame frame) { - return frame.getArguments().length != 4 - || frame.getArguments()[3] != VmUtils.SKIP_TYPECHECK_MARKER; - } - public boolean isUndefined() { return bodyNode instanceof DefaultPropertyBodyNode propBodyNode && propBodyNode.isUndefined(); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java index 8ed49b91..a4acd001 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java @@ -442,13 +442,21 @@ public final class AstBuilder extends AbstractAstBuilder { : doVisitNewExprWithInferredParent(ctx); } + // `new Listing {}` is sugar for: `new Listing {} as Listing` private Object doVisitNewExprWithExplicitParent(NewExprContext ctx, TypeContext typeCtx) { - return doVisitObjectBody( - ctx.objectBody(), - new GetParentForTypeNode( - createSourceSection(ctx), - visitType(typeCtx), - symbolTable.getCurrentScope().getQualifiedName())); + var parentType = visitType(typeCtx); + var expr = + doVisitObjectBody( + ctx.objectBody(), + new GetParentForTypeNode( + createSourceSection(ctx), + parentType, + symbolTable.getCurrentScope().getQualifiedName())); + if (typeCtx instanceof DeclaredTypeContext declaredTypeContext + && declaredTypeContext.typeArgumentList() != null) { + return new TypeCastNode(parentType.getSourceSection(), expr, parentType); + } + return expr; } private Object doVisitNewExprWithInferredParent(NewExprContext ctx) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPredicateMemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPredicateMemberNode.java index 389c5985..71bb230a 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPredicateMemberNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPredicateMemberNode.java @@ -103,7 +103,7 @@ public abstract class GeneratorPredicateMemberNode extends GeneratorMemberNode { var callTarget = member.getCallTarget(); value = callTarget.call(parent, owner, key); } - owner.setCachedValue(key, value); + owner.setCachedValue(key, value, member); } frame.setAuxiliarySlot(customThisSlot, value); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java index e9e557f3..800d95bc 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java @@ -150,8 +150,8 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode { @Fallback @TruffleBoundary - protected void fallback(Object parent) { - elementsEntriesFallback(parent, values[0], false); + protected Object fallback(Object parent) { + return elementsEntriesFallback(parent, values[0], false); } @ExplodeLoop diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java index 975f7632..b843ec01 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java @@ -285,7 +285,7 @@ public abstract class SpecializedObjectLiteralNode extends ObjectLiteralNode { } @TruffleBoundary - protected void elementsEntriesFallback( + protected Object elementsEntriesFallback( Object parent, @Nullable ObjectMember firstElemOrEntry, boolean isElementsOnly) { var parentIsClass = parent instanceof VmClass; var parentClass = parentIsClass ? (VmClass) parent : VmUtils.getClass(parent); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java index fd962ca0..09e42497 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java @@ -71,7 +71,7 @@ public final class ReadLocalPropertyNode extends ExpressionNode { if (result == null) { result = callNode.call(objReceiver, owner, property.getName()); - objReceiver.setCachedValue(property, result); + objReceiver.setCachedValue(property, result, property); } return result; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java index 349e6f11..021d2d8e 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java @@ -184,12 +184,12 @@ public final class ResolveVariableNode extends ExpressionNode { if (member != null) { var constantValue = member.getConstantValue(); if (constantValue != null) { - baseModule.setCachedValue(variableName, constantValue); + baseModule.setCachedValue(variableName, constantValue, member); return new ConstantValueNode(sourceSection, constantValue); } var computedValue = member.getCallTarget().call(baseModule, baseModule); - baseModule.setCachedValue(variableName, computedValue); + baseModule.setCachedValue(variableName, computedValue, member); return new ConstantValueNode(sourceSection, computedValue); } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java index ef42cda0..e8abcb89 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java @@ -118,7 +118,7 @@ public final class FunctionNode extends RegularMemberNode { var result = bodyNode.executeGeneric(frame); if (checkedReturnTypeNode != null) { - checkedReturnTypeNode.execute(frame, result); + return checkedReturnTypeNode.execute(frame, result); } return result; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ListingOrMappingTypeCastNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ListingOrMappingTypeCastNode.java new file mode 100644 index 00000000..83b9c3ab --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ListingOrMappingTypeCastNode.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2024 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.PklRootNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.VmTypeMismatchException; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.util.Nullable; + +/** Performs a typecast on a Mapping entry value, or a Listing element. */ +public class ListingOrMappingTypeCastNode extends PklRootNode { + + @Child private TypeNode typeNode; + private final String qualifiedName; + + public ListingOrMappingTypeCastNode( + VmLanguage language, FrameDescriptor descriptor, TypeNode typeNode, String qualifiedName) { + super(language, descriptor); + this.typeNode = typeNode; + this.qualifiedName = qualifiedName; + } + + public TypeNode getTypeNode() { + return typeNode; + } + + @Override + public SourceSection getSourceSection() { + return typeNode.getSourceSection(); + } + + @Override + public @Nullable String getName() { + return qualifiedName; + } + + @Override + public Object execute(VirtualFrame frame) { + try { + return typeNode.execute(frame, frame.getArguments()[2]); + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw e.toVmException(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/LocalTypedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/LocalTypedPropertyNode.java index 87784d7e..12fe0986 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/LocalTypedPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/LocalTypedPropertyNode.java @@ -66,8 +66,7 @@ public final class LocalTypedPropertyNode extends RegularMemberNode { unresolvedTypeNode = null; } var result = bodyNode.executeGeneric(frame); - typeNode.execute(frame, result); - return result; + return typeNode.execute(frame, result); } catch (VmTypeMismatchException e) { CompilerDirectives.transferToInterpreter(); throw e.toVmException(); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java index 773c9cf8..4cad0072 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java @@ -134,7 +134,8 @@ public final class ObjectMember extends Member { var skip = 0; var text = candidate.getCharacters(); var ch = text.charAt(skip); - while (ch == '=' || Character.isWhitespace(ch)) { + // body section of entries needs to chomp the ending delimiter too. + while ((ch == ']' && isEntry()) || ch == '=' || Character.isWhitespace(ch)) { ch = text.charAt(++skip); } return source.createSection(candidate.getCharIndex() + skip, candidate.getCharLength() - skip); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/PropertyTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/PropertyTypeNode.java index 2edabf7f..f38f1a98 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/PropertyTypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/PropertyTypeNode.java @@ -62,10 +62,9 @@ public final class PropertyTypeNode extends PklRootNode { } @Override - public @Nullable Object execute(VirtualFrame frame) { + public Object execute(VirtualFrame frame) { try { - typeNode.execute(frame, frame.getArguments()[2]); - return null; + return typeNode.execute(frame, frame.getArguments()[2]); } catch (VmTypeMismatchException e) { CompilerDirectives.transferToInterpreter(); throw e.toVmException(); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/TypeCheckedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeCheckedPropertyNode.java index 0b0038fa..4498c7f9 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/TypeCheckedPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeCheckedPropertyNode.java @@ -54,8 +54,8 @@ public abstract class TypeCheckedPropertyNode extends RegularMemberNode { var result = executeBody(frame); // TODO: propagate SUPER_CALL_MARKER to disable constraint (but not type) check - if (callNode != null && shouldRunTypeCheck(frame)) { - callNode.call(VmUtils.getReceiverOrNull(frame), property.getOwner(), result); + if (callNode != null && VmUtils.shouldRunTypeCheck(frame)) { + return callNode.call(VmUtils.getReceiverOrNull(frame), property.getOwner(), result); } return result; @@ -67,11 +67,11 @@ public abstract class TypeCheckedPropertyNode extends RegularMemberNode { var result = executeBody(frame); - if (shouldRunTypeCheck(frame)) { + if (VmUtils.shouldRunTypeCheck(frame)) { var property = getProperty(owner.getVmClass()); var typeAnnNode = property.getTypeNode(); if (typeAnnNode != null) { - callNode.call( + return callNode.call( typeAnnNode.getCallTarget(), VmUtils.getReceiverOrNull(frame), property.getOwner(), diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/TypedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/TypedPropertyNode.java index 1a139da7..d0330a82 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/TypedPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/TypedPropertyNode.java @@ -45,8 +45,9 @@ public final class TypedPropertyNode extends RegularMemberNode { @Override public Object execute(VirtualFrame frame) { var propertyValue = executeBody(frame); - if (shouldRunTypeCheck(frame)) { - typeCheckCallNode.call(VmUtils.getReceiver(frame), VmUtils.getOwner(frame), propertyValue); + if (VmUtils.shouldRunTypeCheck(frame)) { + return typeCheckCallNode.call( + VmUtils.getReceiver(frame), VmUtils.getOwner(frame), propertyValue); } return propertyValue; } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/IdentityMixinNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/IdentityMixinNode.java index eb0889d2..d376c04d 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/IdentityMixinNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/IdentityMixinNode.java @@ -66,7 +66,7 @@ public final class IdentityMixinNode extends PklRootNode { try { var argument = arguments[2]; if (argumentTypeNode != null) { - argumentTypeNode.execute(frame, argument); + return argumentTypeNode.execute(frame, argument); } return argument; } catch (VmTypeMismatchException e) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java index f2ee4958..501da72e 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java @@ -73,7 +73,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode { var result = module.getCachedValue(importName); if (result == null) { result = callNode.call(member.getCallTarget(), module, module, importName); - module.setCachedValue(importName, result); + module.setCachedValue(importName, result, member); } return (VmTyped) result; } @@ -94,7 +94,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode { var result = module.getCachedValue(typeName); if (result == null) { result = callNode.call(member.getCallTarget(), module, module, typeName); - module.setCachedValue(typeName, result); + module.setCachedValue(typeName, result, member); } return result; } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeCastNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeCastNode.java index 87e77916..ff280139 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeCastNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeCastNode.java @@ -17,10 +17,12 @@ package org.pkl.core.ast.type; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.util.LateInit; +@NodeInfo(shortName = "as") public final class TypeCastNode extends ExpressionNode { @Child private ExpressionNode valueNode; @Child private UnresolvedTypeNode unresolvedTypeNode; @@ -47,8 +49,7 @@ public final class TypeCastNode extends ExpressionNode { var value = valueNode.executeGeneric(frame); try { - typeNode.execute(frame, value); - return value; + return typeNode.execute(frame, value); } catch (VmTypeMismatchException e) { CompilerDirectives.transferToInterpreter(); throw e.toVmException(); 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 3eec0953..eaaac0af 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 @@ -32,6 +32,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.pkl.core.PType; import org.pkl.core.PType.StringLiteral; +import org.pkl.core.PklBugException; import org.pkl.core.TypeParameter; import org.pkl.core.ast.*; import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; @@ -39,11 +40,14 @@ import org.pkl.core.ast.expression.primary.GetModuleNode; import org.pkl.core.ast.frame.WriteFrameSlotNode; import org.pkl.core.ast.frame.WriteFrameSlotNodeGen; import org.pkl.core.ast.member.DefaultPropertyBodyNode; +import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.UntypedObjectMemberNode; import org.pkl.core.runtime.*; import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.EconomicSets; import org.pkl.core.util.LateInit; +import org.pkl.core.util.MutableBoolean; import org.pkl.core.util.Nonnull; import org.pkl.core.util.Nullable; @@ -67,16 +71,28 @@ public abstract class TypeNode extends PklNode { public abstract TypeNode initWriteSlotNode(int slot); /** - * Checks if `value` conforms to this type. If so, returns normally. Otherwise, throws a - * `VmTypeMismatchException`. + * Checks if {@code value} conforms to this type, and possibly casts it in the case of {@link + * MappingTypeNode} or {@link ListingTypeNode}. */ - public abstract void execute(VirtualFrame frame, Object value); + public abstract Object execute(VirtualFrame frame, Object value); /** - * Checks if `value` conforms to this type. If so, sets `slot` to `value`. Otherwise, throws a - * `VmTypeMismatchException`. + * Checks if {@code value} conforms to this type, and possibly casts its value. + * + *

If {@code value} is conforming, sets {@code slot} to {@code value}. Otherwise, throws a + * {@link VmTypeMismatchException}. */ - public abstract void executeAndSet(VirtualFrame frame, Object value); + public abstract Object executeAndSet(VirtualFrame frame, Object value); + + /** + * Checks if {@code value} conforms to this type. + * + *

In the case of a parameterized {@link VmObject} (e.g. {@link VmListing}), shallow-force and + * check its members. + */ + public Object executeEagerly(VirtualFrame frame, Object value) { + return execute(frame, value); + } // method arguments are used when default value contains a root node public @Nullable Object createDefaultValue( @@ -88,6 +104,12 @@ public abstract class TypeNode extends PklNode { return null; } + /** + * Visit child type nodes; but not paramaterized types (does not visit {@code String} in {@code + * Listing}). + */ + protected abstract boolean acceptTypeNode(TypeNodeConsumer consumer); + public static TypeNode forClass(SourceSection sourceSection, VmClass clazz) { return clazz.isClosed() ? new FinalClassTypeNode(sourceSection, clazz) @@ -126,6 +148,13 @@ public abstract class TypeNode extends PklNode { .build(); } + protected boolean isParametric() { + return false; + } + + /** Tells if this typenode is the same typecheck as the other typenode. */ + public abstract boolean isEquivalentTo(TypeNode other); + public @Nullable VmClass getVmClass() { return null; } @@ -179,10 +208,10 @@ public abstract class TypeNode extends PklNode { } @Override - public final void executeAndSet(VirtualFrame frame, Object value) { + public final Object executeAndSet(VirtualFrame frame, Object value) { execute(frame, value); - frame.setLong(slot, (long) value); + return value; } } @@ -196,10 +225,10 @@ public abstract class TypeNode extends PklNode { } @Override - public final void executeAndSet(VirtualFrame frame, Object value) { - execute(frame, value); - - frame.setObject(slot, value); + public final Object executeAndSet(VirtualFrame frame, Object value) { + var result = execute(frame, value); + frame.setObject(slot, result); + return result; } } @@ -229,9 +258,10 @@ public abstract class TypeNode extends PklNode { } @Override - public final void executeAndSet(VirtualFrame frame, Object value) { - execute(frame, value); - writeSlotNode.executeWithValue(frame, value); + public final Object executeAndSet(VirtualFrame frame, Object value) { + var result = execute(frame, value); + writeSlotNode.executeWithValue(frame, result); + return result; } } @@ -247,8 +277,14 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public boolean isEquivalentTo(TypeNode other) { + return other instanceof UnknownTypeNode; + } + + @Override + public Object execute(VirtualFrame frame, Object value) { // do nothing + return value; } public VmTyped getMirror() { @@ -259,6 +295,11 @@ public abstract class TypeNode extends PklNode { protected PType doExport() { return PType.UNKNOWN; } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } /** The `nothing` type. */ @@ -274,14 +315,17 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { CompilerDirectives.transferToInterpreter(); throw new VmTypeMismatchException.Nothing(sourceSection, value); } @Override - public void executeAndSet(VirtualFrame frame, Object value) { + public Object executeAndSet(VirtualFrame frame, Object value) { execute(frame, value); + // guaranteed to never run (execute will always throw). + CompilerDirectives.transferToInterpreter(); + throw PklBugException.unreachableCode(); } @Override @@ -294,10 +338,20 @@ public abstract class TypeNode extends PklNode { return MirrorFactories.nothingTypeFactory.create(null); } + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof NothingTypeNode; + } + @Override protected PType doExport() { return PType.NOTHING; } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } /** The `module` type for a final module. */ @@ -310,8 +364,8 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof VmTyped typed && typed.getVmClass() == moduleClass) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmTyped typed && typed.getVmClass() == moduleClass) return value; throw typeMismatch(value, moduleClass); } @@ -321,6 +375,14 @@ public abstract class TypeNode extends PklNode { return MirrorFactories.moduleTypeFactory.create(null); } + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof FinalModuleTypeNode finalModuleTypeNode)) { + return false; + } + return moduleClass.equals(finalModuleTypeNode.moduleClass); + } + @Override protected PType doExport() { return PType.MODULE; @@ -330,6 +392,11 @@ public abstract class TypeNode extends PklNode { public VmClass getVmClass() { return moduleClass; } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } /** The `module` type for an open module. */ @@ -344,12 +411,12 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { var moduleClass = ((VmTyped) getModuleNode.executeGeneric(frame)).getVmClass(); if (value instanceof VmTyped typed) { var valueClass = typed.getVmClass(); - if (moduleClass.isSuperclassOf(valueClass)) return; + if (moduleClass.isSuperclassOf(valueClass)) return value; } throw typeMismatch(value, moduleClass); @@ -360,6 +427,14 @@ public abstract class TypeNode extends PklNode { return MirrorFactories.moduleTypeFactory.create(null); } + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof NonFinalModuleTypeNode nonFinalModuleTypeNode)) { + return false; + } + return moduleClass.equals(nonFinalModuleTypeNode.moduleClass); + } + @Override protected PType doExport() { return PType.MODULE; @@ -369,6 +444,11 @@ public abstract class TypeNode extends PklNode { public VmClass getVmClass() { return moduleClass; } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class StringLiteralTypeNode extends ObjectSlotTypeNode { @@ -384,8 +464,16 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (literal.equals(value)) return; + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof StringLiteralTypeNode stringLiteralTypeNode)) { + return false; + } + return literal.equals(stringLiteralTypeNode.literal); + } + + @Override + public Object execute(VirtualFrame frame, Object value) { + if (literal.equals(value)) return value; throw typeMismatch(value, literal); } @@ -406,6 +494,11 @@ public abstract class TypeNode extends PklNode { protected PType doExport() { return new PType.StringLiteral(literal); } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class TypedTypeNode extends ObjectSlotTypeNode { @@ -414,8 +507,8 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof VmTyped) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmTyped) return value; throw typeMismatch(value, BaseModule.getTypedClass()); } @@ -424,6 +517,16 @@ public abstract class TypeNode extends PklNode { public VmClass getVmClass() { return BaseModule.getTypedClass(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof TypedTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class DynamicTypeNode extends ObjectSlotTypeNode { @@ -432,8 +535,8 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof VmDynamic) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmDynamic) return value; throw typeMismatch(value, BaseModule.getDynamicClass()); } @@ -449,6 +552,16 @@ public abstract class TypeNode extends PklNode { return VmDynamic.empty(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof DynamicTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } /** @@ -465,8 +578,8 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof VmValue vmValue && clazz == vmValue.getVmClass()) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmValue vmValue && clazz == vmValue.getVmClass()) return value; throw typeMismatch(value, clazz); } @@ -489,6 +602,19 @@ public abstract class TypeNode extends PklNode { return TypeNode.createDefaultValue(clazz); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof FinalClassTypeNode finalClassTypeNode)) { + return false; + } + return clazz.equals(finalClassTypeNode.clazz); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } /** @@ -517,25 +643,25 @@ public abstract class TypeNode extends PklNode { @ExplodeLoop @SuppressWarnings("unused") @Specialization(guards = "value.getVmClass() == cachedClass") - protected void eval( + protected Object eval( VmValue value, @Cached("value.getVmClass()") VmClass cachedClass, @Cached("clazz.isSuperclassOf(cachedClass)") boolean isSuperclass) { - if (isSuperclass) return; + if (isSuperclass) return value; throw typeMismatch(value, clazz); } @Specialization - protected void eval(VmValue value) { - if (clazz.isSuperclassOf(value.getVmClass())) return; + protected Object eval(VmValue value) { + if (clazz.isSuperclassOf(value.getVmClass())) return value; throw typeMismatch(value, clazz); } @Fallback - protected void eval(Object value) { + protected Object eval(Object value) { throw typeMismatch(value, clazz); } @@ -545,9 +671,22 @@ public abstract class TypeNode extends PklNode { return TypeNode.createDefaultValue(clazz); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof NonFinalClassTypeNode nonFinalClassTypeNode)) { + return false; + } + return clazz.equals(nonFinalClassTypeNode.clazz); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } - public abstract static class NullableTypeNode extends WriteFrameSlotTypeNode { + public static class NullableTypeNode extends WriteFrameSlotTypeNode { @Child private TypeNode elementTypeNode; public NullableTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { @@ -582,17 +721,42 @@ public abstract class TypeNode extends PklNode { return new PType.Nullable(elementTypeNode.doExport()); } - @Specialization - @SuppressWarnings("unused") - protected void eval(VmNull value) {} // do nothing + @Override + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmNull) { + // do nothing + return value; + } + return elementTypeNode.execute(frame, value); + } - @Fallback - protected void eval(VirtualFrame frame, Object value) { - elementTypeNode.execute(frame, value); + @Override + public Object executeEagerly(VirtualFrame frame, Object value) { + if (value instanceof VmNull) { + // do nothing + return value; + } + return elementTypeNode.executeEagerly(frame, value); + } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof NullableTypeNode nullableTypeNode)) { + return false; + } + return elementTypeNode.isEquivalentTo(nullableTypeNode.elementTypeNode); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + if (!consumer.accept(this)) { + return false; + } + return elementTypeNode.acceptTypeNode(consumer); } } - public static final class UnionTypeNode extends WriteFrameSlotTypeNode { + public static class UnionTypeNode extends WriteFrameSlotTypeNode { @Children final TypeNode[] elementTypeNodes; private final boolean skipElementTypeChecks; private final int defaultIndex; @@ -646,21 +810,125 @@ public abstract class TypeNode extends PklNode { @Override @ExplodeLoop - public void execute(VirtualFrame frame, Object value) { - if (skipElementTypeChecks) return; + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof UnionTypeNode unionTypeNode)) { + return false; + } + if (elementTypeNodes.length != unionTypeNode.elementTypeNodes.length) { + return false; + } + var ret = true; + // Note: a further optimization is to say that A|B is equivalent to B|A, + // but this requires knowing how to match A to A first, e.g. by sorting them, which we don't + // know how to do. + for (var i = 0; i < elementTypeNodes.length; i++) { + if (!ret) { + // don't return early so that we can ensure a constant number of loop iterations; helps + // the partial evaluator unroll this loop to be flat. + continue; + } + if (!elementTypeNodes[i].isEquivalentTo(unionTypeNode.elementTypeNodes[i])) { + ret = false; + } + } + LoopNode.reportLoopCount(this, elementTypeNodes.length); + return ret; + } + + @Override + @ExplodeLoop + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + if (!consumer.accept(this)) { + return false; + } + var ret = true; + // don't break early to ensure constant number of iterations + //noinspection ForLoopReplaceableByForEach + for (var i = 0; i < elementTypeNodes.length; i++) { + if (!ret) { + continue; + } + if (!elementTypeNodes[i].acceptTypeNode(consumer)) { + ret = false; + } + } + LoopNode.reportLoopCount(this, elementTypeNodes.length); + return ret; + } + + /** + * Tells if the union type should be eagerly checked or not (shallow-force members of + * Listing/Mapping). + * + *

Union types should be eagerly checked if two of the alternatives are the same generic + * type; e.g. {@code Listing|Listing} + */ + @TruffleBoundary + private boolean shouldEagerCheck() { + var seenParameterizedClasses = EconomicSets.create(); + var ret = new MutableBoolean(false); + this.acceptTypeNode( + (typeNode) -> { + if (!typeNode.isParametric()) { + return true; + } + var typeNodeClass = typeNode.getVmClass(); + if (typeNodeClass == null) { + return true; + } + if (seenParameterizedClasses.contains(typeNodeClass)) { + ret.set(true); + return false; + } else { + EconomicSets.add(seenParameterizedClasses, typeNodeClass); + return true; + } + }); + return ret.get(); + } + + @Fallback + @ExplodeLoop + public Object execute(VirtualFrame frame, Object value) { + if (skipElementTypeChecks) return value; + + // escape analysis should remove this allocation in compiled code + var typeMismatches = new VmTypeMismatchException[elementTypeNodes.length]; + + // Do eager checks (shallow-force) if there are two listings or two mappings represented. + // (we can't know that `new Listing { 0; "hi" }[0]` fails for `Listing|Listing` + // without checking both index 0 and index 1). + var shouldEagerCheck = shouldEagerCheck(); + for (var i = 0; i < elementTypeNodes.length; i++) { + var elementTypeNode = elementTypeNodes[i]; + try { + if (shouldEagerCheck) { + return elementTypeNode.executeEagerly(frame, value); + } else { + return elementTypeNode.execute(frame, value); + } + } catch (VmTypeMismatchException e) { + typeMismatches[i] = e; + } + } + throw new VmTypeMismatchException.Union(sourceSection, value, this, typeMismatches); + } + + @Override + public Object executeEagerly(VirtualFrame frame, Object value) { + if (skipElementTypeChecks) return value; // escape analysis should remove this allocation in compiled code var typeMismatches = new VmTypeMismatchException[elementTypeNodes.length]; for (var i = 0; i < elementTypeNodes.length; i++) { + // eager checks try { - elementTypeNodes[i].execute(frame, value); - return; + return elementTypeNodes[i].executeEagerly(frame, value); } catch (VmTypeMismatchException e) { typeMismatches[i] = e; } } - throw new VmTypeMismatchException.Union(sourceSection, value, this, typeMismatches); } } @@ -696,8 +964,8 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (contains(value)) return; + public Object execute(VirtualFrame frame, Object value) { + if (contains(value)) return value; throw typeMismatch(value, stringLiterals); } @@ -714,6 +982,20 @@ public abstract class TypeNode extends PklNode { stringLiterals.stream().map(StringLiteral::new).collect(Collectors.toList())); } + @Override + @TruffleBoundary + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof UnionOfStringLiteralsTypeNode unionOfStringLiteralsTypeNode)) { + return false; + } + return stringLiterals.equals(unionOfStringLiteralsTypeNode.stringLiterals); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + @Override @TruffleBoundary public @Nullable Object createDefaultValue( @@ -723,17 +1005,40 @@ public abstract class TypeNode extends PklNode { } } - public abstract static class CollectionTypeNode extends ObjectSlotTypeNode { + public static final class CollectionTypeNode extends ObjectSlotTypeNode { @Child private TypeNode elementTypeNode; - protected CollectionTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { + public CollectionTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { super(sourceSection); this.elementTypeNode = elementTypeNode; } @Override - public @Nullable Object createDefaultValue( + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmList vmList) { + return evalList(frame, vmList); + } + if (value instanceof VmSet vmSet) { + return evalSet(frame, vmSet); + } + throw typeMismatch(value, BaseModule.getCollectionClass()); + } + + @Override + public Object executeEagerly(VirtualFrame frame, Object value) { + if (value instanceof VmList vmList) { + return evalListEagerly(frame, vmList); + } + if (value instanceof VmSet vmSet) { + // sets are always checked eagerly + return evalSet(frame, vmSet); + } + throw typeMismatch(value, BaseModule.getCollectionClass()); + } + + @Override + public Object createDefaultValue( VmLanguage language, SourceSection headerSection, String qualifiedName) { return VmList.EMPTY; @@ -745,58 +1050,91 @@ public abstract class TypeNode extends PklNode { } @Override - protected final PType doExport() { + protected PType doExport() { return new PType.Class(BaseModule.getCollectionClass().export(), elementTypeNode.doExport()); } @Override - public final VmList getTypeArgumentMirrors() { + public VmList getTypeArgumentMirrors() { return VmList.of(elementTypeNode.getMirror()); } - @Specialization - protected void eval(VirtualFrame frame, VmList value) { + @Override + public boolean isEquivalentTo(TypeNode other) { + return false; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + + @SuppressWarnings("DuplicatedCode") + @ExplodeLoop + private Object evalList(VirtualFrame frame, VmList value) { + var ret = value; + var idx = 0; + for (var elem : value) { - elementTypeNode.execute(frame, elem); + var result = elementTypeNode.execute(frame, elem); + if (result != elem) { + ret = ret.replace(idx, result); + } + idx++; + } + + LoopNode.reportLoopCount(this, idx); + return ret; + } + + private Object evalListEagerly(VirtualFrame frame, VmList value) { + for (var elem : value) { + elementTypeNode.executeEagerly(frame, elem); } LoopNode.reportLoopCount(this, value.getLength()); + return value; } - @Specialization - protected void eval(VirtualFrame frame, VmSet value) { + private Object evalSet(VirtualFrame frame, VmSet value) { for (var elem : value) { - elementTypeNode.execute(frame, elem); + elementTypeNode.executeEagerly(frame, elem); } LoopNode.reportLoopCount(this, value.getLength()); + return value; } - @Fallback - protected void fallback(Object value) { - throw typeMismatch(value, BaseModule.getCollectionClass()); + @Override + protected boolean isParametric() { + return true; } } - public abstract static class ListTypeNode extends ObjectSlotTypeNode { + public static final class ListTypeNode extends ObjectSlotTypeNode { @Child private TypeNode elementTypeNode; private final boolean skipElementTypeChecks; - protected ListTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { + public ListTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { super(sourceSection); this.elementTypeNode = elementTypeNode; skipElementTypeChecks = elementTypeNode.isNoopTypeCheck(); } @Override - public final Object createDefaultValue( + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + + @Override + public Object createDefaultValue( VmLanguage language, SourceSection headerSection, String qualifiedName) { return VmList.EMPTY; } @Override - public final VmClass getVmClass() { + public VmClass getVmClass() { return BaseModule.getListClass(); } @@ -805,29 +1143,64 @@ public abstract class TypeNode extends PklNode { } @Override - public final VmList getTypeArgumentMirrors() { + public VmList getTypeArgumentMirrors() { return VmList.of(elementTypeNode.getMirror()); } @Override - protected final PType doExport() { + protected PType doExport() { return new PType.Class(BaseModule.getListClass().export(), elementTypeNode.doExport()); } - @Specialization - protected void eval(VirtualFrame frame, VmList value) { - if (skipElementTypeChecks) return; + @Override + public Object executeEagerly(VirtualFrame frame, Object value) { + if (!(value instanceof VmList vmList)) { + throw typeMismatch(value, BaseModule.getListClass()); + } + if (skipElementTypeChecks) return vmList; - for (var elem : value) { - elementTypeNode.execute(frame, elem); + for (var elem : vmList) { + elementTypeNode.executeEagerly(frame, elem); } - LoopNode.reportLoopCount(this, value.getLength()); + LoopNode.reportLoopCount(this, vmList.getLength()); + return value; } - @Fallback - protected void fallback(Object value) { - throw typeMismatch(value, BaseModule.getListClass()); + @SuppressWarnings("DuplicatedCode") + @Override + @ExplodeLoop + public Object execute(VirtualFrame frame, Object value) { + if (!(value instanceof VmList vmList)) { + throw typeMismatch(value, BaseModule.getListClass()); + } + if (skipElementTypeChecks) return vmList; + var ret = vmList; + var idx = 0; + + for (var elem : vmList) { + var result = elementTypeNode.execute(frame, elem); + if (result != elem) { + ret = ret.replace(idx, result); + } + idx++; + } + + LoopNode.reportLoopCount(this, vmList.getLength()); + return ret; + } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof ListTypeNode listTypeNode)) { + return false; + } + return elementTypeNode.isEquivalentTo(listTypeNode.elementTypeNode); + } + + @Override + protected boolean isParametric() { + return true; } } @@ -862,35 +1235,54 @@ public abstract class TypeNode extends PklNode { return VmList.of(elementTypeNode.getMirror()); } + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof SetTypeNode setTypeNode)) { + return false; + } + return elementTypeNode.isEquivalentTo(setTypeNode.elementTypeNode); + } + @Override protected final PType doExport() { return new PType.Class(BaseModule.getSetClass().export(), elementTypeNode.doExport()); } - @Specialization - protected void eval(VirtualFrame frame, VmSet value) { - if (skipElementTypeChecks) return; + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + @Specialization + protected Object eval(VirtualFrame frame, VmSet value) { + if (skipElementTypeChecks) return value; for (var elem : value) { - elementTypeNode.execute(frame, elem); + // no point doing a lazy check because set members have their hash code computed, which + // necessarily deep-forces them. + elementTypeNode.executeEagerly(frame, elem); } LoopNode.reportLoopCount(this, value.getLength()); + return value; } @Fallback - protected void fallback(Object value) { + protected Object fallback(Object value) { throw typeMismatch(value, BaseModule.getSetClass()); } + + @Override + protected boolean isParametric() { + return true; + } } - public abstract static class MapTypeNode extends ObjectSlotTypeNode { + public static final class MapTypeNode extends ObjectSlotTypeNode { @Child private TypeNode keyTypeNode; @Child private TypeNode valueTypeNode; private final boolean skipEntryTypeChecks; - protected MapTypeNode( - SourceSection sourceSection, TypeNode keyTypeNode, TypeNode valueTypeNode) { + public MapTypeNode(SourceSection sourceSection, TypeNode keyTypeNode, TypeNode valueTypeNode) { super(sourceSection); this.keyTypeNode = keyTypeNode; @@ -899,14 +1291,30 @@ public abstract class TypeNode extends PklNode { } @Override - public final Object createDefaultValue( + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmMap vmMap) { + return eval(frame, vmMap); + } + throw typeMismatch(value, BaseModule.getMapClass()); + } + + @Override + public Object executeEagerly(VirtualFrame frame, Object value) { + if (value instanceof VmMap vmMap) { + return evalEager(frame, vmMap); + } + throw typeMismatch(value, BaseModule.getMapClass()); + } + + @Override + public Object createDefaultValue( VmLanguage language, SourceSection headerSection, String qualifiedName) { return VmMap.EMPTY; } @Override - public final VmClass getVmClass() { + public VmClass getVmClass() { return BaseModule.getMapClass(); } @@ -915,94 +1323,180 @@ public abstract class TypeNode extends PklNode { } @Override - public final VmList getTypeArgumentMirrors() { + public VmList getTypeArgumentMirrors() { return VmList.of(keyTypeNode.getMirror(), valueTypeNode.getMirror()); } @Override - protected final PType doExport() { + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof MapTypeNode mapTypeNode)) { + return false; + } + return keyTypeNode.isEquivalentTo(mapTypeNode.keyTypeNode) + && valueTypeNode.isEquivalentTo(mapTypeNode.valueTypeNode); + } + + @Override + protected PType doExport() { return new PType.Class( BaseModule.getMapClass().export(), keyTypeNode.doExport(), valueTypeNode.doExport()); } - @Specialization - protected void eval(VirtualFrame frame, VmMap value) { - if (skipEntryTypeChecks) return; + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + + private Object eval(VirtualFrame frame, VmMap value) { + if (skipEntryTypeChecks) return value; + var ret = value; for (var entry : value) { - keyTypeNode.execute(frame, VmUtils.getKey(entry)); + var key = VmUtils.getKey(entry); + keyTypeNode.executeEagerly(frame, key); + var result = valueTypeNode.execute(frame, VmUtils.getValue(entry)); + if (result != VmUtils.getValue(entry)) { + ret = ret.put(key, result); + } + } + + LoopNode.reportLoopCount(this, value.getLength()); + return ret; + } + + private Object evalEager(VirtualFrame frame, VmMap value) { + if (skipEntryTypeChecks) return value; + for (var entry : value) { + keyTypeNode.executeEagerly(frame, VmUtils.getKey(entry)); valueTypeNode.execute(frame, VmUtils.getValue(entry)); } LoopNode.reportLoopCount(this, value.getLength()); + return value; } - @Fallback - protected void fallback(Object value) { - throw typeMismatch(value, BaseModule.getMapClass()); + @Override + protected boolean isParametric() { + return true; } } - public abstract static class ListingTypeNode extends ListingOrMappingTypeNode { + public static final class ListingTypeNode extends ListingOrMappingTypeNode { public ListingTypeNode(SourceSection sourceSection, TypeNode valueTypeNode) { super(sourceSection, null, valueTypeNode); } - @Specialization - protected void eval(VirtualFrame frame, VmListing value) { - doEval(frame, value); + @Override + public Object execute(VirtualFrame frame, Object value) { + if (!(value instanceof VmListing vmListing)) { + throw typeMismatch(value, BaseModule.getListingClass()); + } + return doTypeCast(frame, vmListing); } @Override - public final VmClass getVmClass() { + public Object executeEagerly(VirtualFrame frame, Object value) { + if (!(value instanceof VmListing vmListing)) { + throw typeMismatch(value, BaseModule.getListingClass()); + } + doEagerCheck(frame, vmListing); + return value; + } + + @Override + public VmClass getVmClass() { return BaseModule.getListingClass(); } @Override - public final VmList getTypeArgumentMirrors() { + public VmList getTypeArgumentMirrors() { return VmList.of(valueTypeNode.getMirror()); } @Override - protected final PType doExport() { + protected PType doExport() { return new PType.Class(BaseModule.getListingClass().export(), valueTypeNode.doExport()); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof ListingTypeNode listingTypeNode)) { + return false; + } + return valueTypeNode.isEquivalentTo(listingTypeNode.valueTypeNode); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } - public abstract static class MappingTypeNode extends ListingOrMappingTypeNode { + public static final class MappingTypeNode extends ListingOrMappingTypeNode { public MappingTypeNode( SourceSection sourceSection, TypeNode keyTypeNode, TypeNode valueTypeNode) { super(sourceSection, keyTypeNode, valueTypeNode); } - @Specialization - protected void eval(VirtualFrame frame, VmMapping value) { - doEval(frame, value); + @Override + public Object execute(VirtualFrame frame, Object value) { + if (!(value instanceof VmMapping vmMapping)) { + throw typeMismatch(value, BaseModule.getMappingClass()); + } + // execute type checks on mapping keys + doEagerCheck(frame, vmMapping, false, true); + return doTypeCast(frame, vmMapping); } @Override - public final VmClass getVmClass() { + public Object executeEagerly(VirtualFrame frame, Object value) { + if (!(value instanceof VmMapping vmMapping)) { + throw typeMismatch(value, BaseModule.getMappingClass()); + } + doEagerCheck(frame, vmMapping, false, false); + return value; + } + + @Override + public VmClass getVmClass() { return BaseModule.getMappingClass(); } @Override - public final VmList getTypeArgumentMirrors() { + public VmList getTypeArgumentMirrors() { assert keyTypeNode != null; return VmList.of(keyTypeNode.getMirror(), valueTypeNode.getMirror()); } @Override - protected final PType doExport() { + protected PType doExport() { assert keyTypeNode != null; return new PType.Class( BaseModule.getMappingClass().export(), keyTypeNode.doExport(), valueTypeNode.doExport()); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof MappingTypeNode mappingTypeNode)) { + return false; + } + assert keyTypeNode != null; + assert mappingTypeNode.keyTypeNode != null; + return keyTypeNode.isEquivalentTo(mappingTypeNode.keyTypeNode) + && valueTypeNode.isEquivalentTo(mappingTypeNode.valueTypeNode); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public abstract static class ListingOrMappingTypeNode extends ObjectSlotTypeNode { @Child protected @Nullable TypeNode keyTypeNode; @Child protected TypeNode valueTypeNode; + @Child @Nullable protected ListingOrMappingTypeCastNode listingOrMappingTypeCastNode; private final boolean skipKeyTypeChecks; private final boolean skipValueTypeChecks; @@ -1030,6 +1524,19 @@ public abstract class TypeNode extends PklNode { return valueTypeNode; } + protected ListingOrMappingTypeCastNode getListingOrMappingTypeCastNode() { + if (listingOrMappingTypeCastNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + listingOrMappingTypeCastNode = + new ListingOrMappingTypeCastNode( + VmLanguage.get(this), + getRootNode().getFrameDescriptor(), + valueTypeNode, + getRootNode().getName()); + } + return listingOrMappingTypeCastNode; + } + // either (if defaultMemberValue != null): // x: Listing // = new Listing { // default = name -> new Foo {} @@ -1108,7 +1615,24 @@ public abstract class TypeNode extends PklNode { EconomicMaps.of(Identifier.DEFAULT, defaultMember)); } - protected void doEval(VirtualFrame frame, VmObject object) { + protected > T doTypeCast(VirtualFrame frame, T original) { + // optimization: don't create new object if the original already has the same typecheck, or if + // this typecheck is a no-op. + if (isNoopTypeCheck() || original.hasSameChecksAs(valueTypeNode)) { + return original; + } + return original.withCheckedMembers(getListingOrMappingTypeCastNode(), frame.materialize()); + } + + protected void doEagerCheck(VirtualFrame frame, VmObject object) { + doEagerCheck(frame, object, skipKeyTypeChecks, skipValueTypeChecks); + } + + protected void doEagerCheck( + VirtualFrame frame, + VmObject object, + boolean skipKeyTypeChecks, + boolean skipValueTypeChecks) { if (skipKeyTypeChecks && skipValueTypeChecks) return; var loopCount = 0; @@ -1125,7 +1649,15 @@ public abstract class TypeNode extends PklNode { if (!skipKeyTypeChecks) { assert keyTypeNode != null; - keyTypeNode.execute(frame, memberKey); + try { + keyTypeNode.executeEagerly(frame, memberKey); + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + e.putInsertedStackFrame( + getRootNode().getCallTarget(), + VmUtils.createStackFrame(member.getHeaderSection(), member.getQualifiedName())); + throw e; + } } if (!skipValueTypeChecks) { @@ -1136,9 +1668,9 @@ public abstract class TypeNode extends PklNode { var callTarget = member.getCallTarget(); memberValue = callTarget.call(object, owner, memberKey); } - object.setCachedValue(memberKey, memberValue); + object.setCachedValue(memberKey, memberValue, member); } - valueTypeNode.execute(frame, memberValue); + valueTypeNode.executeEagerly(frame, memberValue); } } } @@ -1146,11 +1678,9 @@ public abstract class TypeNode extends PklNode { LoopNode.reportLoopCount(this, loopCount); } - @Fallback - protected void fallback(Object value) { - var clazz = getVmClass(); - assert clazz != null; // either Listing or Mapping - throw typeMismatch(value, clazz); + @Override + protected boolean isParametric() { + return true; } } @@ -1184,6 +1714,34 @@ public abstract class TypeNode extends PklNode { return returnTypeNode.getMirror(); } + @Override + @ExplodeLoop + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof FunctionTypeNode functionTypeNode)) { + return false; + } + if (!returnTypeNode.isEquivalentTo(functionTypeNode.returnTypeNode)) { + return false; + } + if (parameterTypeNodes.length != functionTypeNode.parameterTypeNodes.length) { + return false; + } + var ret = true; + // optimization: don't return early so that we can ensure a constant number of loop iterations + for (var i = 0; i < parameterTypeNodes.length; i++) { + var typeNode = parameterTypeNodes[i]; + var otherTypeNode = functionTypeNode.parameterTypeNodes[i]; + if (!ret) { + continue; + } + if (!typeNode.isEquivalentTo(otherTypeNode)) { + ret = false; + } + } + LoopNode.reportLoopCount(this, parameterTypeNodes.length); + return ret; + } + @Override protected final PType doExport() { var parameterTypes = @@ -1191,14 +1749,20 @@ public abstract class TypeNode extends PklNode { return new PType.Function(parameterTypes, TypeNode.export(returnTypeNode)); } + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + @SuppressWarnings("unused") @Specialization(guards = "value.getVmClass() == getFunctionNClass()") - protected void eval(VmFunction value) { + protected Object eval(VmFunction value) { /* do nothing */ + return value; } @Fallback - protected void fallback(Object value) { + protected Object fallback(Object value) { throw typeMismatch(value, getFunctionNClass()); } @@ -1206,6 +1770,11 @@ public abstract class TypeNode extends PklNode { protected VmClass getFunctionNClass() { return BaseModule.getFunctionNClass(parameterTypeNodes.length); } + + @Override + protected boolean isParametric() { + return true; + } } // A type such as `Function` (but not `FunctionN<...>`). @@ -1233,14 +1802,33 @@ public abstract class TypeNode extends PklNode { } @Specialization - protected void eval(@SuppressWarnings("unused") VmFunction value) { + protected Object eval(VmFunction value) { /* do nothing */ + return value; } @Fallback protected void fallback(Object value) { throw typeMismatch(value, BaseModule.getFunctionClass()); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof FunctionClassTypeNode functionClassTypeNode)) { + return false; + } + return typeArgumentNode.isEquivalentTo(functionClassTypeNode.typeArgumentNode); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + + @Override + protected boolean isParametric() { + return true; + } } // A type such as `Function2`. @@ -1261,6 +1849,31 @@ public abstract class TypeNode extends PklNode { return getMirrors(typeArgumentNodes); } + @Override + @ExplodeLoop + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof FunctionNClassTypeNode functionNClassTypeNode)) { + return false; + } + if (typeArgumentNodes.length != functionNClassTypeNode.typeArgumentNodes.length) { + return false; + } + var ret = true; + for (var i = 0; i < typeArgumentNodes.length; i++) { + if (!ret) { + // don't return early so that we can ensure a constant number of loop iterations. + continue; + } + var typeNode = typeArgumentNodes[i]; + var otherTypeNode = functionNClassTypeNode.typeArgumentNodes[i]; + if (!typeNode.isEquivalentTo(otherTypeNode)) { + ret = false; + } + } + LoopNode.reportLoopCount(this, typeArgumentNodes.length); + return ret; + } + @Override protected final PType doExport() { var typeArguments = @@ -1270,12 +1883,12 @@ public abstract class TypeNode extends PklNode { @SuppressWarnings("unused") @Specialization(guards = "value.getVmClass() == getFunctionNClass()") - protected void eval(VmFunction value) { - /* do nothing */ + protected Object eval(VmFunction value) { + return value; } @Fallback - protected void fallback(Object value) { + protected Object fallback(Object value) { throw typeMismatch(value, getFunctionNClass()); } @@ -1283,13 +1896,23 @@ public abstract class TypeNode extends PklNode { protected VmClass getFunctionNClass() { return BaseModule.getFunctionNClass(typeArgumentNodes.length - 1); } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + + @Override + protected boolean isParametric() { + return true; + } } - public abstract static class PairTypeNode extends ObjectSlotTypeNode { + public static final class PairTypeNode extends ObjectSlotTypeNode { @Child private TypeNode firstTypeNode; @Child private TypeNode secondTypeNode; - protected PairTypeNode( + public PairTypeNode( SourceSection sourceSection, TypeNode firstTypeNode, TypeNode secondTypeNode) { super(sourceSection); @@ -1298,30 +1921,61 @@ public abstract class TypeNode extends PklNode { } @Override - public final VmClass getVmClass() { + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof VmPair vmPair) { + var first = firstTypeNode.execute(frame, vmPair.getFirst()); + var second = secondTypeNode.execute(frame, vmPair.getSecond()); + if (first == vmPair.getFirst() && second == vmPair.getSecond()) { + return vmPair; + } + return new VmPair(first, second); + } + throw typeMismatch(value, BaseModule.getPairClass()); + } + + @Override + public Object executeEagerly(VirtualFrame frame, Object value) { + if (value instanceof VmPair vmPair) { + firstTypeNode.executeEagerly(frame, vmPair.getFirst()); + secondTypeNode.executeEagerly(frame, vmPair.getSecond()); + return value; + } + throw typeMismatch(value, BaseModule.getPairClass()); + } + + @Override + public VmClass getVmClass() { return BaseModule.getPairClass(); } @Override - public final VmList getTypeArgumentMirrors() { + public VmList getTypeArgumentMirrors() { return VmList.of(firstTypeNode.getMirror(), secondTypeNode.getMirror()); } @Override - protected final PType doExport() { + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof PairTypeNode pairTypeNode)) { + return false; + } + return firstTypeNode.isEquivalentTo(pairTypeNode.firstTypeNode) + && secondTypeNode.isEquivalentTo(pairTypeNode.secondTypeNode); + } + + @Override + protected PType doExport() { return new PType.Class( BaseModule.getPairClass().export(), firstTypeNode.doExport(), secondTypeNode.doExport()); } - @Specialization - protected void eval(VirtualFrame frame, VmPair value) { - firstTypeNode.execute(frame, value.getFirst()); - secondTypeNode.execute(frame, value.getSecond()); + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); } - @Fallback - protected void fallback(Object value) { - throw typeMismatch(value, BaseModule.getPairClass()); + @Override + protected boolean isParametric() { + return true; } } @@ -1351,10 +2005,28 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { CompilerDirectives.transferToInterpreter(); throw exceptionBuilder().evalError("internalStdLibClass", "VarArgs").build(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + if (!(other instanceof VarArgsTypeNode varArgsTypeNode)) { + return false; + } + return elementTypeNode.isEquivalentTo(varArgsTypeNode.elementTypeNode); + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } + + @Override + protected boolean isParametric() { + return true; + } } public static final class TypeVariableNode extends WriteFrameSlotTypeNode { @@ -1385,14 +2057,25 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { // do nothing + return value; + } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof TypeVariableNode; } @Override protected PType doExport() { return new PType.TypeVariable(typeParameter); } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class NonNullTypeAliasTypeNode extends WriteFrameSlotTypeNode { @@ -1401,11 +2084,12 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { if (value instanceof VmNull) { throw new VmTypeMismatchException.Constraint( BaseModule.getNonNullTypeAlias().getConstraintSection(), value); } + return value; } @Override @@ -1417,6 +2101,16 @@ public abstract class TypeNode extends PklNode { public VmTyped getMirror() { return MirrorFactories.typeAliasTypeFactory.create(this); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof NonNullTypeAliasTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class UIntTypeAliasTypeNode extends IntSlotTypeNode { @@ -1431,9 +2125,9 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { if (value instanceof Long l) { - if ((l & mask) == l) return; + if ((l & mask) == l) return value; throw new VmTypeMismatchException.Constraint(typeAlias.getConstraintSection(), value); } @@ -1456,6 +2150,16 @@ public abstract class TypeNode extends PklNode { public VmTyped getMirror() { return MirrorFactories.typeAliasTypeFactory.create(this); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof UIntTypeAliasTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class Int8TypeAliasTypeNode extends IntSlotTypeNode { @@ -1464,9 +2168,9 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { if (value instanceof Long l) { - if (l == l.byteValue()) return; + if (l == l.byteValue()) return value; throw new VmTypeMismatchException.Constraint( BaseModule.getInt8TypeAlias().getConstraintSection(), value); @@ -1490,6 +2194,16 @@ public abstract class TypeNode extends PklNode { public VmTyped getMirror() { return MirrorFactories.typeAliasTypeFactory.create(this); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof Int8TypeAliasTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class Int16TypeAliasTypeNode extends IntSlotTypeNode { @@ -1498,9 +2212,9 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { if (value instanceof Long l) { - if (l == l.shortValue()) return; + if (l == l.shortValue()) return value; throw new VmTypeMismatchException.Constraint( BaseModule.getInt16TypeAlias().getConstraintSection(), value); @@ -1524,6 +2238,16 @@ public abstract class TypeNode extends PklNode { public VmTyped getMirror() { return MirrorFactories.typeAliasTypeFactory.create(this); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof Int16TypeAliasTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class Int32TypeAliasTypeNode extends IntSlotTypeNode { @@ -1532,9 +2256,9 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { if (value instanceof Long l) { - if (l == l.intValue()) return; + if (l == l.intValue()) return value; throw new VmTypeMismatchException.Constraint( BaseModule.getInt32TypeAlias().getConstraintSection(), value); @@ -1558,6 +2282,16 @@ public abstract class TypeNode extends PklNode { public VmTyped getMirror() { return MirrorFactories.typeAliasTypeFactory.create(this); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof Int32TypeAliasTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class TypeAliasTypeNode extends TypeNode { @@ -1618,14 +2352,14 @@ public abstract class TypeNode extends PklNode { *

Before executing the typealias body, use the owner and receiver of the original frame * where the typealias was declared, so that we preserve its original scope. */ - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { var prevOwner = VmUtils.getOwner(frame); var prevReceiver = VmUtils.getReceiver(frame); VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame())); VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame())); try { - aliasedTypeNode.execute(frame, value); + return aliasedTypeNode.execute(frame, value); } finally { VmUtils.setOwner(frame, prevOwner); VmUtils.setReceiver(frame, prevReceiver); @@ -1634,14 +2368,14 @@ public abstract class TypeNode extends PklNode { /** See docstring on {@link TypeAliasTypeNode#execute}. */ @Override - public void executeAndSet(VirtualFrame frame, Object value) { + public Object executeAndSet(VirtualFrame frame, Object value) { var prevOwner = VmUtils.getOwner(frame); var prevReceiver = VmUtils.getReceiver(frame); VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame())); VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame())); try { - aliasedTypeNode.executeAndSet(frame, value); + return aliasedTypeNode.executeAndSet(frame, value); } finally { VmUtils.setOwner(frame, prevOwner); VmUtils.setReceiver(frame, prevReceiver); @@ -1688,6 +2422,14 @@ public abstract class TypeNode extends PklNode { return typeAlias; } + @Override + public boolean isEquivalentTo(TypeNode other) { + if ((other instanceof TypeAliasTypeNode typeAliasTypeNode)) { + return aliasedTypeNode.isEquivalentTo(typeAliasTypeNode.aliasedTypeNode); + } + return aliasedTypeNode.isEquivalentTo(other); + } + @Override protected PType doExport() { return new PType.Alias( @@ -1695,6 +2437,19 @@ public abstract class TypeNode extends PklNode { Arrays.stream(typeArgumentNodes).map(TypeNode::export).collect(Collectors.toList()), aliasedTypeNode.doExport()); } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + if (!consumer.accept(this)) { + return false; + } + return aliasedTypeNode.acceptTypeNode(consumer); + } + + @Override + protected boolean isParametric() { + return typeArgumentNodes.length > 0; + } } public static final class ConstrainedTypeNode extends TypeNode { @@ -1722,7 +2477,7 @@ public abstract class TypeNode extends PklNode { } @ExplodeLoop - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { if (customThisSlot == -1) { CompilerDirectives.transferToInterpreterAndInvalidate(); // deferred until execution time s.t. nodes of inlined type aliases get the right frame slot @@ -1730,18 +2485,20 @@ public abstract class TypeNode extends PklNode { frame.getFrameDescriptor().findOrAddAuxiliarySlot(CustomThisScope.FRAME_SLOT_ID); } - childNode.execute(frame, value); + var ret = childNode.execute(frame, value); frame.setAuxiliarySlot(customThisSlot, value); for (var node : constraintNodes) { node.execute(frame); } + return ret; } @Override - public void executeAndSet(VirtualFrame frame, Object value) { - execute(frame, value); - childNode.executeAndSet(frame, value); + public Object executeAndSet(VirtualFrame frame, Object value) { + var ret = execute(frame, value); + childNode.executeAndSet(frame, ret); + return ret; } @Override @@ -1768,6 +2525,20 @@ public abstract class TypeNode extends PklNode { .collect(Collectors.toList())); } + @Override + public boolean isEquivalentTo(TypeNode other) { + // consider constrained types as always different + return false; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + if (!consumer.accept(this)) { + return false; + } + return childNode.acceptTypeNode(consumer); + } + public VmTyped getMirror() { // pkl:reflect doesn't currently expose constraints return childNode.getMirror(); @@ -1785,14 +2556,25 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { + public Object execute(VirtualFrame frame, Object value) { // do nothing + return value; } @Override public VmClass getVmClass() { return BaseModule.getAnyClass(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof AnyTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class StringTypeNode extends ObjectSlotTypeNode { @@ -1801,8 +2583,8 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof String) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof String) return value; throw typeMismatch(value, BaseModule.getStringClass()); } @@ -1811,6 +2593,16 @@ public abstract class TypeNode extends PklNode { public VmClass getVmClass() { return BaseModule.getStringClass(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof StringTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class NumberTypeNode extends FrameSlotTypeNode { @@ -1824,14 +2616,14 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof Long || value instanceof Double) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof Long || value instanceof Double) return value; throw typeMismatch(value, BaseModule.getNumberClass()); } @Override - public void executeAndSet(VirtualFrame frame, Object value) { + public Object executeAndSet(VirtualFrame frame, Object value) { var kind = frame.getFrameDescriptor().getSlotKind(slot); if (value instanceof Long l) { if (kind == FrameSlotKind.Double || kind == FrameSlotKind.Object) { @@ -1841,6 +2633,7 @@ public abstract class TypeNode extends PklNode { frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Long); frame.setLong(slot, l); } + return value; } else if (value instanceof Double d) { if (kind == FrameSlotKind.Long || kind == FrameSlotKind.Object) { frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Object); @@ -1849,6 +2642,7 @@ public abstract class TypeNode extends PklNode { frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Double); frame.setDouble(slot, d); } + return value; } else { throw typeMismatch(value, BaseModule.getNumberClass()); } @@ -1858,6 +2652,16 @@ public abstract class TypeNode extends PklNode { public VmClass getVmClass() { return BaseModule.getNumberClass(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof NumberTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class IntTypeNode extends IntSlotTypeNode { @@ -1866,8 +2670,8 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof Long) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof Long) return value; throw typeMismatch(value, BaseModule.getIntClass()); } @@ -1876,6 +2680,16 @@ public abstract class TypeNode extends PklNode { public VmClass getVmClass() { return BaseModule.getIntClass(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof IntTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class FloatTypeNode extends FrameSlotTypeNode { @@ -1889,22 +2703,33 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof Double) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof Double) return value; throw typeMismatch(value, BaseModule.getFloatClass()); } @Override - public void executeAndSet(VirtualFrame frame, Object value) { + public Object executeAndSet(VirtualFrame frame, Object value) { execute(frame, value); frame.setDouble(slot, (double) value); + return value; } @Override public VmClass getVmClass() { return BaseModule.getFloatClass(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof FloatTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } public static final class BooleanTypeNode extends FrameSlotTypeNode { @@ -1918,22 +2743,33 @@ public abstract class TypeNode extends PklNode { } @Override - public void execute(VirtualFrame frame, Object value) { - if (value instanceof Boolean) return; + public Object execute(VirtualFrame frame, Object value) { + if (value instanceof Boolean) return value; throw typeMismatch(value, BaseModule.getBooleanClass()); } @Override - public void executeAndSet(VirtualFrame frame, Object value) { + public Object executeAndSet(VirtualFrame frame, Object value) { execute(frame, value); frame.setBoolean(slot, (boolean) value); + return value; } @Override public VmClass getVmClass() { return BaseModule.getBooleanClass(); } + + @Override + public boolean isEquivalentTo(TypeNode other) { + return other instanceof BooleanTypeNode; + } + + @Override + protected boolean acceptTypeNode(TypeNodeConsumer consumer) { + return consumer.accept(this); + } } private static @Nullable Object createDefaultValue(VmClass clazz) { @@ -1962,4 +2798,10 @@ public abstract class TypeNode extends PklNode { } return builder.build(); } + + @FunctionalInterface + protected interface TypeNodeConsumer { + /** Returns true if the visitor should continue visiting type nodes. */ + boolean accept(TypeNode typeNode); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java index 6f528619..d321da3f 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java @@ -17,10 +17,12 @@ package org.pkl.core.ast.type; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.util.Nullable; +@NodeInfo(shortName = "is") public final class TypeTestNode extends ExpressionNode { @Child private ExpressionNode valueNode; @Child private UnresolvedTypeNode unresolvedTypeNode; @@ -50,9 +52,11 @@ public final class TypeTestNode extends ExpressionNode { unresolvedTypeNode = null; } + // TODO: throw if typeNode is FunctionTypeNode (it's impossible to check) + // https://github.com/apple/pkl/issues/639 Object value = valueNode.executeGeneric(frame); try { - typeNode.execute(frame, value); + typeNode.executeEagerly(frame, value); return true; } catch (VmTypeMismatchException e) { return false; 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 79fb5cfa..799d659f 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 @@ -219,11 +219,11 @@ public abstract class UnresolvedTypeNode extends PklNode { checkNumberOfTypeArguments(clazz); if (clazz.isCollectionClass()) { - return CollectionTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + return new CollectionTypeNode(sourceSection, typeArgumentNodes[0].execute(frame)); } if (clazz.isListClass()) { - return ListTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + return new ListTypeNode(sourceSection, typeArgumentNodes[0].execute(frame)); } if (clazz.isSetClass()) { @@ -231,25 +231,25 @@ public abstract class UnresolvedTypeNode extends PklNode { } if (clazz.isMapClass()) { - return MapTypeNodeGen.create( + return new MapTypeNode( sourceSection, typeArgumentNodes[0].execute(frame), typeArgumentNodes[1].execute(frame)); } if (clazz.isListingClass()) { - return ListingTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + return new ListingTypeNode(sourceSection, typeArgumentNodes[0].execute(frame)); } if (clazz.isMappingClass()) { - return MappingTypeNodeGen.create( + return new MappingTypeNode( sourceSection, typeArgumentNodes[0].execute(frame), typeArgumentNodes[1].execute(frame)); } if (clazz.isPairClass()) { - return PairTypeNodeGen.create( + return new PairTypeNode( sourceSection, typeArgumentNodes[0].execute(frame), typeArgumentNodes[1].execute(frame)); @@ -324,7 +324,7 @@ public abstract class UnresolvedTypeNode extends PklNode { public TypeNode execute(VirtualFrame frame) { CompilerDirectives.transferToInterpreter(); - return NullableTypeNodeGen.create(sourceSection, elementTypeNode.execute(frame)); + return new NullableTypeNode(sourceSection, elementTypeNode.execute(frame)); } } 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 f34c39ee..c4eac112 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 @@ -15,16 +15,19 @@ */ package org.pkl.core.ast.type; +import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.nodes.ControlFlowException; import com.oracle.truffle.api.source.SourceSection; import java.util.*; import java.util.stream.Collectors; +import org.pkl.core.StackFrame; import org.pkl.core.ValueFormatter; import org.pkl.core.ast.type.TypeNode.UnionTypeNode; import org.pkl.core.runtime.*; import org.pkl.core.runtime.VmException.ProgramValue; import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.Nullable; /** * Indicates that a type check failed. [TypeNode]s use this exception instead of [VmException] to @@ -35,18 +38,35 @@ import org.pkl.core.util.ErrorMessages; public abstract class VmTypeMismatchException extends ControlFlowException { protected final SourceSection sourceSection; protected final Object actualValue; + protected @Nullable Map insertedStackFrames = null; protected VmTypeMismatchException(SourceSection sourceSection, Object actualValue) { this.sourceSection = sourceSection; this.actualValue = actualValue; } + @TruffleBoundary + public void putInsertedStackFrame(CallTarget callTarget, StackFrame stackFrame) { + if (this.insertedStackFrames == null) { + this.insertedStackFrames = new HashMap<>(); + } + this.insertedStackFrames.put(callTarget, stackFrame); + } + @TruffleBoundary public abstract void describe(StringBuilder builder, String indent); @TruffleBoundary public abstract VmException toVmException(); + protected VmExceptionBuilder exceptionBuilder() { + var builder = new VmExceptionBuilder(); + if (insertedStackFrames != null) { + builder.withInsertedStackFrames(insertedStackFrames); + } + return builder; + } + public static final class Simple extends VmTypeMismatchException { private final Object expectedType; @@ -128,11 +148,12 @@ public abstract class VmTypeMismatchException extends ControlFlowException { return exceptionBuilder().build(); } - private VmExceptionBuilder exceptionBuilder() { + @Override + protected VmExceptionBuilder exceptionBuilder() { var builder = new StringBuilder(); describe(builder, ""); - return new VmExceptionBuilder() + return super.exceptionBuilder() .adhocEvalError(builder.toString()) .withSourceSection(sourceSection); } @@ -162,11 +183,12 @@ public abstract class VmTypeMismatchException extends ControlFlowException { return exceptionBuilder().build(); } - private VmExceptionBuilder exceptionBuilder() { + @Override + protected VmExceptionBuilder exceptionBuilder() { var builder = new StringBuilder(); describe(builder, ""); - return new VmExceptionBuilder() + return super.exceptionBuilder() .adhocEvalError(builder.toString()) .withSourceSection(sourceSection); } @@ -199,14 +221,15 @@ public abstract class VmTypeMismatchException extends ControlFlowException { return exceptionBuilder().build(); } - private VmExceptionBuilder exceptionBuilder() { + @Override + protected VmExceptionBuilder exceptionBuilder() { var summary = new StringBuilder(); describeSummary(summary, ""); var details = new StringBuilder(); describeDetails(details, ""); - return new VmExceptionBuilder() + return super.exceptionBuilder() .adhocEvalError(summary.toString()) .withSourceSection(sourceSection) .withHint(details.toString()); @@ -304,11 +327,12 @@ public abstract class VmTypeMismatchException extends ControlFlowException { return exceptionBuilder().build(); } - private VmExceptionBuilder exceptionBuilder() { + @Override + protected VmExceptionBuilder exceptionBuilder() { var builder = new StringBuilder(); describe(builder, ""); - return new VmExceptionBuilder() + return super.exceptionBuilder() .adhocEvalError(builder.toString()) .withSourceSection(sourceSection); } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java index 47da5a83..82957bcc 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java @@ -79,19 +79,25 @@ public final class TestRunner { var factsMapping = (VmMapping) facts; factsMapping.forceAndIterateMemberValues( (groupKey, groupMember, groupValue) -> { + var listing = (VmListing) groupValue; var result = results.newResult(String.valueOf(groupKey)); - var groupListing = (VmListing) groupValue; - groupListing.forceAndIterateMemberValues( - ((factIndex, factMember, factValue) -> { - assert factValue instanceof Boolean; - if (factValue == Boolean.FALSE) { - result.addFailure( - Failure.buildFactFailure( - factMember.getSourceSection(), getDisplayUri(factMember))); + return listing.iterateMembers( + (idx, member) -> { + if (member.isLocalOrExternalOrHidden()) { + return true; + } + try { + var factValue = VmUtils.readMember(listing, idx); + if (factValue == Boolean.FALSE) { + result.addFailure( + Failure.buildFactFailure(member.getSourceSection(), getDisplayUri(member))); + } + } catch (VmException err) { + result.addError( + new Error(err.getMessage(), err.toPklException(stackFrameTransformer))); } return true; - })); - return true; + }); }); } @@ -142,12 +148,14 @@ public final class TestRunner { var expectedExampleOutputs = loadExampleOutputs(expectedOutputFile); var actualExampleOutputs = new MutableReference(null); var allGroupsSucceeded = new MutableBoolean(true); + var errored = new MutableBoolean(false); examples.forceAndIterateMemberValues( (groupKey, groupMember, groupValue) -> { var testName = String.valueOf(groupKey); var group = (VmListing) groupValue; var expectedGroup = (VmDynamic) VmUtils.readMemberOrNull(expectedExampleOutputs, groupKey); + var result = results.newResult(testName); if (expectedGroup == null) { results.newResult( @@ -158,8 +166,7 @@ public final class TestRunner { } if (group.getLength() != expectedGroup.getLength()) { - results.newResult( - testName, + result.addFailure( Failure.buildExampleLengthMismatchFailure( getDisplayUri(groupMember), String.valueOf(groupKey), @@ -169,8 +176,21 @@ public final class TestRunner { } var groupSucceeded = new MutableBoolean(true); - group.forceAndIterateMemberValues( - ((exampleIndex, exampleMember, exampleValue) -> { + group.iterateMembers( + ((exampleIndex, exampleMember) -> { + if (exampleMember.isLocalOrExternalOrHidden()) { + return true; + } + Object exampleValue; + try { + exampleValue = VmUtils.readMember(group, exampleIndex); + } catch (VmException err) { + errored.set(true); + result.addError( + new Error(err.getMessage(), err.toPklException(stackFrameTransformer))); + groupSucceeded.set(false); + return true; + } var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex); var exampleValuePcf = renderAsPcf(exampleValue); @@ -202,8 +222,7 @@ public final class TestRunner { .build(); } - results.newResult( - testName, + result.addFailure( Failure.buildExampleFailure( getDisplayUri(exampleMember), getDisplayUri(expectedMember), @@ -215,9 +234,7 @@ public final class TestRunner { return true; })); - if (groupSucceeded.get()) { - results.newResult(testName); - } else { + if (!groupSucceeded.get()) { allGroupsSucceeded.set(false); } @@ -231,26 +248,52 @@ public final class TestRunner { } if (examples.getCachedValue(groupKey) == null) { allGroupsSucceeded.set(false); - results.newResult( - String.valueOf(groupKey), - Failure.buildExamplePropertyMismatchFailure( - getDisplayUri(groupMember), String.valueOf(groupKey), false)); + results + .newResult(String.valueOf(groupKey)) + .addFailure( + Failure.buildExamplePropertyMismatchFailure( + getDisplayUri(groupMember), String.valueOf(groupKey), false)); } return true; }); - if (!allGroupsSucceeded.get() && actualExampleOutputs.isNull()) { + if (!allGroupsSucceeded.get() && actualExampleOutputs.isNull() && !errored.get()) { writeExampleOutputs(actualOutputFile, examples); } } private void doRunAndWriteExamples(VmMapping examples, Path outputFile, TestResults results) { - examples.forceAndIterateMemberValues( - (groupKey, groupMember, groupValue) -> { - results.newResult(String.valueOf(groupKey)).setExampleWritten(true); - return true; - }); - writeExampleOutputs(outputFile, examples); + var allSucceeded = + examples.forceAndIterateMemberValues( + (groupKey, groupMember, groupValue) -> { + var listing = (VmListing) groupValue; + var success = + listing.iterateMembers( + (idx, member) -> { + if (member.isLocalOrExternalOrHidden()) { + return true; + } + try { + VmUtils.readMember(listing, idx); + return true; + } catch (VmException err) { + results + .newResult(String.valueOf(groupKey)) + .addError( + new Error( + err.getMessage(), err.toPklException(stackFrameTransformer))); + return false; + } + }); + if (!success) { + return false; + } + results.newResult(String.valueOf(groupKey)).setExampleWritten(true); + return true; + }); + if (allSucceeded) { + writeExampleOutputs(outputFile, examples); + } } private void writeExampleOutputs(Path outputFile, VmMapping examples) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmBugException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmBugException.java index f3e2f743..1cd04615 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmBugException.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmBugException.java @@ -15,10 +15,12 @@ */ package org.pkl.core.runtime; +import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.List; +import java.util.Map; import org.pkl.core.*; import org.pkl.core.util.Nullable; @@ -32,7 +34,8 @@ public final class VmBugException extends VmException { @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint) { + @Nullable String hint, + Map insertedStackFrames) { super( message, @@ -43,7 +46,8 @@ public final class VmBugException extends VmException { location, sourceSection, memberName, - hint); + hint, + insertedStackFrames); } @Override diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmEvalException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmEvalException.java index cef00778..0a2996c1 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmEvalException.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmEvalException.java @@ -15,9 +15,12 @@ */ package org.pkl.core.runtime; +import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.List; +import java.util.Map; +import org.pkl.core.StackFrame; import org.pkl.core.util.Nullable; public class VmEvalException extends VmException { @@ -30,7 +33,8 @@ public class VmEvalException extends VmException { @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint) { + @Nullable String hint, + Map insertedStackFrames) { super( message, @@ -41,6 +45,7 @@ public class VmEvalException extends VmException { location, sourceSection, memberName, - hint); + hint, + insertedStackFrames); } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java index c971bf22..e291c4d4 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java @@ -20,7 +20,6 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; -import java.util.HashMap; import java.util.List; import java.util.Map; import org.pkl.core.*; @@ -33,8 +32,7 @@ public abstract class VmException extends AbstractTruffleException { private final @Nullable SourceSection sourceSection; private final @Nullable String memberName; protected @Nullable String hint; - - private final Map insertedStackFrames = new HashMap<>(); + private final Map insertedStackFrames; public VmException( String message, @@ -45,7 +43,8 @@ public abstract class VmException extends AbstractTruffleException { @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint) { + @Nullable String hint, + Map insertedStackFrames) { super(message, cause, UNLIMITED_STACK_TRACE, location); this.isExternalMessage = isExternalMessage; this.messageArguments = messageArguments; @@ -53,6 +52,7 @@ public abstract class VmException extends AbstractTruffleException { this.sourceSection = sourceSection; this.memberName = memberName; this.hint = hint; + this.insertedStackFrames = insertedStackFrames; } public final boolean isExternalMessage() { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java index 6fba2080..141bd97b 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java @@ -15,11 +15,13 @@ */ package org.pkl.core.runtime; +import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.pkl.core.StackFrame; import org.pkl.core.runtime.MemberLookupSuggestions.Candidate.Kind; import org.pkl.core.runtime.VmException.ProgramValue; import org.pkl.core.util.Nullable; @@ -50,6 +52,7 @@ import org.pkl.core.util.Nullable; public final class VmExceptionBuilder { private @Nullable Object receiver; + private @Nullable Map insertedStackFrames; public static class MultilineValue { private final Iterable lines; @@ -329,11 +332,19 @@ public final class VmExceptionBuilder { return this; } + public VmExceptionBuilder withInsertedStackFrames( + Map insertedStackFrames) { + this.insertedStackFrames = insertedStackFrames; + return this; + } + public VmException build() { if (message == null) { throw new IllegalStateException("No message set."); } + var effectiveInsertedStackFrames = + insertedStackFrames == null ? new HashMap() : insertedStackFrames; return switch (kind) { case EVAL_ERROR -> new VmEvalException( @@ -345,7 +356,8 @@ public final class VmExceptionBuilder { location, sourceSection, memberName, - hint); + hint, + effectiveInsertedStackFrames); case UNDEFINED_VALUE -> new VmUndefinedValueException( message, @@ -357,7 +369,8 @@ public final class VmExceptionBuilder { sourceSection, memberName, hint, - receiver); + receiver, + effectiveInsertedStackFrames); case BUG -> new VmBugException( message, @@ -368,7 +381,8 @@ public final class VmExceptionBuilder { location, sourceSection, memberName, - hint); + hint, + effectiveInsertedStackFrames); }; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java index 57dfc65a..621a9b51 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java @@ -113,7 +113,7 @@ public final class VmFunction extends VmObjectLike { @Override @TruffleBoundary - public void setCachedValue(Object key, Object value) { + public void setCachedValue(Object key, Object value, ObjectMember objectMember) { throw new VmExceptionBuilder() .bug("Class `VmFunction` does not support method `setCachedValue()`.") .build(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java index a931d90a..804c8c44 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java @@ -21,13 +21,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.Nullable; -// TODO: make sure that "default" isn't forced -// when a listing is rendered ("default" should be allowed to be partial) -public final class VmListing extends VmObject { +public final class VmListing extends VmListingOrMapping { private static final class EmptyHolder { private static final VmListing EMPTY = new VmListing( @@ -48,7 +47,25 @@ public final class VmListing extends VmObject { VmObject parent, UnmodifiableEconomicMap members, int length) { - super(enclosingFrame, Objects.requireNonNull(parent), members); + super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null); + this.length = length; + } + + public VmListing( + MaterializedFrame enclosingFrame, + VmObject parent, + UnmodifiableEconomicMap members, + int length, + @Nullable VmListing delegate, + ListingOrMappingTypeCastNode typeCheckNode, + MaterializedFrame typeNodeFrame) { + super( + enclosingFrame, + Objects.requireNonNull(parent), + members, + delegate, + typeCheckNode, + typeNodeFrame); this.length = length; } @@ -100,6 +117,20 @@ public final class VmListing extends VmObject { return converter.convertListing(this, path); } + @Override + public VmListing withCheckedMembers( + ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) { + + return new VmListing( + getEnclosingFrame(), + Objects.requireNonNull(parent), + members, + length, + this, + typeCheckNode, + typeNodeFrame); + } + @Override @TruffleBoundary public boolean equals(@Nullable Object obj) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmListingOrMapping.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmListingOrMapping.java new file mode 100644 index 00000000..beaa2f1f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmListingOrMapping.java @@ -0,0 +1,169 @@ +/** + * Copyright © 2024 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; +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.EconomicSet; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.PklBugException; +import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.EconomicSets; +import org.pkl.core.util.Nullable; + +public abstract class VmListingOrMapping> extends VmObject { + + /** + * A Listing or Mapping typecast creates a new object that contains a new typecheck node, and + * delegates member lookups to this delegate. + */ + private final @Nullable SELF delegate; + + private final @Nullable ListingOrMappingTypeCastNode typeCastNode; + private final MaterializedFrame typeNodeFrame; + private final EconomicMap cachedMembers = EconomicMaps.create(); + private final EconomicSet checkedMembers = EconomicSets.create(); + + public VmListingOrMapping( + MaterializedFrame enclosingFrame, + @Nullable VmObject parent, + UnmodifiableEconomicMap members, + @Nullable SELF delegate, + @Nullable ListingOrMappingTypeCastNode typeCastNode, + @Nullable MaterializedFrame typeNodeFrame) { + super(enclosingFrame, parent, members); + this.delegate = delegate; + this.typeCastNode = typeCastNode; + this.typeNodeFrame = typeNodeFrame; + } + + ObjectMember findMember(Object key) { + var member = EconomicMaps.get(cachedMembers, key); + if (member != null) { + return member; + } + if (delegate != null) { + return delegate.findMember(key); + } + // member is guaranteed to exist; this is only called if `getCachedValue()` returns non-null + // and `setCachedValue` will record the object member in `cachedMembers`. + throw PklBugException.unreachableCode(); + } + + public @Nullable ListingOrMappingTypeCastNode getTypeCastNode() { + return typeCastNode; + } + + @Override + public void setCachedValue(Object key, Object value, ObjectMember objectMember) { + super.setCachedValue(key, value, objectMember); + EconomicMaps.put(cachedMembers, key, objectMember); + } + + @Override + public @Nullable Object getCachedValue(Object key) { + var myCachedValue = super.getCachedValue(key); + if (myCachedValue != null || delegate == null) { + return myCachedValue; + } + var memberValue = delegate.getCachedValue(key); + // if this object member appears inside `checkedMembers`, we have already checked its type + // and can safely return it. + if (EconomicSets.contains(checkedMembers, key)) { + return memberValue; + } + if (memberValue == null) { + return null; + } + // If a cached value already exists on the delegate, run a typecast on it. + // optimization: don't use `VmUtils.findMember` to avoid iterating over all members + var objectMember = findMember(key); + var ret = typecastObjectMember(objectMember, memberValue, IndirectCallNode.getUncached()); + if (ret != memberValue) { + EconomicMaps.put(cachedValues, key, ret); + } else { + // optimization: don't add to own cached values if typecast results in the same value + EconomicSets.add(checkedMembers, key); + } + return ret; + } + + @Override + public Object getExtraStorage() { + if (delegate != null) { + return delegate.getExtraStorage(); + } + assert extraStorage != null; + return extraStorage; + } + + /** Perform a typecast on this member, */ + public Object typecastObjectMember( + ObjectMember member, Object memberValue, IndirectCallNode callNode) { + if (!(member.isEntry() || member.isElement()) || typeCastNode == null) { + return memberValue; + } + assert typeNodeFrame != null; + var ret = memberValue; + if (delegate != null) { + ret = delegate.typecastObjectMember(member, ret, callNode); + } + var callTarget = typeCastNode.getCallTarget(); + try { + return callNode.call( + callTarget, VmUtils.getReceiver(typeNodeFrame), VmUtils.getOwner(typeNodeFrame), ret); + } catch (VmException vmException) { + CompilerDirectives.transferToInterpreter(); + // treat typecheck as part of the call stack to read the original member if there is a + // source section for it. + var sourceSection = member.getBodySection(); + if (!sourceSection.isAvailable()) { + sourceSection = member.getSourceSection(); + } + if (sourceSection.isAvailable()) { + vmException + .getInsertedStackFrames() + .put(callTarget, VmUtils.createStackFrame(sourceSection, member.getQualifiedName())); + } + throw vmException; + } + } + + public abstract SELF withCheckedMembers( + ListingOrMappingTypeCastNode typeCastNode, MaterializedFrame typeNodeFrame); + + /** Tells if this mapping/listing runs the same typechecks as {@code typeNode}. */ + public boolean hasSameChecksAs(TypeNode typeNode) { + if (typeCastNode == null) { + return false; + } + if (typeCastNode.getTypeNode().isEquivalentTo(typeNode)) { + return true; + } + // we can say the check is the same if the delegate has this check. + // when `Listing` delegates to `Listing`, it has the same checks as a `UInt` + // typenode. + if (delegate != null) { + return delegate.hasSameChecksAs(typeNode); + } + return false; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java index 3d42f696..cdf157f0 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java @@ -21,14 +21,14 @@ import java.util.Map; import java.util.Objects; import javax.annotation.concurrent.GuardedBy; import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.util.CollectionUtils; import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.LateInit; -// TODO: make sure that "default" isn't forced -// when a mapping is rendered ("default" should be allowed to be partial) -public final class VmMapping extends VmObject { +public final class VmMapping extends VmListingOrMapping { + private int cachedEntryCount = -1; @GuardedBy("this") @@ -51,7 +51,23 @@ public final class VmMapping extends VmObject { VmObject parent, UnmodifiableEconomicMap members) { - super(enclosingFrame, Objects.requireNonNull(parent), members); + super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null); + } + + public VmMapping( + MaterializedFrame enclosingFrame, + VmObject parent, + UnmodifiableEconomicMap members, + VmMapping delegate, + ListingOrMappingTypeCastNode typeCheckNode, + MaterializedFrame typeNodeFrame) { + super( + enclosingFrame, + Objects.requireNonNull(parent), + members, + delegate, + typeCheckNode, + typeNodeFrame); } public static boolean isDefaultProperty(Object propertyKey) { @@ -181,4 +197,17 @@ public final class VmMapping extends VmObject { cachedEntryCount = result; return result; } + + @Override + @TruffleBoundary + public VmMapping withCheckedMembers( + ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) { + return new VmMapping( + getEnclosingFrame(), + Objects.requireNonNull(getParent()), + getMembers(), + this, + typeCheckNode, + typeNodeFrame); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java index beb6e794..3c00e137 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java @@ -82,12 +82,12 @@ public abstract class VmObject extends VmObjectLike { } @Override - public final @Nullable Object getCachedValue(Object key) { + public @Nullable Object getCachedValue(Object key) { return EconomicMaps.get(cachedValues, key); } @Override - public final void setCachedValue(Object key, Object value) { + public void setCachedValue(Object key, Object value, ObjectMember objectMember) { EconomicMaps.put(cachedValues, key, value); } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java index ffb66428..f062600c 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java @@ -52,7 +52,7 @@ public abstract class VmObjectLike extends VmValue { return extraStorage != null; } - public final Object getExtraStorage() { + public Object getExtraStorage() { assert extraStorage != null; return extraStorage; } @@ -96,7 +96,7 @@ public abstract class VmObjectLike extends VmValue { * receiver. */ @TruffleBoundary - public abstract void setCachedValue(Object key, Object value); + public abstract void setCachedValue(Object key, Object value, ObjectMember objectMember); /** * Prefer this method over {@link #getCachedValue} if the value is not required. (There is no diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmStackOverflowException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmStackOverflowException.java index 3683c241..85ad8ca1 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmStackOverflowException.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmStackOverflowException.java @@ -15,11 +15,22 @@ */ package org.pkl.core.runtime; +import java.util.HashMap; import java.util.List; public final class VmStackOverflowException extends VmException { public VmStackOverflowException(StackOverflowError e) { - super("stackOverflow", e, true, new Object[0], List.of(), null, null, null, null); + super( + "stackOverflow", + e, + true, + new Object[0], + List.of(), + null, + null, + null, + null, + new HashMap<>()); } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmUndefinedValueException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmUndefinedValueException.java index 9329a959..2c5492d9 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmUndefinedValueException.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmUndefinedValueException.java @@ -15,10 +15,13 @@ */ package org.pkl.core.runtime; +import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.Deque; import java.util.List; +import java.util.Map; +import org.pkl.core.StackFrame; import org.pkl.core.parser.Lexer; import org.pkl.core.util.Nullable; @@ -35,7 +38,8 @@ public final class VmUndefinedValueException extends VmEvalException { @Nullable SourceSection sourceSection, @Nullable String memberName, @Nullable String hint, - @Nullable Object receiver) { + @Nullable Object receiver, + Map insertedStackFrames) { super( message, @@ -46,7 +50,8 @@ public final class VmUndefinedValueException extends VmEvalException { location, sourceSection, memberName, - hint); + hint, + insertedStackFrames); this.receiver = receiver; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java index 6fcbc305..80aeb0ab 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java @@ -263,6 +263,7 @@ public final class VmUtils { final var constantValue = member.getConstantValue(); if (constantValue != null) { + var ret = constantValue; // for a property, do a type check if (member.isProp()) { var property = receiver.getVmClass().getProperty(member.getName()); @@ -270,10 +271,15 @@ public final class VmUtils { var callTarget = property.getTypeNode().getCallTarget(); try { if (checkType) { - callNode.call(callTarget, receiver, property.getOwner(), constantValue); + ret = callNode.call(callTarget, receiver, property.getOwner(), constantValue); } else { - callNode.call( - callTarget, receiver, property.getOwner(), constantValue, SKIP_TYPECHECK_MARKER); + ret = + callNode.call( + callTarget, + receiver, + property.getOwner(), + constantValue, + VmUtils.SKIP_TYPECHECK_MARKER); } } catch (VmException e) { CompilerDirectives.transferToInterpreter(); @@ -293,21 +299,25 @@ public final class VmUtils { throw e; } } + } else if (receiver instanceof VmListingOrMapping vmListingOrMapping) { + ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode); } - - receiver.setCachedValue(memberKey, constantValue); - return constantValue; + receiver.setCachedValue(memberKey, ret, member); + return ret; } var callTarget = member.getCallTarget(); - Object computedValue; + Object ret; if (checkType) { - computedValue = callNode.call(callTarget, receiver, owner, memberKey); + ret = callNode.call(callTarget, receiver, owner, memberKey); } else { - computedValue = callNode.call(callTarget, receiver, owner, memberKey, SKIP_TYPECHECK_MARKER); + ret = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER); } - receiver.setCachedValue(memberKey, computedValue); - return computedValue; + if (receiver instanceof VmListingOrMapping vmListingOrMapping) { + ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode); + } + receiver.setCachedValue(memberKey, ret, member); + return ret; } public static @Nullable ObjectMember findMember(VmObjectLike receiver, Object memberKey) { @@ -849,4 +859,17 @@ public final class VmUtils { public static V getMapValue(Map map, K key) { return map.get(key); } + + /** + * If true, the value computed by this node is not the final value exposed to user code but will + * still be amended. + * + *

Used to disable type check for to-be-amended properties. See {@link + * org.pkl.core.runtime.VmUtils#SKIP_TYPECHECK_MARKER}. IDEA: might be more appropriate to only + * skip constraints check + */ + public static boolean shouldRunTypeCheck(VirtualFrame frame) { + return frame.getArguments().length != 4 + || frame.getArguments()[3] != VmUtils.SKIP_TYPECHECK_MARKER; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java index 7cce6058..8ebcfa29 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java @@ -18,6 +18,7 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.IndirectCallNode; +import java.util.HashSet; import org.pkl.core.ast.lambda.ApplyVmFunction3Node; import org.pkl.core.ast.lambda.ApplyVmFunction3NodeGen; import org.pkl.core.runtime.*; @@ -50,9 +51,14 @@ public final class MappingNodes { @Specialization @TruffleBoundary protected long eval(VmMapping self) { - MutableLong count = new MutableLong(0); - self.iterateMemberValues( - (key, member, value) -> { + var count = new MutableLong(0); + var visited = new HashSet<>(); + self.iterateMembers( + (key, member) -> { + var alreadyVisited = !visited.add(key); + // important to record hidden member as visited before skipping it + // because any overriding member won't carry a `hidden` identifier + if (alreadyVisited || member.isLocalOrExternalOrHidden()) return true; count.getAndIncrement(); return true; }); 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 d5c7439c..7cdde50e 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 @@ -42,8 +42,6 @@ import org.pkl.core.ast.type.TypeNode.StringTypeNode; import org.pkl.core.ast.type.TypeNode.TypeAliasTypeNode; import org.pkl.core.ast.type.TypeNode.UnionOfStringLiteralsTypeNode; import org.pkl.core.ast.type.TypeNode.UnionTypeNode; -import org.pkl.core.ast.type.TypeNodeFactory.ListingTypeNodeGen; -import org.pkl.core.ast.type.TypeNodeFactory.MappingTypeNodeGen; import org.pkl.core.ast.type.VmTypeMismatchException; import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.VmClass; @@ -575,7 +573,7 @@ public final class RendererNodes { type = requiresWrapper() ? null - : ListingTypeNodeGen.create(VmUtils.unavailableSourceSection(), valueType); + : new ListingTypeNode(VmUtils.unavailableSourceSection(), valueType); return type; } else if (type instanceof MappingTypeNode mappingType) { var keyType = resolveType(mappingType.getKeyTypeNode()); @@ -589,8 +587,7 @@ public final class RendererNodes { } var valueType = resolveType(mappingType.getValueTypeNode()); assert valueType != null : "Incomplete or malformed Mapping type"; - mappingType = - MappingTypeNodeGen.create(VmUtils.unavailableSourceSection(), keyType, valueType); + mappingType = new MappingTypeNode(VmUtils.unavailableSourceSection(), keyType, valueType); type = requiresWrapper() ? null : mappingType; return type; diff --git a/pkl-core/src/main/java/org/pkl/core/util/EconomicSets.java b/pkl-core/src/main/java/org/pkl/core/util/EconomicSets.java index a95191c9..5a25ff0a 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/EconomicSets.java +++ b/pkl-core/src/main/java/org/pkl/core/util/EconomicSets.java @@ -30,4 +30,9 @@ public final class EconomicSets { public static boolean add(EconomicSet self, E element) { return self.add(element); } + + @TruffleBoundary + public static boolean contains(EconomicSet self, E element) { + return self.contains(element); + } } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/as2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/as2.pkl index 15063d7a..2c44d663 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/as2.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/as2.pkl @@ -20,9 +20,9 @@ examples { ["listing"] { new Listing { 1; 2; 3 } as Listing - module.catch(() -> new Listing { 1; 2; 3 } as Listing) + module.catch(() -> (new Listing { 1; 2; 3 } as Listing)[0]) } - + ["mapping"] { module.catch(() -> new Listing { 1; 2; 3 } as Mapping) new Mapping { ["Pigeon"] = 42; ["Barn Owl"] = 21 } as Mapping @@ -49,9 +49,9 @@ examples { ["function type"] { (((x) -> x) as (Int) -> Int).apply(42) (((x) -> x) as (String) -> String).apply(42) - module.catch(() -> ((x, y) -> x) as (Int) -> Int) + module.catch(() -> ((x, _) -> x) as (Int) -> Int) } - + ["string literal type"] { "Pigeon" as "Pigeon"|"Barn Owl" module.catch(() -> "Pigeon" as "Piggy"|"Barn Owl") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/as3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/as3.pkl new file mode 100644 index 00000000..b2559fe6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/as3.pkl @@ -0,0 +1,34 @@ +amends "../snippetTest.pkl" + +examples { + ["set of listing"] { + local s1 = Set( + new Listing { 1; 2; 3 } + ) as Set> + local s2 = Set( + new Listing { "one"; "two"; "three" } + ) as Set> + s1.first[0] + module.catchOrNull(() -> s2.first) == null + module.catch(() -> s2.first[0]) + } + ["listing"] { + local l = new Listing { 1; 2; 3 } as Listing + module.catchOrNull(() -> l) == null + module.catch(() -> l[0]) + } + ["mapping"] { + local m1 = new Mapping { + ["hi"] = 1 + ["bye"] = 2 + } as Mapping + module.catchOrNull(() -> m1) == null + module.catch(() -> m1["hi"]) + module.catch(() -> m1["bye"]) + local m2 = new Mapping { + ["hi"] = 1 + ["bye"] = 2 + } as Mapping + module.catch(() -> m2) + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError1.pkl new file mode 100644 index 00000000..17ea42a1 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError1.pkl @@ -0,0 +1,3 @@ +res = new Listing { + 1 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError2.pkl new file mode 100644 index 00000000..f0b5cef5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError2.pkl @@ -0,0 +1,5 @@ +one = 1 + +res = new Listing { + one +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError3.pkl new file mode 100644 index 00000000..e7927583 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError3.pkl @@ -0,0 +1,3 @@ +res: Listing = new Listing { + "" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError4.pkl new file mode 100644 index 00000000..a4ff1f1a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError4.pkl @@ -0,0 +1,3 @@ +res: Listing = new Listing { + "hola" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError5.pkl new file mode 100644 index 00000000..65178449 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError5.pkl @@ -0,0 +1,4 @@ +local l = new Listing { 1; 2; 3 } as Listing + +res = l[0] + diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError1.pkl new file mode 100644 index 00000000..aefe7b40 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError1.pkl @@ -0,0 +1,3 @@ +res = new Mapping { + ["foo"] = 1 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError2.pkl new file mode 100644 index 00000000..85f6a44a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError2.pkl @@ -0,0 +1,3 @@ +res: Mapping = new { + ["foo"] = 1 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError3.pkl new file mode 100644 index 00000000..ab944f27 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError3.pkl @@ -0,0 +1,4 @@ +// ConstantEntriesLiteralNode +res: Mapping = new { + [1] = "foo" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError4.pkl new file mode 100644 index 00000000..471635c9 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError4.pkl @@ -0,0 +1,6 @@ +num = 1 + +// EntriesLiteralNode +res: Mapping = new { + [num] = "foo" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError5.pkl new file mode 100644 index 00000000..5274123a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError5.pkl @@ -0,0 +1,7 @@ +// GeneratorObjectLiteralNode +res: Mapping = new { + when (false) { + ["foo"] = "foo" + } + [1] = "foo" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError6.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError6.pkl new file mode 100644 index 00000000..2ef18478 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError6.pkl @@ -0,0 +1,11 @@ +a = new Listing { "hi" } + +b = (a) { + "hihih" +} + +bar = new Mapping { + [b] = "foo" +} + +res: Mapping, String> = bar diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError7.pkl new file mode 100644 index 00000000..d75df227 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError7.pkl @@ -0,0 +1,3 @@ +res = new Mapping, String> { + [new Listing { "hi" }] = "hi" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError8.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError8.pkl new file mode 100644 index 00000000..273b329b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError8.pkl @@ -0,0 +1,3 @@ +res: Mapping = new { + ...Map("foo", 1) +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing4.pkl new file mode 100644 index 00000000..0d1ff55d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing4.pkl @@ -0,0 +1,32 @@ +amends "../snippetTest.pkl" + +examples { + ["listings are lazy"] { + // backed by ConstantEntriesLiteralNode + local listing = new Listing { + "foo" + throw("uh oh") + } + listing[0] + module.catch(() -> listing[1]) + } + ["listings are lazy with generator entries"] { + local listing = new Listing { + when (false) { + "uh oh" + } + "foo" + throw("uh oh") + } + listing[0] + } + ["nested listings are also lazy"] { + local listing = new Listing> { + new { + "bar" + throw("uh oh") + } + } + listing[0][0] + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing5.pkl new file mode 100644 index 00000000..2d910213 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing5.pkl @@ -0,0 +1,168 @@ +amends "../snippetTest.pkl" + +facts { + ["equals"] { + local l1 = new Listing {} + local l2: Listing = l1 + + l1 == l2 + } +} + +examples { + ["type check: new with explicit parent"] { + local l = new Listing { + 1 + } + + module.catch(() -> l[0]) + } + + ["type check: local new with inferred parent"] { + local l: Listing = new { + 1 + } + + module.catch(() -> l[0]) + } + + ["type check: local paramaterized property type, unparamaterized new with explicit parent"] { + local m: Listing = new Listing { + 1 + } + module.catch(() -> m[0]) + } + + ["type check: local unparameterized property type, paramaterized new with explicit parent"] { + local m: Listing = new Listing { + 1 + } + module.catch(() -> m[0]) + } + + ["amending listings does not require type checks on amending object members"] { + local m: Listing = new { + "hi" + } + // ElementsLiteralNode + (m) { + 1 + } + // ElementsEntriesLiteralNode + (m) { + [0] = 1 + 2 + } + // GeneratorObjectLiteralNode + (m) { + when (false) { + "hi" + } + 1 + } + } + + ["type check: constraints on both property type node and explicit parent type node are checked"] { + local l: Listing = new Listing { + "Ba" + "bar" + } + + module.catch(() -> l[0]) + module.catch(() -> l[1]) + } + + ["type check: nested listings: constraints on both parent type node and child type node are checked"] { + local res12: Listing> = + new Listing> { + new { + "Ba" + "bar" + } + } + + module.catch(() -> res12[0][0]) + module.catch(() -> res12[0][1]) + } + + ["type check: propagate from List"] { + local l: List> = List( + new Listing { + "Ba" + "bar" + } + ) + module.catch(() -> l[0][0]) + module.catch(() -> l[0][1]) + } + + ["type check: propagate function types"] { + local l = new Listing { + "Ba" // fails `length.isOdd` + "bar" // fails `this == capitalize()` + } + local l2 = new Listing { + "Ba" // fails `length.isOdd` + "bar" // fails `this == capitalize()` + } + // type check String(length.isOdd) should be propagated to the listing via a paramater type + // annotation + local function func1(listing: Listing) = listing + // type check String(length.isOdd) should be propagated to the listing via a return type + // annotation + local function func2(listing): Listing = listing + // type check String(length.isOdd) and String(this == capitalize()) should be propagated to the + // listing via both parameter type and return type annotations + local function func3(listing: Listing): Listing = listing + + module.catch(() -> func1(l)[0]) + module.catch(() -> func1(l)[1]) + module.catch(() -> func2(l)[0]) + module.catch(() -> func2(l)[1]) + module.catch(() -> func3(l2)[0]) + module.catch(() -> func3(l2)[1]) + } + + ["type check: union type"] { + local l: Listing|Listing = + new Listing { + "Ba" // fails length.isOdd and length == 4 + "bar" // fails this == capitalize() + "Bazz" // passes this == capitalize() and length == 4 + "Qux" // passes this == capitalize() and length.isOdd + } + module.catch(() -> l) + } + + ["type check: nullable type"] { + local l: Listing? = + new Listing { + "Ba" // fails length.isOdd + "bar" // fails this == capitalize() + } + module.catch(() -> l!![0]) + module.catch(() -> l!![1]) + } + + ["type check: propagate lambda type"] { + local func1 = (it: Listing) -> it + + local l = new Listing { + "Ba" // fails `length.isOdd` + "bar" // fails `this == capitalize()` + } + + module.catch(() -> func1.apply(l)[0]) + module.catch(() -> func1.apply(l)[1]) + } + + ["intermediary objects are not checked"] { + local l = new Listing { + // okay, because this node never gets evaluated + 50 + } + (l) { + [0] = "Hello" + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing6.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing6.pkl new file mode 100644 index 00000000..2a899c53 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing6.pkl @@ -0,0 +1,46 @@ +amends "../snippetTest.pkl" + +local class TheClass { + local isLongString = (it) -> it.length > 10 + + prop: Listing +} + +local class TheClass2 { + hidden requiredLength: Int + + prop: Listing requiredLength)> +} + +examples { + ["name resolution in type constraint"] { + // should be able to resolve `isLongString` when checking this member + module.catch(() -> new TheClass { + prop { + "too short" + } + }.prop[0]) + + new TheClass { + prop { + "this is long enough" + } + } + } + ["resolves the receiver"] { + local base: TheClass2 = new { + requiredLength = 5 + } + (base) { + prop { + "long enough" + } + } + module.catch(() -> (base) { + requiredLength = 10 + prop { + "too short" + } + }.prop[0]) + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing7.pkl new file mode 100644 index 00000000..47e6a843 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/listing7.pkl @@ -0,0 +1,10 @@ +// ensure that these members are only evaluated once (trace should only be emitted once) +listing = new Listing { trace(1) } + +listing2: Listing = listing + +listing3 = new Listing { + new Listing { trace(2) } +} + +listing4: Listing> = listing3 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/listings/typeCheck.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/typeCheck.pkl index 0a3b8c0c..95935a01 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/listings/typeCheck.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/listings/typeCheck.pkl @@ -1,41 +1,367 @@ import "pkl:test" -hidden x1: Listing = new { +local x1: Listing = new { "pigeon" 42 "barn owl" } -hidden x2: Listing 3)> = new { +local x2: Listing 3)> = new { "pigeon" "bob" } -hidden x3: Listing(!isEmpty) +res1 = x1[0] +res2 = test.catchOrNull(() -> x1[1]) +res3 = x2[0] +res4 = test.catchOrNull(() -> x2[1]) -res1 = test.catch(() -> x1) -res2 = test.catch(() -> x2) -res3 = test.catch(() -> x3) - -hidden x4: Listing = new { +local x4: Listing = new { throw("element unnecessarily evaluated") } -hidden x5: Listing = new { +local x5: Listing = new { throw("element unnecessarily evaluated") } -hidden x6: Listing = new { +local x6: Listing = new { throw("element unnecessarily evaluated") } -hidden x7 = new Listing { +local x7 = new Listing { throw("element unnecessarily evaluated") 42 throw("element unnecessarily evaluated") } -res4 = x4.length == 1 -res5 = x5.length == 1 -res6 = x6.length == 1 -res7 = x7[1] == 42 +local x8 = new Listing { + throw("element unneccessarily evaluated") +} + +res5 = x4.length == 1 +res6 = x5.length == 1 +res7 = x6.length == 1 +res8 = x7[1] == 42 +res9 = x8.length == 1 + +local x9 = new Listing { + "foo" + 1 +} +local x10 = x9 as Listing + +res10 = x9 is Listing +res11 = x10[0] +res12 = test.catch(() -> x10[1]) + +local x11: Listing = new Listing { + "" +} + +res13 = test.catch(() -> x11[0]) + +local x12: Listing = new Listing { + "" +} + +res14 = test.catch(() -> x12[0]) + +local l = new Listing { "foo"; 1 } + +local x13: (Listing|Listing) = l + +local x14: Listing|Listing? = l + +local x15: Listing|(Listing|Int) = l + +local x16: Listing|Int = l + +res15 = test.catch(() -> x13) +res16 = test.catch(() -> x14) +res17 = test.catch(() -> x15) +// just accessing x16 doesn't throw because only one Listing in the union type +res18 = x16.length +// noinspection TypeMismatch +res19 = test.catch(() -> x16[1]) + +local x17: Listing> = new { + new { + 5 + } +} + +res20 = x17.length +res21 = x17[0].length +res22 = test.catch(() -> x17[0][0]) + +local x18 = new Listing { 1; 2; 3 } as Listing + +res23 = x18.length +res24 = test.catch(() -> x18[0]) + +local x19 = new Listing { + when (true) { + 15 + } +} + +res25 = x19.length +res26 = test.catch(() -> x19[0]) + +local x20 = new Listing { + ...List(1, 2, 3) +} + +res27 = x20.length +res28 = test.catch(() -> x20[0]) + +local x21 = new Listing { + for (elem in List(1, 2, 3)) { + elem + } +} + +res29 = x21.length +res30 = test.catch(() -> x21[0]) + +local x22: Listing = new { + "hi" +} + +// typechecks not required when amending +// ElementsLiteralNode +res31 = (x22) { + "hi" +} + +// ElementsEntriesLiteralNode +res32 = (x22) { + [0] = 1 + 2 +} + +// GeneratorObjectLiteralNode +res33 = (x22) { + when (false) { + "hi" + } + 1 +} + +// GeneratorSpreadNode +res34 = (x22) { + ...List(1, 2, 3) +} + +local x23: Listing> = + new Listing> { + new { + "Ba" + "bar" + } + } + +res35 = test.catch(() -> x23[0][0]) +res36 = test.catch(() -> x23[0][1]) + +// check listings from inside a list +local x24: List> = List( + new Listing { + "Ba" + "bar" + } +) + +res37 = test.catch(() -> x24[0][0]) +res38 = test.catch(() -> x24[0][1]) + +local x25: List> = List( + "hello", + new Listing { + "foo" + }, + "goodbye" +) + +res39 = x25[0] +// retain lazy typecheck of listing. +res40 = x25[1].length +res41 = test.catch(() -> x25[1][0]) +res42 = x25[2] + +// check listings from inside a set +local x26: Set> = Set( + new Listing { + "Ba" + "bar" + } +) + +res43 = test.catch(() -> x26[0][0]) + +local x27: Set> = Set( + "hello", + new Listing { + "foo" + }, + "goodbye" +) + +// sets are eagerly checked (need to compute hash code, therefore need to deep force) +res45 = test.catch(() -> x27) + +local x28: List>|List> = List( + new Listing { "hello" } +) + +res46 = x28[0][0] + +local x29: List>|List> = List( + new Listing { 1; "hello" } +) + +res47 = test.catch(() -> x29) + +// check listings from inside a map +local x30: Map> = Map( + "hello", + new Listing { + "Ba" + "bar" + } +) + +res48 = x30["hello"].length +res49 = test.catch(() -> x30["hello"][0]) +res50 = test.catch(() -> x30["hello"][1]) + +local x31: Map> = Map( + "hello", 1, + "thelisting", new Listing { + 1 + 2 + }, + "goodbye", 2 +) + +res51 = x31.length +res52 = x31["hello"] +res53 = x31["goodbye"] +res54 = x31["thelisting"].length +res55 = test.catch(() -> x31["thelisting"][0]) +res56 = test.catch(() -> x31["thelisting"][1]) + +local x32: Map, Int> = Map( + new Listing { 1; 2 }, + 1 +) + +res57 = test.catch(() -> x32) + +local x33: Map|Int> = Map( + "first", 1, + "second", new Listing { "hi" } +) + +res58 = x33.length +res59 = x33["first"] +res60 = x33["second"].length +res61 = test.catch(() -> x33["second"][0]) + +local x34: Pair, Listing> = Pair( + new Listing { 1 }, + new Listing { 2 } +) + +res62 = x34.first.length +res63 = x34.second.length + +res64 = test.catch(() -> x34.first[0]) +res65 = test.catch(() -> x34.second[0]) + +local x35: Pair> = Pair( + 5, + new Listing { 1 } +) + +res66 = x35.first +res67 = x35.second.length +res68 = test.catch(() -> x35.second[0]) + +local x36: Collection> = List( + 1, + new Listing { "hello"; 1 } +) + +res69 = x36.length +res70 = x36.first +res71 = x36[1].length +res73 = x36[1][0] +res74 = test.catch(() -> x36[1][1]) + +local x37: Collection> = Set( + 1, + new Listing { + "hello" + 1 + } +) + +res75 = test.catch(() -> x37) + +local x38: Collection>|Collection> = + List(new Listing { + 1 + "hi" + }) + +res76 = test.catch(() -> x38) + +local class Person { + prop1 = 1 + prop2 = 2 + prop3 = "hi" +} + +local x39: Listing = new Person {}.toMap().values.toListing() + +res77 = x39.length +res78 = x39[0] +res79 = x39[1] +res80 = test.catch(() -> x39[2]) + +local x40: Listing = new { + ...List(1, 2, "hello") +} + +res81 = x40.length +res82 = x40[0] +res83 = x40[1] +res84 = test.catch(() -> x40[2]) + +// returns a new listing +function myFunction(elem: Listing) = elem + +local x41 = myFunction(new Listing { "hello" }) + +res85 = x41.length +res86 = test.catch(() -> x41[0]) + +function myFunction2(elem): Listing = elem + +local x42 = myFunction(new Listing { "hello" }) + +res87 = x42.length +res88 = test.catch(() -> x42[0]) + +local x43 = (it: Listing) -> it +local x44 = x43.apply(new Listing { "hello" }) + +res89 = x44.length +res90 = test.catch(() -> x44[0]) + +function myFunction3(elem: Listing): Listing = elem +local x45 = myFunction3(new Listing { "hello" }) + +res91 = x45.length +res92 = test.catch(() -> x45[0]) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/listings2/typeCheck.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/listings2/typeCheck.pkl index cfbac17e..6aaf2642 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/listings2/typeCheck.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/listings2/typeCheck.pkl @@ -15,9 +15,9 @@ hidden x2: Listing 3)> = new { hidden x3: Listing(!isEmpty) -res1 = test.catch(() -> x1) -res2 = test.catch(() -> x2) -res3 = test.catch(() -> x3) +res1 = test.catch(() -> x1.toList()) +res2 = test.catch(() -> x2.toList()) +res3 = test.catch(() -> x3.toList()) hidden x4: Listing = new { when (true) { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/mappings/typeCheck.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/mappings/typeCheck.pkl index 70e861b1..0e4ca041 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/mappings/typeCheck.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/mappings/typeCheck.pkl @@ -27,12 +27,16 @@ local x6: Mapping = new { ["pigeon"] = "" } -res1 = test.catch(() -> x1) -res2 = test.catch(() -> x2) -res3 = test.catch(() -> x3) -res4 = test.catch(() -> x4) -res5 = test.catch(() -> x5) -res6 = test.catch(() -> x6) +res1 = x1.length +res2 = test.catch(() -> x1["barn owl"]) +res3 = x2.length +res4 = x2["fred"] +res5 = test.catch(() -> x2["barney"]) +res6 = test.catch(() -> x3) +res7 = test.catch(() -> x4) +res8 = test.catch(() -> x5) +res9 = x6.length +res10 = test.catch(() -> x6["pigeon"]) hidden x7: Mapping = new { ["first"] = throw("value unnecessarily evaluated") @@ -52,7 +56,312 @@ hidden x10 = new Mapping { ["third"] = throw("value unnecessarily evaluated") } -res7 = !x7.isEmpty -res8 = !x8.isEmpty -res9 = !x9.isEmpty -res10 = x10["second"] == 42 +res11 = x7.length +res12 = x8.length +res13 = x9.length +res14 = x10.length +res15 = x10["second"] + +local x11: Mapping = new Mapping { + ["foo"] = "" +} + +res16 = x11.length +res17 = test.catch(() -> x11["foo"]) + +local x12: Mapping = new Mapping { + ["foo"] = "" +} + +res18 = x12.length +res19 = test.catch(() -> x12["foo"]) + +local m = new Mapping { + ["one"] = 1 + ["two"] = "two" +} + +local x13: Mapping|Mapping = m +local x14: Mapping|Mapping? = m +local x15: Mapping|(Mapping|Int) = m +local x16: Mapping|Int = m + +res20 = test.catch(() -> x13) +res21 = test.catch(() -> x14) +res22 = test.catch(() -> x15) +res23 = x16.length +res24 = test.catch(() -> x16["one"]) + +local x17: Mapping> = new { + ["foo"] { + ["bar"] = 1 + } +} + +res25 = x17.length +res26 = x17["foo"].length +res27 = test.catch(() -> x17["foo"]["bar"]) + +local x18 = new Mapping { ["foo"] = 1 } as Mapping + +res28 = x18.length +res29 = test.catch(() -> x18["foo"]) + +local x19 = new Mapping { + when (true) { + ["foo"] = 1 + } +} + +res30 = x19.length +res31 = test.catch(() -> x19["foo"]) + +local x20 = new Mapping { + ...Map("foo", 1) +} + +res32 = x20.length +res33 = test.catch(() -> x20["foo"]) + +local x21 = new Mapping { + for (k, v in Map("foo", 1)) { + [k] = v + } +} + +res34 = x21.length +res35 = test.catch(() -> x21["foo"]) + +local x22: Mapping = new { + ["hi"] = "hi" +} + +// typechecks not required when amending +// ElementsEntriesLiteralNode +res36 = (x22) { + ["hi2"] = 1 +} + +// GeneratorObjectLiteralNode +res37 = (x22) { + when (true) { + ["hi2"] = 1 + } +} + +// GeneratorSpreadNode +res38 = (x22) { + ...Map("hi2", 1) +} + +local x23: Mapping> = new Mapping> { + ["foo"] { + ["first"] = "Ba" + ["second"] = "bar" + } +} + +res39 = test.catch(() -> x23["foo"]["first"]) +res40 = test.catch(() -> x23[""]) + +// check mappings from inside a list +local x24: List> = List( + new Mapping { + ["first"] = "Ba" + ["second"] = "bar" + } +) + +res41 = test.catch(() -> x24[0]["first"]) +res42 = test.catch(() -> x24[0]["second"]) + +local x25: List> = List( + "hello", + new Mapping { + ["foo"] = "foo" + }, + "goodbye" +) + +res43 = x25[0] +// retain lazy typecheck of mapping. +res44 = x25[1].length +res45 = test.catch(() -> x25[1]["foo"]) +res46 = x25[2] + +// check mappings from inside a set +local x26: Set> = Set( + new Mapping { + ["first"] = "Ba" + ["second"] = "bar" + } +) + +// sets are eagerly checked (need to compute hash code, therefore need to deep force) +res47 = test.catch(() -> x26) + +local x28: List>|List> = List( + new Mapping { ["foo"] = 1 } +) + +res48 = x28[0]["foo"] + +local x29: List>|List> = List( + new Mapping { + ["foo"] = 1 + ["bar"] = "hi" + } +) + +res49 = test.catch(() -> x29) + +// check mappings from inside a map +local x30: Map> = Map( + "hello", + new Mapping { + ["first"] = "Ba" + ["second"] = "bar" + } +) + +res50 = x30["hello"].length +res51 = test.catch(() -> x30["hello"]["first"]) +res52 = test.catch(() -> x30["hello"]["second"]) + +local x31: Map> = Map( + "hello", 1, + "themapping", new Mapping { + ["one"] = 1 + ["two"] = 2 + }, + "goodbye", 2 +) + +res53 = x31.length +res54 = x31["hello"] +res55 = x31["goodbye"] +res56 = x31["themapping"].length +res57 = test.catch(() -> x31["themapping"]["one"]) +res58 = test.catch(() -> x31["themapping"]["two"]) + +local x32: Map, Int> = Map( + new Mapping { ["one"] = 1 }, + 1 +) + +res59 = test.catch(() -> x32) + +local x33: Map|Int> = Map( + "first", 1, + "second", new Mapping { ["hi"] = "hi" } +) + +res60 = x33.length +res61 = x33["first"] +res62 = x33["second"].length +res63 = test.catch(() -> x33["second"]["hi"]) + +local x34: Pair, Mapping> = Pair( + new Mapping { ["one"] = 1 }, + new Mapping { ["one"] = 1 } +) + +res64 = x34.first.length +res65 = x34.second.length + +res66 = test.catch(() -> x34.first["one"]) +res67 = test.catch(() -> x34.second["one"]) + +local x35: Pair> = Pair( + 5, + new Mapping { ["one"] = 1 } +) + +res68 = x35.first +res69 = x35.second.length +res70 = test.catch(() -> x35.second["one"]) + +local x36: Collection> = List( + 1, + new Mapping { + ["one"] = "hello" + ["two"] = 1 + } +) + + +res71 = x36.length +res72 = x36.first +res73 = x36[1].length +res74 = x36[1]["one"] +res75 = test.catch(() -> x36[1]["two"]) + +local x37: Collection> = Set( + 1, + new Mapping { + ["one"] = "hello" + ["two"] = 1 + } +) + +res77 = test.catch(() -> x37) + +local x38: Collection>|Collection> = + List(new Mapping { + ["one"] = "hello" + ["two"] = 1 + }) + +res78 = test.catch(() -> x38) + +local class Person { + prop1 = 1 + prop2 = 2 + prop3 = "hi" +} + +local x39: Mapping = new Person {} + .toMap() + .toMapping() + + +res79 = x39.length +res80 = x39["prop1"] +res81 = x39["prop2"] +res82 = test.catch(() -> x39["prop3"]) + +local x40: Mapping = new { + ...Map("one", 1, "two", 2, "three", "hello") +} + +res83 = x40.length +res84 = x40["one"] +res85 = x40["two"] +res86 = test.catch(() -> x40["three"]) + +// returns a new mapping +function myFunction(elem: Mapping) = elem + +local x41 = myFunction(new Mapping { ["hello"] = "hello" }) + +res87 = x41.length +res88 = test.catch(() -> x41["hello"]) + +function myFunction2(elem): Mapping = elem + +local x42 = myFunction(new Mapping { ["hello"] = "hello" }) + +res89 = x42.length +res90 = test.catch(() -> x42["hello"]) + +local x43 = (it: Mapping) -> it +local x44 = x43.apply(new Mapping { ["hello"] = "hello" }) + +res91 = x44.length +res92 = test.catch(() -> x44["hello"]) + +function myFunction3(elem: Mapping): Mapping = elem +local x45 = myFunction3(new Mapping { ["hello"] = "hello" }) + +res93 = x45.length +res94 = test.catch(() -> x45["hello"]) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/mappings2/typeCheck.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/mappings2/typeCheck.pkl index edb89901..9886de75 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/mappings2/typeCheck.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/mappings2/typeCheck.pkl @@ -33,12 +33,12 @@ local x6: Mapping = new { ["pigeon"] = "" } -res1 = test.catch(() -> x1) -res2 = test.catch(() -> x2) +res1 = test.catch(() -> x1["barn owl"]) +res2 = test.catch(() -> x2["barney"]) res3 = test.catch(() -> x3) res4 = test.catch(() -> x4) res5 = test.catch(() -> x5) -res6 = test.catch(() -> x6) +res6 = test.catch(() -> x6["pigeon"]) hidden x7: Mapping = new { when (true) { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/as3.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/as3.pcf new file mode 100644 index 00000000..c2024570 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/as3.pcf @@ -0,0 +1,17 @@ +examples { + ["set of listing"] { + 1 + false + "Expected value of type `Int`, but got type `String`. Value: \"one\"" + } + ["listing"] { + true + "Expected value of type `String`, but got type `Int`. Value: 1" + } + ["mapping"] { + true + "Expected value of type `String`, but got type `Int`. Value: 1" + "Expected value of type `String`, but got type `Int`. Value: 2" + "Expected value of type `Int`, but got type `String`. Value: \"hi\"" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/forGeneratorWrongVariableName.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/forGeneratorWrongVariableName.err index e8f50b79..82d55cbd 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/forGeneratorWrongVariableName.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/forGeneratorWrongVariableName.err @@ -5,10 +5,6 @@ x | ["\(idx)_2"] = o // at this point, `o` should be out of scope ^ at forGeneratorWrongVariableName#res[#2] (file:///$snippetsDir/input/errors/forGeneratorWrongVariableName.pkl) -x | res: Mapping = new { - ^^^^^ -at forGeneratorWrongVariableName#res (file:///$snippetsDir/input/errors/forGeneratorWrongVariableName.pkl) - xxx | text = renderer.renderDocument(value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError1.err new file mode 100644 index 00000000..5d76a016 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError1.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res = new Listing { + ^^^^^^ +at listingTypeCheckError1#res (file:///$snippetsDir/input/errors/listingTypeCheckError1.pkl) + +x | 1 + ^ +at listingTypeCheckError1#res[#1] (file:///$snippetsDir/input/errors/listingTypeCheckError1.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError2.err new file mode 100644 index 00000000..52a01085 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError2.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res = new Listing { + ^^^^^^ +at listingTypeCheckError2#res (file:///$snippetsDir/input/errors/listingTypeCheckError2.pkl) + +x | one + ^^^ +at listingTypeCheckError2#res[#1] (file:///$snippetsDir/input/errors/listingTypeCheckError2.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError3.err new file mode 100644 index 00000000..d8ea2829 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError3.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Type constraint `!isEmpty` violated. +Value: "" + +x | res: Listing = new Listing { + ^^^^^^^^ +at listingTypeCheckError3#res (file:///$snippetsDir/input/errors/listingTypeCheckError3.pkl) + +x | "" + ^^ +at listingTypeCheckError3#res[#1] (file:///$snippetsDir/input/errors/listingTypeCheckError3.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError4.err new file mode 100644 index 00000000..9ed55f3a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError4.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Type constraint `endsWith("ga")` violated. +Value: "hola" + +x | res: Listing = new Listing { + ^^^^^^^^^^^^^^ +at listingTypeCheckError4#res (file:///$snippetsDir/input/errors/listingTypeCheckError4.pkl) + +x | "hola" + ^^^^^^ +at listingTypeCheckError4#res[#1] (file:///$snippetsDir/input/errors/listingTypeCheckError4.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError5.err new file mode 100644 index 00000000..db8f3773 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError5.err @@ -0,0 +1,19 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | local l = new Listing { 1; 2; 3 } as Listing + ^^^^^^ +at listingTypeCheckError5#l (file:///$snippetsDir/input/errors/listingTypeCheckError5.pkl) + +x | local l = new Listing { 1; 2; 3 } as Listing + ^ +at listingTypeCheckError5#l[#1] (file:///$snippetsDir/input/errors/listingTypeCheckError5.pkl) + +x | res = l[0] + ^^^^ +at listingTypeCheckError5#res (file:///$snippetsDir/input/errors/listingTypeCheckError5.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError1.err new file mode 100644 index 00000000..80b073f0 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError1.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res = new Mapping { + ^^^^^^ +at mappingTypeCheckError1#res (file:///$snippetsDir/input/errors/mappingTypeCheckError1.pkl) + +x | ["foo"] = 1 + ^ +at mappingTypeCheckError1#res["foo"] (file:///$snippetsDir/input/errors/mappingTypeCheckError1.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError2.err new file mode 100644 index 00000000..023c569d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError2.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res: Mapping = new { + ^^^^^^ +at mappingTypeCheckError2#res (file:///$snippetsDir/input/errors/mappingTypeCheckError2.pkl) + +x | ["foo"] = 1 + ^ +at mappingTypeCheckError2#res["foo"] (file:///$snippetsDir/input/errors/mappingTypeCheckError2.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError3.err new file mode 100644 index 00000000..d14fd287 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError3.err @@ -0,0 +1,19 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res: Mapping = new { + ^^^^^^ +at mappingTypeCheckError3#res (file:///$snippetsDir/input/errors/mappingTypeCheckError3.pkl) + +x | [1] = "foo" + ^ +at mappingTypeCheckError3#res[1] (file:///$snippetsDir/input/errors/mappingTypeCheckError3.pkl) + +x | res: Mapping = new { + ^^^^^ +at mappingTypeCheckError3#res (file:///$snippetsDir/input/errors/mappingTypeCheckError3.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError4.err new file mode 100644 index 00000000..80a9a12c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError4.err @@ -0,0 +1,19 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res: Mapping = new { + ^^^^^^ +at mappingTypeCheckError4#res (file:///$snippetsDir/input/errors/mappingTypeCheckError4.pkl) + +x | [num] = "foo" + ^^^ +at mappingTypeCheckError4#res[#1] (file:///$snippetsDir/input/errors/mappingTypeCheckError4.pkl) + +x | res: Mapping = new { + ^^^^^ +at mappingTypeCheckError4#res (file:///$snippetsDir/input/errors/mappingTypeCheckError4.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError5.err new file mode 100644 index 00000000..4b88406a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError5.err @@ -0,0 +1,19 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res: Mapping = new { + ^^^^^^ +at mappingTypeCheckError5#res (file:///$snippetsDir/input/errors/mappingTypeCheckError5.pkl) + +x | [1] = "foo" + ^ +at mappingTypeCheckError5#res[1] (file:///$snippetsDir/input/errors/mappingTypeCheckError5.pkl) + +x | res: Mapping = new { + ^^^^^ +at mappingTypeCheckError5#res (file:///$snippetsDir/input/errors/mappingTypeCheckError5.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError6.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError6.err new file mode 100644 index 00000000..caea6ec7 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError6.err @@ -0,0 +1,19 @@ +–– Pkl Error –– +Type constraint `length == 5` violated. +Value: "hi" + +xx | res: Mapping, String> = bar + ^^^^^^^^^^^ +at mappingTypeCheckError6#res (file:///$snippetsDir/input/errors/mappingTypeCheckError6.pkl) + +x | [b] = "foo" + ^ +at mappingTypeCheckError6#bar[#1] (file:///$snippetsDir/input/errors/mappingTypeCheckError6.pkl) + +xx | res: Mapping, String> = bar + ^^^ +at mappingTypeCheckError6#res (file:///$snippetsDir/input/errors/mappingTypeCheckError6.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError7.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError7.err new file mode 100644 index 00000000..f0a65d3f --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError7.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Type constraint `length == 5` violated. +Value: "hi" + +x | res = new Mapping, String> { + ^^^^^^^^^^^ +at mappingTypeCheckError7#res (file:///$snippetsDir/input/errors/mappingTypeCheckError7.pkl) + +x | [new Listing { "hi" }] = "hi" + ^^^^^^^^^^^^^^^^^^^^ +at mappingTypeCheckError7#res[#1] (file:///$snippetsDir/input/errors/mappingTypeCheckError7.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError8.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError8.err new file mode 100644 index 00000000..db5ad74c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError8.err @@ -0,0 +1,11 @@ +–– Pkl Error –– +Expected value of type `String`, but got type `Int`. +Value: 1 + +x | res: Mapping = new { + ^^^^^^ +at mappingTypeCheckError8#res (file:///$snippetsDir/input/errors/mappingTypeCheckError8.pkl) + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing4.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing4.pcf new file mode 100644 index 00000000..8eaad442 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing4.pcf @@ -0,0 +1,12 @@ +examples { + ["listings are lazy"] { + "foo" + "uh oh" + } + ["listings are lazy with generator entries"] { + "foo" + } + ["nested listings are also lazy"] { + "bar" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing5.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing5.pcf new file mode 100644 index 00000000..1fe6bf38 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing5.pcf @@ -0,0 +1,69 @@ +facts { + ["equals"] { + true + } +} +examples { + ["type check: new with explicit parent"] { + "Expected value of type `String`, but got type `Int`. Value: 1" + } + ["type check: local new with inferred parent"] { + "Expected value of type `String`, but got type `Int`. Value: 1" + } + ["type check: local paramaterized property type, unparamaterized new with explicit parent"] { + "Expected value of type `String`, but got type `Int`. Value: 1" + } + ["type check: local unparameterized property type, paramaterized new with explicit parent"] { + "Expected value of type `String`, but got type `Int`. Value: 1" + } + ["amending listings does not require type checks on amending object members"] { + new { + "hi" + 1 + } + new { + 1 + 2 + } + new { + "hi" + 1 + } + } + ["type check: constraints on both property type node and explicit parent type node are checked"] { + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + } + ["type check: nested listings: constraints on both parent type node and child type node are checked"] { + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + } + ["type check: propagate from List"] { + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + } + ["type check: propagate function types"] { + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + } + ["type check: union type"] { + "Expected value of type `Listing|Listing`, but got a different `Listing`. Value: new Listing { \"Ba\"; ?; ?; ? }" + } + ["type check: nullable type"] { + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + } + ["type check: propagate lambda type"] { + "Type constraint `length.isOdd` violated. Value: \"Ba\"" + "Type constraint `this == capitalize()` violated. Value: \"bar\"" + } + ["intermediary objects are not checked"] { + new { + "Hello" + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing6.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing6.pcf new file mode 100644 index 00000000..64c46378 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing6.pcf @@ -0,0 +1,18 @@ +examples { + ["name resolution in type constraint"] { + "Type constraint `isLongString` violated. Value: \"too short\"" + new { + prop { + "this is long enough" + } + } + } + ["resolves the receiver"] { + new { + prop { + "long enough" + } + } + "Type constraint `length > requiredLength` violated. Value: \"too short\"" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing7.err b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing7.err new file mode 100644 index 00000000..ad24c7ed --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/listing7.err @@ -0,0 +1,18 @@ +listing { + 1 +} +listing2 { + 1 +} +listing3 { + new { + 2 + } +} +listing4 { + new { + 2 + } +} +pkl: TRACE: 1 = 1 (file:///$snippetsDir/input/listings/listing7.pkl) +pkl: TRACE: 2 = 2 (file:///$snippetsDir/input/listings/listing7.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/listings/typeCheck.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/typeCheck.pcf index d2eb0b52..f3256142 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/listings/typeCheck.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/listings/typeCheck.pcf @@ -1,7 +1,104 @@ -res1 = "Expected value of type `String`, but got type `Int`. Value: 42" -res2 = "Type constraint `length > 3` violated. Value: \"bob\"" -res3 = "Type constraint `!isEmpty` violated. Value: new Listing {}" -res4 = true +res1 = "pigeon" +res2 = "Expected value of type `String`, but got type `Int`. Value: 42" +res3 = "pigeon" +res4 = "Type constraint `length > 3` violated. Value: \"bob\"" res5 = true res6 = true res7 = true +res8 = true +res9 = true +res10 = false +res11 = "foo" +res12 = "Expected value of type `String`, but got type `Int`. Value: 1" +res13 = "Type constraint `!isEmpty` violated. Value: \"\"" +res14 = "Type constraint `!isEmpty` violated. Value: \"\"" +res15 = "Expected value of type `Listing|Listing`, but got a different `Listing`. Value: new Listing { \"foo\"; 1 }" +res16 = "Expected value of type `Listing|Listing?`, but got a different `Listing`. Value: new Listing { \"foo\"; 1 }" +res17 = "Expected value of type `Listing|(Listing|Int)`, but got a different `Listing`. Value: new Listing { \"foo\"; 1 }" +res18 = 2 +res19 = "Expected value of type `String`, but got type `Int`. Value: 1" +res20 = 1 +res21 = 1 +res22 = "Expected value of type `String`, but got type `Int`. Value: 5" +res23 = 3 +res24 = "Expected value of type `String`, but got type `Int`. Value: 1" +res25 = 1 +res26 = "Expected value of type `String`, but got type `Int`. Value: 15" +res27 = 3 +res28 = "Expected value of type `String`, but got type `Int`. Value: 1" +res29 = 3 +res30 = "Expected value of type `String`, but got type `Int`. Value: 1" +res31 { + "hi" + "hi" +} +res32 { + 1 + 2 +} +res33 { + "hi" + 1 +} +res34 { + "hi" + 1 + 2 + 3 +} +res35 = "Type constraint `length.isOdd` violated. Value: \"Ba\"" +res36 = "Type constraint `this == capitalize()` violated. Value: \"bar\"" +res37 = "Type constraint `length.isOdd` violated. Value: \"Ba\"" +res38 = "Type constraint `this == capitalize()` violated. Value: \"bar\"" +res39 = "hello" +res40 = 1 +res41 = "Expected value of type `Int`, but got type `String`. Value: \"foo\"" +res42 = "goodbye" +res43 = "Type constraint `this == capitalize()` violated. Value: \"bar\"" +res45 = "Expected value of type `String|Listing`, but got a different `Listing`. Value: new Listing { \"foo\" }" +res46 = "hello" +res47 = "Expected value of type `List>|List>`, but got a different `List`. Value: List(new Listing { 1; \"hello\" })" +res48 = 2 +res49 = "Type constraint `length.isOdd` violated. Value: \"Ba\"" +res50 = "Type constraint `this == capitalize()` violated. Value: \"bar\"" +res51 = 3 +res52 = 1 +res53 = 2 +res54 = 2 +res55 = "Expected value of type `String`, but got type `Int`. Value: 1" +res56 = "Expected value of type `String`, but got type `Int`. Value: 2" +res57 = "Expected value of type `String`, but got type `Int`. Value: 1" +res58 = 2 +res59 = 1 +res60 = 1 +res61 = "Expected value of type `Int`, but got type `String`. Value: \"hi\"" +res62 = 1 +res63 = 1 +res64 = "Expected value of type `String`, but got type `Int`. Value: 1" +res65 = "Expected value of type `String`, but got type `Int`. Value: 2" +res66 = 5 +res67 = 1 +res68 = "Expected value of type `String`, but got type `Int`. Value: 1" +res69 = 2 +res70 = 1 +res71 = 2 +res73 = "hello" +res74 = "Expected value of type `String`, but got type `Int`. Value: 1" +res75 = "Expected value of type `Int|Listing`, but got a different `Listing`. Value: new Listing { \"hello\"; 1 }" +res76 = "Expected value of type `Collection>|Collection>`, but got a different `List`. Value: List(new Listing { 1; \"hi\" })" +res77 = 3 +res78 = 1 +res79 = 2 +res80 = "Expected value of type `Int`, but got type `String`. Value: \"hi\"" +res81 = 3 +res82 = 1 +res83 = 2 +res84 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res85 = 1 +res86 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res87 = 1 +res88 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res89 = 1 +res90 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res91 = 1 +res92 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/mappings/typeCheck.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/mappings/typeCheck.pcf index 8f8b7eb8..df10ec50 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/mappings/typeCheck.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/mappings/typeCheck.pcf @@ -1,10 +1,102 @@ -res1 = "Expected value of type `List`, but got type `Int`. Value: 42" +res1 = 2 res2 = "Expected value of type `List`, but got type `Int`. Value: 42" -res3 = "Expected value of type `List`, but got type `Int`. Value: 42" -res4 = "Expected value of type `List`, but got type `Int`. Value: 42" -res5 = "Type constraint `!isEmpty` violated. Value: \"\"" -res6 = "Type constraint `!isEmpty` violated. Value: \"\"" -res7 = true -res8 = true -res9 = true -res10 = true +res3 = 2 +res4 = List(1, 2, 3) +res5 = "Expected value of type `List`, but got type `Int`. Value: 42" +res6 = "Expected value of type `List`, but got type `Int`. Value: 42" +res7 = "Expected value of type `List`, but got type `Int`. Value: 42" +res8 = "Type constraint `!isEmpty` violated. Value: \"\"" +res9 = 1 +res10 = "Type constraint `!isEmpty` violated. Value: \"\"" +res11 = 1 +res12 = 1 +res13 = 1 +res14 = 3 +res15 = 42 +res16 = 1 +res17 = "Type constraint `!isEmpty` violated. Value: \"\"" +res18 = 1 +res19 = "Type constraint `!isEmpty` violated. Value: \"\"" +res20 = "Expected value of type `Mapping|Mapping`, but got a different `Mapping`. Value: new Mapping { [\"one\"] = 1; [\"two\"] = \"two\" }" +res21 = "Expected value of type `Mapping|Mapping?`, but got a different `Mapping`. Value: new Mapping { [\"one\"] = 1; [\"two\"] = \"two\" }" +res22 = "Expected value of type `Mapping|(Mapping|Int)`, but got a different `Mapping`. Value: new Mapping { [\"one\"] = 1; [\"two\"] = \"two\" }" +res23 = 2 +res24 = "Expected value of type `String`, but got type `Int`. Value: 1" +res25 = 1 +res26 = 1 +res27 = "Expected value of type `String`, but got type `Int`. Value: 1" +res28 = 1 +res29 = "Expected value of type `String`, but got type `Int`. Value: 1" +res30 = 1 +res31 = "Expected value of type `String`, but got type `Int`. Value: 1" +res32 = 1 +res33 = "Expected value of type `String`, but got type `Int`. Value: 1" +res34 = 1 +res35 = "Expected value of type `String`, but got type `Int`. Value: 1" +res36 { + ["hi"] = "hi" + ["hi2"] = 1 +} +res37 { + ["hi"] = "hi" + ["hi2"] = 1 +} +res38 { + ["hi"] = "hi" + ["hi2"] = 1 +} +res39 = "Type constraint `length.isOdd` violated. Value: \"Ba\"" +res40 = "Cannot find key `\"\"`." +res41 = "Type constraint `length.isOdd` violated. Value: \"Ba\"" +res42 = "Type constraint `this == capitalize()` violated. Value: \"bar\"" +res43 = "hello" +res44 = 1 +res45 = "Expected value of type `Int`, but got type `String`. Value: \"foo\"" +res46 = "goodbye" +res47 = "Type constraint `this == capitalize()` violated. Value: \"bar\"" +res48 = 1 +res49 = "Expected value of type `List>|List>`, but got a different `List`. Value: List(new Mapping { [\"foo\"] = 1; [\"bar\"] = \"hi\" })" +res50 = 2 +res51 = "Type constraint `length.isOdd` violated. Value: \"Ba\"" +res52 = "Type constraint `this == capitalize()` violated. Value: \"bar\"" +res53 = 3 +res54 = 1 +res55 = 2 +res56 = 2 +res57 = "Expected value of type `String`, but got type `Int`. Value: 1" +res58 = "Expected value of type `String`, but got type `Int`. Value: 2" +res59 = "Expected value of type `String`, but got type `Int`. Value: 1" +res60 = 2 +res61 = 1 +res62 = 1 +res63 = "Expected value of type `Int`, but got type `String`. Value: \"hi\"" +res64 = 1 +res65 = 1 +res66 = "Expected value of type `String`, but got type `Int`. Value: 1" +res67 = "Expected value of type `String`, but got type `Int`. Value: 1" +res68 = 5 +res69 = 1 +res70 = "Expected value of type `String`, but got type `Int`. Value: 1" +res71 = 2 +res72 = 1 +res73 = 2 +res74 = "hello" +res75 = "Expected value of type `String`, but got type `Int`. Value: 1" +res77 = "Expected value of type `Int|Mapping`, but got a different `Mapping`. Value: new Mapping { [\"one\"] = \"hello\"; [\"two\"] = 1 }" +res78 = "Expected value of type `Collection>|Collection>`, but got a different `List`. Value: List(new Mapping { [\"one\"] = \"hello\"; [\"two\"] = 1 })" +res79 = 3 +res80 = 1 +res81 = 2 +res82 = "Expected value of type `Int`, but got type `String`. Value: \"hi\"" +res83 = 3 +res84 = 1 +res85 = 2 +res86 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res87 = 1 +res88 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res89 = 1 +res90 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res91 = 1 +res92 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" +res93 = 1 +res94 = "Expected value of type `Int`, but got type `String`. Value: \"hello\"" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err index fafb5600..ac788fb4 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err @@ -10,6 +10,6 @@ at pkl.Project#dependencies (pkl:Project) Type constraint `isValidLoadDependency` violated. Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetti... -x | dependencies { - ^^^^^^^^^^^^^^ -at PklProject#dependencies (file:///$snippetsDir/input/projects/badProjectDeps4/PklProject) +x | ["badLocalProject"] = import("../badLocalProject/PklProject") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at PklProject#dependencies["badLocalProject"] (file:///$snippetsDir/input/projects/badProjectDeps4/PklProject) diff --git a/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt index 15f99d6b..a4a55f77 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt @@ -114,13 +114,13 @@ class EvaluateTestsTest { ) assertThat(results.totalTests()).isEqualTo(1) - assertThat(results.totalFailures()).isEqualTo(0) + assertThat(results.totalFailures()).isEqualTo(1) assertThat(results.failed()).isTrue val res = results.results[0] - assertThat(res.name).isEqualTo("text") - assertThat(res.failures).isEmpty() - assertThat(res.errors.size).isEqualTo(1) + assertThat(res.name).isEqualTo("should fail") + assertThat(res.failures).hasSize(1) + assertThat(res.errors).hasSize(1) val error = res.errors[0] assertThat(error.message).isEqualTo("got an error") @@ -134,10 +134,6 @@ class EvaluateTestsTest { ^^^^^^^^^^^^^^^^^^^^^ at text#facts["should fail"][#2] (repl:text) - 3 | facts { - ^^^^^^^ - at text#facts (repl:text) - """ .trimIndent() ) diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt index b5a77a95..484b3d8e 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt @@ -71,10 +71,12 @@ class TestsTest : AbstractTest() { val output = runTask("evalTest", expectFailure = true).output.stripFilesAndLines() assertThat(output) - .contains( + .containsIgnoringNewLines( """ + > Task :evalTest FAILED module test (file:///file, line x) - test ❌ + should pass ✅ + error ❌ Error: –– Pkl Error –– exception @@ -82,11 +84,7 @@ class TestsTest : AbstractTest() { 9 | throw("exception") ^^^^^^^^^^^^^^^^^^ at test#facts["error"][#1] (file:///file, line x) - - 3 | facts { - ^^^^^^^ - at test#facts (file:///file, line x) - """ + """ .trimIndent() ) } @@ -122,7 +120,6 @@ class TestsTest : AbstractTest() { name = "Pigeon" age = 41 } - user 1 ❌ (file:///file, line x) Expected: (file:///file, line x) new { @@ -189,7 +186,7 @@ class TestsTest : AbstractTest() { .isEqualTo( """ - + @@ -209,8 +206,6 @@ class TestsTest : AbstractTest() { name = "Pigeon" age = 41 } - - (file:///file, line x) Expected: (file:///file, line x) new { @@ -226,7 +221,7 @@ class TestsTest : AbstractTest() { - + """ .trimIndent() )