From 03a7676e64aaecf04d8d3252b2e13ab063244050 Mon Sep 17 00:00:00 2001 From: Daniel Chao Date: Tue, 20 Jan 2026 21:41:33 -0800 Subject: [PATCH] Implement power assertions (#1384) This adds power assertions to Pkl! This implements the SPICE described in https://github.com/apple/pkl-evolution/pull/29 This follows the power assertions style of reporting also found in Groovy, Kotlin, and others. * Literal values are not emitted in the diagram * Stdlib constructors of literals like `List(1, 2)` are also considered literals Power assertions are added to: * Failing type constraints * Failing test facts Power assertions are implemented as a truffle instrument to observe execution. When an assertion fails, the instrument is created and the assertion is run again to observe facts. This incurs runtime overhead to collect facts, but has no impact on code in the non-error case. --------- Co-authored-by: Islon Scherer --- docs/modules/release-notes/pages/0.31.adoc | 84 ++- .../kotlin/org/pkl/cli/CliTestRunnerTest.kt | 61 ++- .../java/org/pkl/core/ast/ExpressionNode.java | 25 +- .../main/java/org/pkl/core/ast/PklNode.java | 8 +- .../org/pkl/core/ast/builder/AstBuilder.java | 2 +- .../ast/expression/binary/ComparatorNode.java | 7 +- .../expression/binary/GreaterThanNode.java | 10 +- .../ast/expression/binary/LessThanNode.java | 10 +- .../member/InvokeMethodVirtualNode.java | 11 +- .../core/ast/frame/WriteFrameSlotNode.java | 7 +- .../pkl/core/ast/internal/BlackholeNode.java | 7 +- .../pkl/core/ast/internal/GetClassNode.java | 10 +- .../pkl/core/ast/internal/SyntheticNode.java | 29 + .../core/ast/lambda/ApplyVmFunction1Node.java | 7 +- .../pkl/core/ast/type/TypeConstraintNode.java | 24 +- .../java/org/pkl/core/ast/type/TypeNode.java | 36 +- .../ast/type/VmTypeMismatchException.java | 150 +++--- .../java/org/pkl/core/repl/ReplServer.java | 3 +- .../java/org/pkl/core/runtime/PklTags.java | 27 + .../org/pkl/core/runtime/PowerAssertions.java | 508 ++++++++++++++++++ .../pkl/core/runtime/StackTraceGenerator.java | 2 +- .../pkl/core/runtime/StackTraceRenderer.java | 37 +- .../java/org/pkl/core/runtime/TestRunner.java | 32 +- .../org/pkl/core/runtime/VmBugException.java | 14 +- .../java/org/pkl/core/runtime/VmContext.java | 13 +- .../org/pkl/core/runtime/VmEvalException.java | 12 +- .../org/pkl/core/runtime/VmException.java | 37 +- .../pkl/core/runtime/VmExceptionBuilder.java | 41 +- .../pkl/core/runtime/VmExceptionRenderer.java | 24 +- .../java/org/pkl/core/runtime/VmFunction.java | 7 +- .../java/org/pkl/core/runtime/VmLanguage.java | 6 +- .../runtime/VmStackOverflowException.java | 3 +- .../runtime/VmUndefinedValueException.java | 23 +- .../org/pkl/core/runtime/VmValueTracker.java | 66 +++ .../core/runtime/VmValueTrackerFactory.java | 33 ++ .../core/runtime/VmWrappedEvalException.java | 12 +- .../pkl/core/stdlib/ExternalPropertyNode.java | 7 +- .../pkl/core/stdlib/base/CollectionNodes.java | 19 +- .../org/pkl/core/stdlib/base/ListNodes.java | 44 +- .../org/pkl/core/stdlib/base/MergeSort.java | 22 +- .../org/pkl/core/stdlib/base/SetNodes.java | 45 +- .../org/pkl/core/stdlib/test/TestNodes.java | 8 +- .../org/pkl/core/util/AnsiStringBuilder.java | 13 + .../java/org/pkl/core/util/AnsiTheme.java | 3 +- .../input/errors/power/typeConstraints1.pkl | 1 + .../input/errors/power/typeConstraints10.pkl | 3 + .../input/errors/power/typeConstraints11.pkl | 4 + .../input/errors/power/typeConstraints12.pkl | 1 + .../input/errors/power/typeConstraints13.pkl | 3 + .../input/errors/power/typeConstraints14.pkl | 9 + .../input/errors/power/typeConstraints15.pkl | 1 + .../input/errors/power/typeConstraints16.pkl | 10 + .../input/errors/power/typeConstraints17.pkl | 4 + .../input/errors/power/typeConstraints2.pkl | 7 + .../input/errors/power/typeConstraints3.pkl | 10 + .../input/errors/power/typeConstraints4.pkl | 3 + .../input/errors/power/typeConstraints5.pkl | 3 + .../input/errors/power/typeConstraints6.pkl | 5 + .../input/errors/power/typeConstraints7.pkl | 1 + .../input/errors/power/typeConstraints8.pkl | 1 + .../input/errors/power/typeConstraints9.pkl | 10 + .../input/errors/power/typeConstraints9a.pkl | 12 + .../input/projects/badProjectDeps4/PklProject | 7 +- .../output/classes/constraints5.err | 5 + .../classes/unionTypesErrorDifferent2.err | 4 + .../output/classes/wrongType6.err | 5 + .../output/errors/constraintDetails1.err | 7 + .../output/errors/constraintDetails2.err | 11 + .../output/errors/constraintDetails3.err | 5 + .../output/errors/intrinsifiedTypeAlias2.err | 4 + .../output/errors/intrinsifiedTypeAlias3.err | 4 + .../output/errors/invalidBytes1.err | 4 + .../output/errors/invalidBytes2.err | 4 + .../output/errors/invalidBytes3.err | 4 + .../output/errors/invalidBytes4.err | 4 + .../output/errors/listingTypeCheckError3.err | 5 + .../output/errors/listingTypeCheckError4.err | 4 + .../output/errors/mappingTypeCheckError6.err | 4 + .../output/errors/mappingTypeCheckError7.err | 4 + .../output/errors/power/typeConstraints1.err | 24 + .../output/errors/power/typeConstraints10.err | 26 + .../output/errors/power/typeConstraints11.err | 25 + .../output/errors/power/typeConstraints12.err | 24 + .../output/errors/power/typeConstraints13.err | 26 + .../output/errors/power/typeConstraints14.err | 23 + .../output/errors/power/typeConstraints15.err | 37 ++ .../output/errors/power/typeConstraints16.err | 24 + .../output/errors/power/typeConstraints17.err | 27 + .../output/errors/power/typeConstraints2.err | 24 + .../output/errors/power/typeConstraints3.err | 41 ++ .../output/errors/power/typeConstraints4.err | 23 + .../output/errors/power/typeConstraints5.err | 26 + .../output/errors/power/typeConstraints6.err | 32 ++ .../output/errors/power/typeConstraints7.err | 23 + .../output/errors/power/typeConstraints8.err | 23 + .../output/errors/power/typeConstraints9.err | 59 ++ .../output/errors/power/typeConstraints9a.err | 67 +++ .../output/modules/typedModuleProperties3.err | 5 + .../output/projects/badProjectDeps4/bug.err | 32 +- .../output/types/typeAliasContext.err | 4 + .../kotlin/org/pkl/core/EvaluateTestsTest.kt | 22 +- .../core/runtime/StackTraceRendererTest.kt | 4 +- .../test/kotlin/org/pkl/gradle/TestsTest.kt | 17 +- .../java/org/pkl/parser/GenericParser.java | 2 +- .../src/main/java/org/pkl/parser/Span.java | 11 +- .../src/main/java/org/pkl/parser/Token.java | 31 +- .../main/java/org/pkl/parser/syntax/Expr.java | 19 +- 107 files changed, 2133 insertions(+), 290 deletions(-) create mode 100644 pkl-core/src/main/java/org/pkl/core/ast/internal/SyntheticNode.java create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/PklTags.java create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/PowerAssertions.java create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/VmValueTracker.java create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/VmValueTrackerFactory.java create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints1.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints10.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints11.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints12.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints13.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints14.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints15.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints16.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints17.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints2.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints3.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints4.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints5.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints6.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints7.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints8.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9a.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints1.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints10.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints11.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints12.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints13.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints14.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints15.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints16.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints17.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints2.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints3.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints4.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints5.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints6.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints7.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints8.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9a.err diff --git a/docs/modules/release-notes/pages/0.31.adoc b/docs/modules/release-notes/pages/0.31.adoc index cb62260d..e40a361c 100644 --- a/docs/modules/release-notes/pages/0.31.adoc +++ b/docs/modules/release-notes/pages/0.31.adoc @@ -20,7 +20,89 @@ To get started, follow xref:pkl-cli:index.adoc#installation[Installation].# News you don't want to miss. -=== XXX +=== Power Assertions + +Pkl's test output and error output has been improved with power assertions (https://github.com/apple/pkl/pull/1384[#1384])! + +Pkl has two places that are effectively assertions. +These are: + +* Type constraint expressions +* Test facts + +Currently, when these assertions fail, the error message prints the assertion's source section. +However, this information can sometimes be lacking. +It tells you what the mechanics of the assertion is, but doesn't tell you _why_ the assertion is failing. + +For example, here is the current error output of a failing typecheck. + +[source,text] +---- +–– Pkl Error –– +Type constraint `name.endsWith(lastName)` violated. +Value: new Person { name = "Bub Johnson" } + +7 | passenger: Person(name.endsWith(lastName)) = new { name = "Bub Johnson" } +---- + +Just from this error message, we don't know what the last name is supposed to be. +What is `name` supposed to end with? +We just know it's some property called `lastName` but, we don't know what it _is_. + +In Pkl 0.31, the error output becomes: + +[source,text] +---- +–– Pkl Error –– +Type constraint `name.endsWith(lastName)` violated. +Value: new Person { name = "Bub Johnson" } + + name.endsWith(lastName) + │ │ │ + │ false "Smith" + "Bub Johnson" + +7 | passenger: Person(name.endsWith(lastName)) = new { name = "Bub Johnson" } +---- + +Now, we know what the expecation actually is. + +This type of diagram is also added to test facts. +When tests fail, Pkl emits a diagram of the expression, and the values produced. + +For example, given the following test: + +.math.pkl +[source,pkl] +---- +amends "pkl:test" + +facts { + local function add(a: Int, b: Int) = a * b + local function divide(a: Int, b: Int) = a % b + ["math"] { + add(3, 4) == 7 + divide(8, 2) == 4 + } +} +---- + +The error output now includes a power assertion diagram, which helps explain why the test failed. + +[source,text] +---- +module math + facts + ✘ math + add(3, 4) == 7 (math.pkl:9) + │ │ + 12 false + divide(8, 2) == 4 (math.pkl:10) + │ │ + 0 false + +0.0% tests pass [1/1 failed], 0.0% asserts pass [2/2 failed] +---- == Noteworthy [small]#🎶# 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 71998ae2..9287baa1 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ class CliTestRunnerTest { facts { ["fail"] { 4 == 9 - "foo" != "bar" + 1 == 5 } } """ @@ -101,8 +101,13 @@ class CliTestRunnerTest { facts ✘ fail 4 == 9 (/tempDir/test.pkl, line xx) + │ + false + 1 == 5 (/tempDir/test.pkl, line xx) + │ + false - 0.0% tests pass [1/1 failed], 50.0% asserts pass [1/2 failed] + 0.0% tests pass [1/1 failed], 0.0% asserts pass [2/2 failed] """ .trimIndent() @@ -283,12 +288,14 @@ class CliTestRunnerTest { - 5 == 9 (/tempDir/test.pkl, line xx) + 5 == 9 (/tempDir/test.pkl, line xx) + │ + false - + """ .trimIndent() ) @@ -481,26 +488,30 @@ class CliTestRunnerTest { assertThat(junitReport) .isEqualTo( """ - - - - - - 5 == 9 (/tempDir/test1.pkl, line xx) - - - - - false (/tempDir/test2.pkl, line xx) - - - false (/tempDir/test2.pkl, line xx) - - - - - - """ + + + + + + 5 == 9 (/tempDir/test1.pkl, line xx) + │ + false + + + + + false (/tempDir/test2.pkl, line xx) + + + + false (/tempDir/test2.pkl, line xx) + + + + + + + """ .trimIndent() ) } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/ExpressionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/ExpressionNode.java index 3df66c54..e9ff3dc4 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/ExpressionNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/ExpressionNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,18 @@ package org.pkl.core.ast; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.instrumentation.GenerateWrapper; +import com.oracle.truffle.api.instrumentation.InstrumentableNode; +import com.oracle.truffle.api.instrumentation.ProbeNode; +import com.oracle.truffle.api.instrumentation.Tag; import com.oracle.truffle.api.nodes.UnexpectedResultException; import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.PklTags; import org.pkl.core.runtime.VmTypesGen; import org.pkl.core.runtime.VmUtils; -public abstract class ExpressionNode extends PklNode { +@GenerateWrapper +public abstract class ExpressionNode extends PklNode implements InstrumentableNode { protected ExpressionNode(SourceSection sourceSection) { super(sourceSection); } @@ -43,4 +49,19 @@ public abstract class ExpressionNode extends PklNode { public boolean executeBoolean(VirtualFrame frame) throws UnexpectedResultException { return VmTypesGen.expectBoolean(executeGeneric(frame)); } + + @Override + public boolean hasTag(Class tag) { + return tag == PklTags.Expression.class; + } + + @Override + public boolean isInstrumentable() { + return true; + } + + @Override + public WrapperNode createWrapper(ProbeNode probe) { + return new ExpressionNodeWrapper(this, probe); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/PklNode.java b/pkl-core/src/main/java/org/pkl/core/ast/PklNode.java index 1eead635..0c96cf50 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/PklNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/PklNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.pkl.core.ast; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.TypeSystemReference; +import com.oracle.truffle.api.instrumentation.InstrumentableNode.WrapperNode; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.source.SourceSection; @@ -38,7 +39,10 @@ public abstract class PklNode extends Node { } @Override - public SourceSection getSourceSection() { + public final SourceSection getSourceSection() { + if (this instanceof WrapperNode wrapperNode) { + return wrapperNode.getDelegateNode().getSourceSection(); + } return sourceSection; } 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 0afce5b9..244f2870 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 @@ -2813,7 +2813,7 @@ public class AstBuilder extends AbstractAstBuilder { throw exceptionBuilder() .evalError(e.getMessage(), e.getMessageArguments()) .withCause(e.getCause()) - .withHint(e.getHint()) + .withHintBuilder(e.getHintBuilder()) .withSourceSection(createSourceSection(ctx)) .build(); } catch (ExternalReaderProcessException e) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ComparatorNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ComparatorNode.java index d288c601..ec376018 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ComparatorNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ComparatorNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ */ package org.pkl.core.ast.expression.binary; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.source.SourceSection; public abstract class ComparatorNode extends BinaryExpressionNode { - public ComparatorNode(SourceSection sourceSection) { + protected ComparatorNode(SourceSection sourceSection) { super(sourceSection); } - public abstract boolean executeWith(Object left, Object right); + public abstract boolean executeWith(VirtualFrame frame, Object left, Object right); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanNode.java index 6846511d..1f0ad8fa 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,15 @@ package org.pkl.core.ast.expression.binary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.instrumentation.GenerateWrapper; +import com.oracle.truffle.api.instrumentation.ProbeNode; import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.runtime.VmDataSize; import org.pkl.core.runtime.VmDuration; @NodeInfo(shortName = ">") +@GenerateWrapper public abstract class GreaterThanNode extends ComparatorNode { protected GreaterThanNode(SourceSection sourceSection) { super(sourceSection); @@ -63,4 +66,9 @@ public abstract class GreaterThanNode extends ComparatorNode { protected boolean eval(VmDataSize left, VmDataSize right) { return left.compareTo(right) > 0; } + + @Override + public WrapperNode createWrapper(ProbeNode probe) { + return new GreaterThanNodeWrapper(sourceSection, this, probe); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanNode.java index 2d6517d5..6690ebe9 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,15 @@ package org.pkl.core.ast.expression.binary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.instrumentation.GenerateWrapper; +import com.oracle.truffle.api.instrumentation.ProbeNode; import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.runtime.VmDataSize; import org.pkl.core.runtime.VmDuration; @NodeInfo(shortName = "<") +@GenerateWrapper public abstract class LessThanNode extends ComparatorNode { protected LessThanNode(SourceSection sourceSection) { super(sourceSection); @@ -63,4 +66,9 @@ public abstract class LessThanNode extends ComparatorNode { protected boolean eval(VmDataSize left, VmDataSize right) { return left.compareTo(right) < 0; } + + @Override + public WrapperNode createWrapper(ProbeNode probe) { + return new LessThanNodeWrapper(sourceSection, this, probe); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java index 79e1cdd6..9b7de94c 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,8 @@ import com.oracle.truffle.api.dsl.ImportStatic; import com.oracle.truffle.api.dsl.NodeChild; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.instrumentation.GenerateWrapper; +import com.oracle.truffle.api.instrumentation.ProbeNode; import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.IndirectCallNode; @@ -40,6 +42,7 @@ import org.pkl.core.runtime.VmFunction; @ImportStatic(Identifier.class) @NodeChild(value = "receiverNode", type = ExpressionNode.class) @NodeChild(value = "receiverClassNode", type = GetClassNode.class, executeWith = "receiverNode") +@GenerateWrapper public abstract class InvokeMethodVirtualNode extends ExpressionNode { protected final Identifier methodName; @Children private final ExpressionNode[] argumentNodes; @@ -173,6 +176,12 @@ public abstract class InvokeMethodVirtualNode extends ExpressionNode { .build(); } + @Override + public WrapperNode createWrapper(ProbeNode probe) { + return new InvokeMethodVirtualNodeWrapper( + sourceSection, methodName, argumentNodes, lookupMode, needsConst, this, probe); + } + private void checkConst(ClassMethod method) { if (needsConst && !method.isConst()) { CompilerDirectives.transferToInterpreter(); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteFrameSlotNode.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteFrameSlotNode.java index 9beaf0e7..5ccfb697 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteFrameSlotNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteFrameSlotNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,4 +78,9 @@ public abstract class WriteFrameSlotNode extends ExpressionNode { var kind = frame.getFrameDescriptor().getSlotKind(slot); return kind == FrameSlotKind.Boolean || kind == FrameSlotKind.Illegal; } + + @Override + public boolean isInstrumentable() { + return false; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/BlackholeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/BlackholeNode.java index ffdd9f1e..f4d17edc 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/internal/BlackholeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/BlackholeNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,4 +47,9 @@ public abstract class BlackholeNode extends ExpressionNode { CompilerDirectives.blackhole(value); return null; } + + @Override + public boolean isInstrumentable() { + return false; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/GetClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/GetClassNode.java index d7a1e34a..9d50409e 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/internal/GetClassNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/GetClassNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,13 @@ import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.NodeChild; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.instrumentation.GenerateWrapper; +import com.oracle.truffle.api.instrumentation.ProbeNode; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.runtime.*; +@GenerateWrapper // NOTE: needs to be kept in sync with VmUtils.getClass() @NodeChild(value = "valueNode", type = ExpressionNode.class) public abstract class GetClassNode extends ExpressionNode { @@ -76,4 +79,9 @@ public abstract class GetClassNode extends ExpressionNode { // - has a guard that triggers per-class respecialization return ((VmValue) value).getClass(); } + + @Override + public WrapperNode createWrapper(ProbeNode probe) { + return new GetClassNodeWrapper(this, probe); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/SyntheticNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/SyntheticNode.java new file mode 100644 index 00000000..eaa51f8c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/SyntheticNode.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.ast.internal; + +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.PklNode; + +/** + * A node that stands in for source sections that are intrinsified (e.g. `isBetween` in `typealias + * Int8 = Int(isBetween(..))`. + */ +public class SyntheticNode extends PklNode { + public SyntheticNode(SourceSection sourceSection) { + super(sourceSection); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction1Node.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction1Node.java index b30ccb12..9f74a64c 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction1Node.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction1Node.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,4 +86,9 @@ public abstract class ApplyVmFunction1Node extends ExpressionNode { return callNode.call(function.getCallTarget(), function.getThisValue(), function, arg1); } + + @Override + public boolean isInstrumentable() { + return false; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeConstraintNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeConstraintNode.java index 1692ee68..2f6a7b9b 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeConstraintNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeConstraintNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,11 +27,13 @@ import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.PklNode; import org.pkl.core.ast.lambda.ApplyVmFunction1Node; import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.VmContext; import org.pkl.core.runtime.VmFunction; import org.pkl.core.runtime.VmUtils; @NodeChild(value = "bodyNode", type = ExpressionNode.class) public abstract class TypeConstraintNode extends PklNode { + @CompilationFinal private int customThisSlot = -1; protected TypeConstraintNode(SourceSection sourceSection) { @@ -40,6 +42,8 @@ public abstract class TypeConstraintNode extends PklNode { public abstract void execute(VirtualFrame frame); + protected abstract ExpressionNode getBodyNode(); + public String export() { return getSourceSection().getCharacters().toString(); } @@ -49,8 +53,15 @@ public abstract class TypeConstraintNode extends PklNode { initConstraintSlot(frame); if (!result) { - throw new VmTypeMismatchException.Constraint( - sourceSection, frame.getAuxiliarySlot(customThisSlot)); + CompilerDirectives.transferToInterpreterAndInvalidate(); + try (var valueTracker = VmContext.get(this).getValueTrackerFactory().create()) { + getBodyNode().executeGeneric(frame); + throw new VmTypeMismatchException.Constraint( + sourceSection, + frame.getAuxiliarySlot(customThisSlot), + sourceSection, + valueTracker.values()); + } } } @@ -64,7 +75,12 @@ public abstract class TypeConstraintNode extends PklNode { var value = frame.getAuxiliarySlot(customThisSlot); var result = applyNode.executeBoolean(function, value); if (!result) { - throw new VmTypeMismatchException.Constraint(sourceSection, value); + CompilerDirectives.transferToInterpreterAndInvalidate(); + try (var valueTracker = VmContext.get(this).getValueTrackerFactory().create()) { + applyNode.executeBoolean(function, value); + throw new VmTypeMismatchException.Constraint( + sourceSection, value, function.getRootNode().getSourceSection(), valueTracker.values()); + } } } 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 74866b46..8bf98611 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 @@ -29,6 +29,8 @@ import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.LoopNode; import com.oracle.truffle.api.source.SourceSection; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.pkl.core.PType; @@ -40,6 +42,7 @@ import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; 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.internal.SyntheticNode; import org.pkl.core.ast.member.DefaultPropertyBodyNode; import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ObjectMember; @@ -143,6 +146,14 @@ public abstract class TypeNode extends PklNode { /** Visit child type nodes of this type. */ protected abstract boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer); + protected VmTypeMismatchException constraintException(Object value, SourceSection sourceSection) { + throw new VmTypeMismatchException.Constraint( + sourceSection, + value, + sourceSection, + Map.of(new SyntheticNode(sourceSection), List.of(false))); + } + public static TypeNode forClass(SourceSection sourceSection, VmClass clazz) { return clazz.isClosed() ? new FinalClassTypeNode(sourceSection, clazz) @@ -1719,9 +1730,9 @@ public abstract class TypeNode extends PklNode { //noinspection ConstantConditions defaultMember.initConstantValue( new VmFunction( - VmUtils.createEmptyMaterializedFrame(), // Assumption: don't need to set the correct `thisValue` // because it is guaranteed to be never accessed. + VmUtils.createEmptyMaterializedFrame(), null, 1, new SimpleRootNode( @@ -2232,8 +2243,8 @@ public abstract class TypeNode extends PklNode { @Override protected Object executeLazily(VirtualFrame frame, Object value) { if (value instanceof VmNull) { - throw new VmTypeMismatchException.Constraint( - BaseModule.getNonNullTypeAlias().getConstraintSection(), value); + CompilerDirectives.transferToInterpreterAndInvalidate(); + throw constraintException(value, BaseModule.getNonNullTypeAlias().getConstraintSection()); } return value; } @@ -2274,7 +2285,9 @@ public abstract class TypeNode extends PklNode { if (value instanceof Long l) { if ((l & mask) == l) return value; - throw new VmTypeMismatchException.Constraint(typealias.getConstraintSection(), value); + CompilerDirectives.transferToInterpreterAndInvalidate(); + var sourceSection = typealias.getConstraintSection(); + throw constraintException(value, sourceSection); } throw new VmTypeMismatchException.Simple( @@ -2337,8 +2350,9 @@ public abstract class TypeNode extends PklNode { if (value instanceof Long l) { if (l == l.byteValue()) return value; - throw new VmTypeMismatchException.Constraint( - BaseModule.getInt8TypeAlias().getConstraintSection(), value); + CompilerDirectives.transferToInterpreterAndInvalidate(); + var sourceSection = BaseModule.getInt8TypeAlias().getConstraintSection(); + throw constraintException(value, sourceSection); } throw new VmTypeMismatchException.Simple( @@ -2381,8 +2395,9 @@ public abstract class TypeNode extends PklNode { if (value instanceof Long l) { if (l == l.shortValue()) return value; - throw new VmTypeMismatchException.Constraint( - BaseModule.getInt16TypeAlias().getConstraintSection(), value); + CompilerDirectives.transferToInterpreterAndInvalidate(); + var sourceSection = BaseModule.getInt16TypeAlias().getConstraintSection(); + throw constraintException(value, sourceSection); } throw new VmTypeMismatchException.Simple( @@ -2425,10 +2440,11 @@ public abstract class TypeNode extends PklNode { if (value instanceof Long l) { if (l == l.intValue()) return value; - throw new VmTypeMismatchException.Constraint( - BaseModule.getInt32TypeAlias().getConstraintSection(), value); + CompilerDirectives.transferToInterpreterAndInvalidate(); + throw constraintException(value, BaseModule.getInt32TypeAlias().getConstraintSection()); } + CompilerDirectives.transferToInterpreterAndInvalidate(); throw new VmTypeMismatchException.Simple( BaseModule.getInt32TypeAlias().getBaseTypeSection(), value, BaseModule.getIntClass()); } 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 7422dd38..b9182acd 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.pkl.core.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.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.*; import java.util.stream.Collectors; @@ -26,6 +27,7 @@ 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.AnsiStringBuilder; import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.Nullable; @@ -36,6 +38,7 @@ import org.pkl.core.util.Nullable; * there aren't any.) */ public abstract class VmTypeMismatchException extends ControlFlowException { + protected final SourceSection sourceSection; protected final Object actualValue; protected @Nullable Map insertedStackFrames = null; @@ -54,20 +57,36 @@ public abstract class VmTypeMismatchException extends ControlFlowException { } @TruffleBoundary - public abstract void describe(StringBuilder builder, String indent); + public abstract void buildMessage( + AnsiStringBuilder builder, String indent, boolean withPowerAssertions); @TruffleBoundary - public abstract VmException toVmException(); + public void buildHint(AnsiStringBuilder builder, String indent, boolean withPowerAssertions) {} - protected VmExceptionBuilder exceptionBuilder() { - var builder = new VmExceptionBuilder(); + @TruffleBoundary + public final VmException toVmException() { + return exceptionBuilder().build(); + } + + protected final VmExceptionBuilder exceptionBuilder() { + var builder = + new VmExceptionBuilder() + .withSourceSection(sourceSection) + .withMessageBuilder( + (b, withPowerAssertions) -> buildMessage(b, "", withPowerAssertions)); + if (hasHint()) { + builder.withHintBuilder((b, withPowerAssertions) -> buildHint(b, "", withPowerAssertions)); + } if (insertedStackFrames != null) { builder.withInsertedStackFrames(insertedStackFrames); } return builder; } + protected abstract Boolean hasHint(); + public static final class Simple extends VmTypeMismatchException { + private final Object expectedType; public Simple(SourceSection sourceSection, Object actualValue, Object expectedType) { @@ -83,7 +102,8 @@ public abstract class VmTypeMismatchException extends ControlFlowException { @Override @TruffleBoundary - public void describe(StringBuilder builder, String indent) { + public void buildMessage( + AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { String renderedType; var valueFormatter = ValueFormatter.basic(); if (expectedType instanceof String string) { @@ -143,54 +163,48 @@ public abstract class VmTypeMismatchException extends ControlFlowException { } @Override - @TruffleBoundary - public VmException toVmException() { - return exceptionBuilder().build(); - } - - @Override - protected VmExceptionBuilder exceptionBuilder() { - var builder = new StringBuilder(); - describe(builder, ""); - - return super.exceptionBuilder() - .adhocEvalError(builder.toString()) - .withSourceSection(sourceSection); + protected Boolean hasHint() { + return false; } } public static final class Constraint extends VmTypeMismatchException { - public Constraint(SourceSection sourceSection, Object actualValue) { + + private final SourceSection constraintBodySourceSection; + private final Map> trackedValues; + + public Constraint( + SourceSection sourceSection, + Object actualValue, + SourceSection constraintBodySourceSection, + Map> trackedValues) { super(sourceSection, actualValue); + this.constraintBodySourceSection = constraintBodySourceSection; + this.trackedValues = trackedValues; } @Override @TruffleBoundary - public void describe(StringBuilder builder, String indent) { + public void buildMessage( + AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { + var expression = sourceSection.getCharacters().toString(); builder - .append( - ErrorMessages.createIndented( - "typeConstraintViolated", indent, sourceSection.getCharacters().toString())) + .append(ErrorMessages.createIndented("typeConstraintViolated", indent, expression)) .append("\n") .append(indent) .append("Value: ") .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); + if (!withPowerAssertions) { + return; + } + builder.append("\n\n"); + PowerAssertions.render( + builder, indent + " ", constraintBodySourceSection, trackedValues, null); } @Override - @TruffleBoundary - public VmException toVmException() { - return exceptionBuilder().build(); - } - - @Override - protected VmExceptionBuilder exceptionBuilder() { - var builder = new StringBuilder(); - describe(builder, ""); - - return super.exceptionBuilder() - .adhocEvalError(builder.toString()) - .withSourceSection(sourceSection); + protected Boolean hasHint() { + return false; } } @@ -209,33 +223,15 @@ public abstract class VmTypeMismatchException extends ControlFlowException { } @Override - @TruffleBoundary - public void describe(StringBuilder builder, String indent) { - describeSummary(builder, indent); - describeDetails(builder, indent); + protected Boolean hasHint() { + var nonTrivialMatches = findNonTrivialMismatches(); + return !nonTrivialMatches.isEmpty(); } @Override @TruffleBoundary - public VmException toVmException() { - return exceptionBuilder().build(); - } - - @Override - protected VmExceptionBuilder exceptionBuilder() { - var summary = new StringBuilder(); - describeSummary(summary, ""); - - var details = new StringBuilder(); - describeDetails(details, ""); - - return super.exceptionBuilder() - .adhocEvalError(summary.toString()) - .withSourceSection(sourceSection) - .withHint(details.toString()); - } - - private void describeSummary(StringBuilder builder, String indent) { + public void buildMessage( + AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { var nonTrivialMismatches = findNonTrivialMismatches(); if (nonTrivialMismatches.isEmpty()) { @@ -267,12 +263,16 @@ public abstract class VmTypeMismatchException extends ControlFlowException { .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); } - private void describeDetails(StringBuilder builder, String indent) { + @Override + @TruffleBoundary + public void buildHint(AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { var nonTrivialMismatches = findNonTrivialMismatches(); var isPeerError = false; for (var idx : nonTrivialMismatches) { - if (!indent.isEmpty() || isPeerError) builder.append("\n\n"); + if (isPeerError) { + builder.append("\n\n"); + } isPeerError = true; builder .append( @@ -285,7 +285,12 @@ public abstract class VmTypeMismatchException extends ControlFlowException { .getCharacters() .toString())) .append("\n"); - children[idx].describe(builder, indent + " "); + var child = children[idx]; + child.buildMessage(builder, indent + " ", withPowerAssertions); + if (child.hasHint()) { + builder.append("\n\n"); + child.buildHint(builder, indent + " ", withPowerAssertions); + } } } @@ -304,13 +309,15 @@ public abstract class VmTypeMismatchException extends ControlFlowException { } public static final class Nothing extends VmTypeMismatchException { + public Nothing(SourceSection sourceSection, Object actualValue) { super(sourceSection, actualValue); } @Override @TruffleBoundary - public void describe(StringBuilder builder, String indent) { + public void buildMessage( + AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { builder .append( ErrorMessages.createIndented( @@ -322,19 +329,8 @@ public abstract class VmTypeMismatchException extends ControlFlowException { } @Override - @TruffleBoundary - public VmException toVmException() { - return exceptionBuilder().build(); - } - - @Override - protected VmExceptionBuilder exceptionBuilder() { - var builder = new StringBuilder(); - describe(builder, ""); - - return super.exceptionBuilder() - .adhocEvalError(builder.toString()) - .withSourceSection(sourceSection); + protected Boolean hasHint() { + return false; } } } diff --git a/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java index d62a9cea..8e000924 100644 --- a/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java +++ b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java @@ -92,7 +92,8 @@ public class ReplServer implements AutoCloseable { this.workingDir = workingDir; this.securityManager = securityManager; this.moduleResolver = new ModuleResolver(moduleKeyFactories); - this.errorRenderer = new VmExceptionRenderer(new StackTraceRenderer(frameTransformer), color); + this.errorRenderer = + new VmExceptionRenderer(new StackTraceRenderer(frameTransformer), color, true); this.color = color; replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype())); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/PklTags.java b/pkl-core/src/main/java/org/pkl/core/runtime/PklTags.java new file mode 100644 index 00000000..4f33f1b5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/PklTags.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.runtime; + +import com.oracle.truffle.api.instrumentation.Tag; + +public final class PklTags { + private PklTags() {} + + @Tag.Identifier("EXPRESSION") + public static final class Expression extends Tag { + private Expression() {} + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/PowerAssertions.java b/pkl-core/src/main/java/org/pkl/core/runtime/PowerAssertions.java new file mode 100644 index 00000000..a6901f87 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/PowerAssertions.java @@ -0,0 +1,508 @@ +/* + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.runtime; + +import com.oracle.truffle.api.instrumentation.InstrumentableNode.WrapperNode; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.pkl.core.ast.ConstantValueNode; +import org.pkl.core.ast.expression.member.InferParentWithinMethodNode; +import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNode; +import org.pkl.core.ast.expression.member.InferParentWithinPropertyNode; +import org.pkl.core.ast.expression.member.InvokeMethodDirectNode; +import org.pkl.core.ast.type.GetParentForTypeNode; +import org.pkl.core.util.AnsiStringBuilder; +import org.pkl.core.util.AnsiStringBuilder.AnsiCode; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.SyntaxHighlighter; +import org.pkl.parser.Lexer; +import org.pkl.parser.Parser; +import org.pkl.parser.Token; +import org.pkl.parser.syntax.Expr; +import org.pkl.parser.syntax.Expr.BinaryOperatorExpr; +import org.pkl.parser.syntax.Expr.BoolLiteralExpr; +import org.pkl.parser.syntax.Expr.FloatLiteralExpr; +import org.pkl.parser.syntax.Expr.FunctionLiteralExpr; +import org.pkl.parser.syntax.Expr.IntLiteralExpr; +import org.pkl.parser.syntax.Expr.LetExpr; +import org.pkl.parser.syntax.Expr.NullLiteralExpr; +import org.pkl.parser.syntax.Expr.QualifiedAccessExpr; +import org.pkl.parser.syntax.Expr.StringLiteralExpr; +import org.pkl.parser.syntax.Expr.SubscriptExpr; +import org.pkl.parser.syntax.Expr.SuperAccessExpr; +import org.pkl.parser.syntax.Expr.SuperSubscriptExpr; +import org.pkl.parser.syntax.Expr.TypeCheckExpr; +import org.pkl.parser.syntax.Expr.UnqualifiedAccessExpr; +import org.pkl.parser.syntax.ObjectMember.ForGenerator; +import org.pkl.parser.syntax.ObjectMember.MemberPredicate; +import org.pkl.parser.syntax.StringPart.StringChars; +import org.pkl.parser.syntax.StringPart.StringInterpolation; + +public class PowerAssertions { + private PowerAssertions() {} + + private static final VmValueRenderer vmValueRenderer = VmValueRenderer.singleLine(100); + private static final Parser parser = new Parser(); + + public static void render( + AnsiStringBuilder out, + String indent, + SourceSection sourceSection, + Map> trackedValues, + @Nullable Consumer firstFrameSuffix) { + out.appendSandboxed( + () -> { + var lines = lines(sourceSection); + var layerEntries = getLayerEntries(trackedValues, sourceSection); + var indentation = + lines.size() == 1 + ? 0 + : Collections.min( + lines.stream() + .skip(1) + .map((it) -> leadingWhitespace(it.getCharacters())) + .toList()); + var sourceLines = lines(sourceSection); + var renderedMarkers = false; + for (var i = 0; i < sourceLines.size(); i++) { + if (renderedMarkers) { + out.append("\n\n"); + } + var line = sourceLines.get(i); + var offset = i == 0 ? line.getStartColumn() - 1 : indentation; + renderedMarkers = + renderLine( + out, indent, line, layerEntries, offset, i == 0 ? firstFrameSuffix : null); + } + }); + } + + private static boolean isInForGeneratorOrLambdaOrPredicate( + org.pkl.parser.syntax.Node myNode, Expr rootExpr) { + var parent = myNode.parent(); + while (parent != null) { + if (parent instanceof FunctionLiteralExpr) { + // okay to show power assert if this lambda is the root constraint expr (this was a lambda + // passed in as a a constraint) + return !parent.equals(rootExpr); + } + if (parent instanceof MemberPredicate) { + return true; + } + // okay if it's in expr section of for generator + if (parent instanceof ForGenerator forGenerator) { + return !forGenerator.getExpr().span().contains(myNode.span()); + } + parent = parent.parent(); + } + return false; + } + + private static boolean isLiteral(org.pkl.parser.syntax.Node parserNode) { + if (parserNode instanceof IntLiteralExpr + || parserNode instanceof FloatLiteralExpr + || parserNode instanceof BoolLiteralExpr + || parserNode instanceof NullLiteralExpr) { + return true; + } + if (parserNode instanceof StringLiteralExpr stringLiteralExpr) { + return !stringLiteralExpr.hasInterpolation(); + } + return false; + } + + // tells if this method is call to a literal value + // treats method calls like `List(1, 2, 3)` as literal values. + private static boolean isLiteral(Node truffleNode, org.pkl.parser.syntax.Node parserNode) { + if (isLiteral(parserNode)) { + return true; + } + if (truffleNode instanceof ConstantValueNode) { + if (parserNode instanceof UnqualifiedAccessExpr unqualifiedAccessExpr) { + // Assumption: if we have both ConstantValueNode, and the parser node is + // UnqualifiedAccessExpr with arguments, then this must be a `List()`, `Map()`, etc. + // + // Note: reading a local property whose value is a constant will also turn into a + // ConstantValueNode. + return unqualifiedAccessExpr.getArgumentList() != null; + } + return true; + } + // assumption: only calls to methods within the base module are direct method calls. + if (!(truffleNode instanceof InvokeMethodDirectNode)) { + return false; + } + var accessExpr = (UnqualifiedAccessExpr) parserNode; + // assumption: "literal" values are method calls that are uppercased. e.g. IntSeq(..), Pair(..) + if (!Character.isUpperCase(accessExpr.getIdentifier().getValue().charAt(0))) { + return false; + } + var argsList = accessExpr.getArgumentList(); + assert argsList != null; + return argsList.getArguments().stream().allMatch(PowerAssertions::isLiteral); + } + + private static boolean shouldHide( + Node truffleNode, org.pkl.parser.syntax.Node parserNode, Object value, Expr rootExpr) { + // literal values are self-evident in their source code + if (isLiteral(truffleNode, parserNode)) { + return true; + } + if ( + // let expressions will already show the resolved value in the expression body; showing the + // let as well simply adds noise. + parserNode instanceof LetExpr + // we'll already show the expression within string interpolation + || parserNode instanceof StringInterpolation + || parserNode instanceof StringChars) { + return true; + } + if (isInForGeneratorOrLambdaOrPredicate(parserNode, rootExpr)) { + return true; + } + if (value instanceof VmFunction) { + return true; + } + // implicit behavior around `new` + return truffleNode instanceof GetParentForTypeNode + || truffleNode instanceof InferParentWithinMethodNode + || truffleNode instanceof InferParentWithinObjectMethodNode + || truffleNode instanceof InferParentWithinPropertyNode; + } + + // tries to find the parser node for this node + private static @Nullable org.pkl.parser.syntax.Node findParserNode( + Node node, @Nullable org.pkl.parser.syntax.Node parserNode, int offset) { + if (!node.getSourceSection().isAvailable()) { + return null; + } + if (parserNode == null) { + return null; + } + var span = parserNode.span(); + var charIndex = span.charIndex() + offset; + var ss = node.getSourceSection(); + if (charIndex == ss.getCharIndex() && span.length() == ss.getCharLength()) { + return parserNode; + } + var children = parserNode.children(); + if (children == null) { + return null; + } + for (var child : children) { + var found = findParserNode(node, child, offset); + if (found != null) { + return found; + } + } + return null; + } + + private static Collection getLayerEntries( + Map> trackedValues, SourceSection sourceSection) { + var exprNode = parser.parseExpressionInput(sourceSection.getCharacters().toString()); + // it's possible that two nodes can turn into identical `SourceEntry`s; ensure these entries are + // distinct by using a set. + var ret = new LinkedHashSet(); + for (var entry : trackedValues.entrySet()) { + var truffleNode = entry.getKey(); + var values = entry.getValue(); + var parserNode = findParserNode(truffleNode, exprNode, sourceSection.getCharIndex()); + if (parserNode == null) { + continue; + } + // this node was executed multiple times; not sure how to best show this in a power assert + // graph + if (values.size() > 1) { + continue; + } + var value = values.get(0); + if (shouldHide(truffleNode, parserNode, value, exprNode)) { + continue; + } + ret.add(SourceEntry.create(truffleNode, parserNode, value)); + } + return ret; + } + + private static boolean canFit(Deque layer, LayerEntry elem) { + if (layer.isEmpty()) { + return true; + } + var nextEntry = layer.getFirst(); + return elem.startColumn() + elem.length() + 1 < nextEntry.startColumn(); + } + + private static boolean canFitMarker(Deque layer, LayerEntry elem) { + if (layer.isEmpty()) { + return true; + } + var nextEntry = layer.getFirst(); + return elem.startColumn() < nextEntry.startColumn(); + } + + private static List> buildLayers( + Collection layerEntries, SourceSection line) { + var sortedSections = + layerEntries.stream() + .filter((it) -> it.startLine() == line.getStartLine()) + .sorted(Comparator.comparingInt((it) -> -it.startColumn())) + .collect(Collectors.toCollection(LinkedList::new)); + if (sortedSections.isEmpty()) { + return Collections.emptyList(); + } + + var layers = new ArrayList>(); + + // first layer is all markers + layers.add(firstLayerMarkers(sortedSections)); + + while (!sortedSections.isEmpty()) { + var layer = new ArrayDeque(); + var iter = sortedSections.iterator(); + while (iter.hasNext()) { + var next = iter.next(); + if (canFit(layer, next)) { + layer.addFirst(next); + iter.remove(); + } else if (canFitMarker(layer, next)) { + layer.addFirst(next.toMarker()); + } + } + layers.add(layer); + } + return layers; + } + + private static Collection firstLayerMarkers(List entries) { + var layer = new ArrayDeque(); + LayerEntry prevEntry = null; + for (var entry : entries) { + if (prevEntry != null && prevEntry.startColumn() == entry.startColumn()) { + continue; + } + layer.addFirst(entry.toMarker()); + prevEntry = entry; + } + return layer; + } + + private static List lines(SourceSection sourceSection) { + var startLine = sourceSection.getStartLine(); + var endLine = sourceSection.getEndLine(); + if (!sourceSection.isAvailable() || startLine == endLine) { + return Collections.singletonList(sourceSection); + } + + var result = new ArrayList(); + var source = sourceSection.getSource(); + var charIndex = sourceSection.getCharIndex(); + var endCharIndex = charIndex + sourceSection.getCharLength(); + + for (var lineNumber = startLine; lineNumber <= endLine; lineNumber++) { + var lineSection = source.createSection(lineNumber); + var lineStartChar = lineSection.getCharIndex(); + var lineEndChar = lineStartChar + lineSection.getCharLength(); + + var sectionStartChar = Math.max(charIndex, lineStartChar); + var sectionEndChar = Math.min(endCharIndex, lineEndChar); + + if (sectionStartChar < sectionEndChar) { + result.add(source.createSection(sectionStartChar, sectionEndChar - sectionStartChar)); + } + } + + return result; + } + + public static String trimLeadingWhitespace(String str, int n) { + var i = 0; + + while (i < str.length() && i < n && Character.isWhitespace(str.charAt(i))) { + i++; + } + + return str.substring(i); + } + + private static boolean renderLine( + AnsiStringBuilder out, + String indent, + SourceSection line, + Collection layerEntries, + int trimStart, + @Nullable Consumer lineSuffix) { + var layers = buildLayers(layerEntries, line); + var content = trimLeadingWhitespace(line.getCharacters().toString(), trimStart); + out.append(indent).append(content); + if (lineSuffix != null) { + lineSuffix.accept(out); + } + out.append('\n'); + if (layers.isEmpty()) { + return false; + } + for (var i = 0; i < layers.size(); i++) { + var layer = layers.get(i); + out.append(indent); + var cursor = 0; + for (var entry : layer) { + var currentOffset = entry.startColumn() - 1 - trimStart - cursor; + out.append(" ".repeat(currentOffset)); + entry.appendTo(out); + cursor += currentOffset + entry.length(); + } + if (i < layers.size() - 1) { + out.append('\n'); + } + } + return true; + } + + private static int leadingWhitespace(CharSequence src) { + var result = 0; + for (var i = 0; i < src.length(); i++) { + var c = src.charAt(i); + if (c != ' ' && c != '\t') { + break; + } + result++; + } + return result; + } + + private sealed interface LayerEntry permits SourceEntry, MarkerEntry { + + /** 1-based (because {@link SourceSection}'s is too). */ + int startLine(); + + /** 1-based (because {@link SourceSection}'s is too). */ + int startColumn(); + + int length(); + + void appendTo(AnsiStringBuilder builder); + + default MarkerEntry toMarker() { + return new MarkerEntry(startLine(), startColumn()); + } + } + + private record SourceEntry(int startLine, int startColumn, String src) implements LayerEntry { + static SourceEntry create( + Node truffleNode, org.pkl.parser.syntax.Node parserNode, Object value) { + var effectiveSourceSection = + truffleNode + .getSourceSection() + .getSource() + .createSection(getCharIndex(truffleNode, parserNode), 1); + return new SourceEntry( + effectiveSourceSection.getStartLine(), + effectiveSourceSection.getStartColumn(), + vmValueRenderer.render(value)); + } + + // create a subsection that points to the operator + // + // 1 + 1 + // | + // + // or in the case of qualified access, the very next character after qualified access + // + // foo.bar + // | + private static int getCharIndex(Node truffleNode, org.pkl.parser.syntax.Node parserNode) { + if (truffleNode instanceof WrapperNode wrapperNode) { + truffleNode = wrapperNode.getDelegateNode(); + } + var originalSource = truffleNode.getSourceSection(); + if (!originalSource.isAvailable()) { + return originalSource.getCharIndex(); + } + var exprText = originalSource.getCharacters().toString(); + var skip = 0; + if (parserNode instanceof BinaryOperatorExpr binaryOperatorExpr) { + skip = + binaryOperatorExpr.getLeft().span().stopIndexExclusive() + - parserNode.span().charIndex(); + } else if (parserNode instanceof TypeCheckExpr typeCheckExpr) { + skip = typeCheckExpr.getExpr().span().stopIndexExclusive() - parserNode.span().charIndex(); + } else if (parserNode instanceof SubscriptExpr subscriptExpr) { + skip = subscriptExpr.getExpr().span().stopIndexExclusive() - parserNode.span().charIndex(); + } else if (parserNode instanceof SuperAccessExpr + || parserNode instanceof SuperSubscriptExpr) { + skip = "super".length(); + } else if (parserNode instanceof QualifiedAccessExpr qualifiedAccessExpr) { + skip = + qualifiedAccessExpr.getExpr().span().stopIndexExclusive() + - parserNode.span().charIndex(); + } + if (skip == 0) { + return originalSource.getCharIndex(); + } + // need to lex the source again because the parse tree doesn't carry the operator's position + var lexer = new Lexer(exprText.substring(skip)); + var nextToken = lexer.next(); + while (!nextToken.isOperator()) { + nextToken = lexer.next(); + } + var span = lexer.span(); + if (nextToken == Token.DOT || nextToken == Token.QDOT) { + return originalSource.getCharIndex() + skip + span.charIndex() + span.length(); + } + return originalSource.getCharIndex() + skip + span.charIndex(); + } + + @Override + public int length() { + return src.length(); + } + + @Override + public void appendTo(AnsiStringBuilder builder) { + SyntaxHighlighter.writeTo(builder, src); + } + } + + private record MarkerEntry(int startLine, int startColumn) implements LayerEntry { + @Override + public int length() { + return 1; + } + + @Override + public void appendTo(AnsiStringBuilder builder) { + builder.append(AnsiCode.FAINT, "│"); + } + + @Override + public MarkerEntry toMarker() { + return this; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceGenerator.java b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceGenerator.java index 27a3c732..9e6776c2 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceGenerator.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java index d328cf08..d88a1ad6 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java @@ -17,6 +17,7 @@ package org.pkl.core.runtime; import java.util.ArrayList; import java.util.List; +import java.util.function.BiConsumer; import java.util.function.Function; import org.pkl.core.StackFrame; import org.pkl.core.util.AnsiStringBuilder; @@ -31,15 +32,20 @@ public final class StackTraceRenderer { this.frameTransformer = frameTransformer; } - public void render(List frames, @Nullable String hint, AnsiStringBuilder out) { + public void render( + List frames, + @Nullable String hint, + @Nullable BiConsumer hintBuilder, + AnsiStringBuilder out) { var compressed = compressFrames(frames); - doRender(compressed, hint, out, "", true); + doRender(compressed, hint, hintBuilder, out, "", true); } // non-private for testing void doRender( List frames, @Nullable String hint, + @Nullable BiConsumer hintBuilder, AnsiStringBuilder out, String leftMargin, boolean isFirstElement) { @@ -47,7 +53,7 @@ public final class StackTraceRenderer { if (frame instanceof StackFrameLoop loop) { // ensure a cycle of length 1 doesn't get rendered as a loop if (loop.count == 1) { - doRender(loop.frames, null, out, leftMargin, isFirstElement); + doRender(loop.frames, null, null, out, leftMargin, isFirstElement); } else { if (!isFirstElement) { out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin).append('\n'); @@ -57,9 +63,9 @@ public final class StackTraceRenderer { .append(AnsiTheme.STACK_TRACE_LOOP_COUNT, loop.count) .append(" repetitions of:\n"); var newLeftMargin = leftMargin + "│ "; - doRender(loop.frames, null, out, newLeftMargin, isFirstElement); + doRender(loop.frames, null, null, out, newLeftMargin, isFirstElement); if (isFirstElement) { - renderHint(hint, out, newLeftMargin); + renderHint(hint, hintBuilder, out, newLeftMargin); isFirstElement = false; } out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin + "└─").append('\n'); @@ -72,7 +78,7 @@ public final class StackTraceRenderer { } if (isFirstElement) { - renderHint(hint, out, leftMargin); + renderHint(hint, hintBuilder, out, leftMargin); isFirstElement = false; } } @@ -84,13 +90,20 @@ public final class StackTraceRenderer { renderSourceLocation(transformed, out, leftMargin); } - private void renderHint(@Nullable String hint, AnsiStringBuilder out, String leftMargin) { - if (hint == null || hint.isEmpty()) return; + private void renderHint( + @Nullable String hint, + @Nullable BiConsumer hintBuilder, + AnsiStringBuilder out, + String leftMargin) { + if (hint == null && hintBuilder == null) return; - out.append('\n') - .append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin) - .append(AnsiTheme.ERROR_MESSAGE_HINT, hint) - .append('\n'); + out.append('\n').append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin); + if (hint != null) { + out.append(AnsiTheme.ERROR_MESSAGE_HINT, hint); + } else { + out.append(AnsiTheme.ERROR_MESSAGE_HINT, () -> hintBuilder.accept(out, true)); + } + out.append('\n'); } private void renderSourceLine(StackFrame frame, AnsiStringBuilder out, String leftMargin) { 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 5364b77c..e416f088 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package org.pkl.core.runtime; +import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -23,6 +24,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.pkl.core.BufferedLogger; import org.pkl.core.StackFrameTransformer; import org.pkl.core.TestResults; @@ -47,6 +49,7 @@ public final class TestRunner { private final StackFrameTransformer stackFrameTransformer; private final boolean overwrite; private final boolean useColor; + private final VmValueTrackerFactory valueTrackerFactory; public TestRunner( BufferedLogger logger, @@ -57,6 +60,7 @@ public final class TestRunner { this.stackFrameTransformer = stackFrameTransformer; this.overwrite = overwrite; this.useColor = useColor; + this.valueTrackerFactory = VmContext.get(null).getValueTrackerFactory(); } public TestResults run(VmTyped testModule) { @@ -108,8 +112,16 @@ public final class TestRunner { try { var factValue = VmUtils.readMember(listing, idx); if (factValue == Boolean.FALSE) { - var failure = factFailure(member.getSourceSection(), getDisplayUri(member)); - resultBuilder.addFailure(failure); + try (var valueTracker = valueTrackerFactory.create()) { + listing.cachedValues.clear(); + VmUtils.readMember(listing, idx); + var failure = + factFailure( + member.getSourceSection(), + getDisplayUri(member), + valueTracker.values()); + resultBuilder.addFailure(failure); + } } else { resultBuilder.addSuccess(); } @@ -395,10 +407,18 @@ public final class TestRunner { moduleInfo.getModuleKey().getUri(), VmContext.get(null).getFrameTransformer()); } - private Failure factFailure(SourceSection sourceSection, String location) { + private Failure factFailure( + SourceSection sourceSection, String location, Map> trackedValues) { var sb = new AnsiStringBuilder(useColor); - sb.append(AnsiTheme.TEST_FACT_SOURCE, sourceSection.getCharacters().toString()).append(" "); - appendLocation(sb, location); + PowerAssertions.render( + sb, + "", + sourceSection, + trackedValues, + (it) -> { + it.append(" "); + appendLocation(it, location); + }); return new Failure("Fact Failure", sb.toString()); } 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 4c4f12d6..6e7d8a5f 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,20 +21,23 @@ import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import org.pkl.core.*; +import org.pkl.core.util.AnsiStringBuilder; import org.pkl.core.util.Nullable; public final class VmBugException extends VmException { public VmBugException( - String message, + @Nullable String message, @Nullable Throwable cause, boolean isExternalMessage, Object[] messageArguments, + @Nullable BiConsumer messageBuilder, List programValues, @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint, + @Nullable BiConsumer hintBuilder, Map insertedStackFrames) { super( @@ -42,11 +45,12 @@ public final class VmBugException extends VmException { cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, + hintBuilder, insertedStackFrames); } @@ -59,7 +63,7 @@ public final class VmBugException extends VmException { @Override @TruffleBoundary public PklException toPklException(StackFrameTransformer transformer, boolean color) { - var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color); + var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color, true); var rendered = renderer.render(this); return new PklBugException(rendered, this); } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java index 50be8fa4..4402cbe3 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.pkl.core.runtime; import com.oracle.truffle.api.TruffleLanguage.ContextReference; +import com.oracle.truffle.api.TruffleLanguage.Env; +import com.oracle.truffle.api.instrumentation.Instrumenter; import com.oracle.truffle.api.nodes.Node; import java.nio.file.Path; import java.util.HashMap; @@ -33,6 +35,11 @@ import org.pkl.core.util.Nullable; public final class VmContext { private static final ContextReference REFERENCE = ContextReference.create(VmLanguage.class); + private final VmValueTrackerFactory valueTrackerFactory; + + public VmContext(VmLanguage vmLanguage, Env env) { + this.valueTrackerFactory = new VmValueTrackerFactory(env.lookup(Instrumenter.class)); + } @LateInit private Holder holder; @@ -151,4 +158,8 @@ public final class VmContext { public TraceMode getTraceMode() { return holder.traceMode; } + + public VmValueTrackerFactory getValueTrackerFactory() { + return valueTrackerFactory; + } } 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 1543ae1c..17bc1951 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,20 +20,23 @@ import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import org.pkl.core.StackFrame; +import org.pkl.core.util.AnsiStringBuilder; import org.pkl.core.util.Nullable; public class VmEvalException extends VmException { public VmEvalException( - String message, + @Nullable String message, @Nullable Throwable cause, boolean isExternalMessage, Object[] messageArguments, + @Nullable BiConsumer messageBuilder, List programValues, @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint, + @Nullable BiConsumer hintBuilder, Map insertedStackFrames) { super( @@ -41,11 +44,12 @@ public class VmEvalException extends VmException { cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, + hintBuilder, 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 bc27d340..f107273a 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,9 @@ import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import org.pkl.core.*; +import org.pkl.core.util.AnsiStringBuilder; import org.pkl.core.util.Nullable; public abstract class VmException extends AbstractTruffleException { @@ -31,28 +33,31 @@ public abstract class VmException extends AbstractTruffleException { private final List programValues; private final @Nullable SourceSection sourceSection; private final @Nullable String memberName; - protected @Nullable String hint; private final Map insertedStackFrames; + @Nullable private final BiConsumer messageBuilder; + @Nullable protected BiConsumer hintBuilder; public VmException( - String message, + @Nullable String message, @Nullable Throwable cause, boolean isExternalMessage, Object[] messageArguments, + @Nullable BiConsumer messageBuilder, List programValues, @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint, + @Nullable BiConsumer hintBuilder, Map insertedStackFrames) { super(message, cause, UNLIMITED_STACK_TRACE, location); + this.messageBuilder = messageBuilder; this.isExternalMessage = isExternalMessage; this.messageArguments = messageArguments; this.programValues = programValues; this.sourceSection = sourceSection; this.memberName = memberName; - this.hint = hint; this.insertedStackFrames = insertedStackFrames; + this.hintBuilder = hintBuilder; } public final boolean isExternalMessage() { @@ -75,14 +80,6 @@ public abstract class VmException extends AbstractTruffleException { return memberName; } - public @Nullable String getHint() { - return hint; - } - - public void setHint(@Nullable String hint) { - this.hint = hint; - } - /** * Stack frames to insert into the stack trace presented to the user. For each entry `(target, * frame)`, `frame` will be inserted below the top-most frame associated with `target`. @@ -91,6 +88,18 @@ public abstract class VmException extends AbstractTruffleException { return insertedStackFrames; } + public @Nullable BiConsumer getMessageBuilder() { + return messageBuilder; + } + + public @Nullable BiConsumer getHintBuilder() { + return hintBuilder; + } + + public void setHint(String hint) { + this.hintBuilder = ((builder, aBoolean) -> builder.append(hint)); + } + public enum Kind { EVAL_ERROR, UNDEFINED_VALUE, @@ -110,7 +119,7 @@ public abstract class VmException extends AbstractTruffleException { @TruffleBoundary public PklException toPklException(StackFrameTransformer transformer, boolean color) { - var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color); + var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color, true); var rendered = renderer.render(this); return new PklException(rendered); } 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 bb69ccaf..46321dde 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,13 @@ 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.function.BiConsumer; 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.AnsiStringBuilder; import org.pkl.core.util.Nullable; /** @@ -49,11 +51,13 @@ import org.pkl.core.util.Nullable; * preferable but isn't currently used. One problem with special formatting is that error * output doesn't always go to a terminal and hence may be rendered verbatim.) */ +@SuppressWarnings("UnusedReturnValue") public final class VmExceptionBuilder { private @Nullable Object receiver; private @Nullable Map insertedStackFrames; private VmException wrappedException; + private @Nullable BiConsumer hintBuilder; public static class MultilineValue { private final Iterable lines; @@ -86,6 +90,7 @@ public final class VmExceptionBuilder { } private @Nullable String message; + private @Nullable BiConsumer messageBuilder; private @Nullable Throwable cause; private VmException.Kind kind = VmException.Kind.EVAL_ERROR; private boolean isExternalMessage; @@ -94,7 +99,6 @@ public final class VmExceptionBuilder { private @Nullable Node location; private @Nullable SourceSection sourceSection; private @Nullable String memberName; - private @Nullable String hint; public VmExceptionBuilder typeMismatch(Object value, VmClass expectedType) { if (value instanceof VmNull) { @@ -293,6 +297,12 @@ public final class VmExceptionBuilder { return withMessage(message, args); } + public VmExceptionBuilder withMessageBuilder( + BiConsumer messageBuilder) { + this.messageBuilder = messageBuilder; + return this; + } + public VmExceptionBuilder withProgramValue(String name, Object value) { programValues.add(new ProgramValue(name, value)); return this; @@ -329,7 +339,15 @@ public final class VmExceptionBuilder { } public VmExceptionBuilder withHint(@Nullable String hint) { - this.hint = hint; + if (hint != null) { + this.hintBuilder = (builder, ignored) -> builder.append(hint); + } + return this; + } + + public VmExceptionBuilder withHintBuilder( + @Nullable BiConsumer hintBuilder) { + this.hintBuilder = hintBuilder; return this; } @@ -346,9 +364,12 @@ public final class VmExceptionBuilder { } public VmException build() { - if (message == null) { + if (message == null && messageBuilder == null) { throw new IllegalStateException("No message set."); } + if (message != null && messageBuilder != null) { + throw new IllegalStateException("Both message and messageBuilder are set"); + } var effectiveInsertedStackFrames = insertedStackFrames == null ? new HashMap() : insertedStackFrames; @@ -359,11 +380,12 @@ public final class VmExceptionBuilder { cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, + hintBuilder, effectiveInsertedStackFrames); case UNDEFINED_VALUE -> new VmUndefinedValueException( @@ -371,11 +393,12 @@ public final class VmExceptionBuilder { cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, + hintBuilder, receiver, effectiveInsertedStackFrames); case BUG -> @@ -384,11 +407,12 @@ public final class VmExceptionBuilder { cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, + hintBuilder, effectiveInsertedStackFrames); case WRAPPED -> new VmWrappedEvalException( @@ -396,11 +420,12 @@ public final class VmExceptionBuilder { cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, + hintBuilder, effectiveInsertedStackFrames, wrappedException); }; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java index c048051b..6358f783 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,14 +28,17 @@ import org.pkl.core.util.Nullable; public final class VmExceptionRenderer { private final @Nullable StackTraceRenderer stackTraceRenderer; private final boolean color; + private final boolean powerAssertions; /** * Constructs an error renderer with the given stack trace renderer. If stack trace renderer is * {@code null}, stack traces will not be included in error output. */ - public VmExceptionRenderer(@Nullable StackTraceRenderer stackTraceRenderer, boolean color) { + public VmExceptionRenderer( + @Nullable StackTraceRenderer stackTraceRenderer, boolean color, boolean powerAssertions) { this.stackTraceRenderer = stackTraceRenderer; this.color = color; + this.powerAssertions = powerAssertions; } @TruffleBoundary @@ -78,7 +81,8 @@ public final class VmExceptionRenderer { private void renderException(VmException exception, AnsiStringBuilder out, boolean withHeader) { String message; - var hint = exception.getHint(); + var hintBuilder = exception.getHintBuilder(); + @Nullable String hint = null; if (exception.isExternalMessage()) { var totalMessage = ErrorMessages.create(exception.getMessage(), exception.getMessageArguments()); @@ -99,7 +103,14 @@ public final class VmExceptionRenderer { if (withHeader) { out.append(AnsiTheme.ERROR_HEADER, "–– Pkl Error ––").append('\n'); } - out.append(AnsiTheme.ERROR_MESSAGE, message).append('\n'); + if (exception.getMessageBuilder() != null) { + out.append( + AnsiTheme.ERROR_MESSAGE, + () -> exception.getMessageBuilder().accept(out, powerAssertions)); + out.append('\n'); + } else { + out.append(AnsiTheme.ERROR_MESSAGE, message).append('\n'); + } // include cause's message unless it's the same as this exception's message if (exception.getCause() != null) { @@ -132,10 +143,13 @@ public final class VmExceptionRenderer { } if (!frames.isEmpty()) { - stackTraceRenderer.render(frames, hint, out.append('\n')); + stackTraceRenderer.render(frames, hint, hintBuilder, out.append('\n')); } else if (hint != null) { // render hint if there are no stack frames out.append('\n').append(AnsiTheme.ERROR_MESSAGE_HINT, hint); + } else if (hintBuilder != null) { + out.append('\n') + .append(AnsiTheme.ERROR_MESSAGE_HINT, () -> hintBuilder.accept(out, powerAssertions)); } } } 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 8ba81cef..2fe73ec6 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.Nullable; public final class VmFunction extends VmObjectLike { + private final Object thisValue; private final int paramCount; private final PklRootNode rootNode; @@ -97,6 +98,10 @@ public final class VmFunction extends VmObjectLike { return EconomicMaps.create(); } + public PklRootNode getRootNode() { + return rootNode; + } + @Override public @Nullable Object getCachedValue(Object key) { return null; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java index 1b57be15..2bbb4591 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.ContextThreadLocal; import com.oracle.truffle.api.TruffleLanguage; import com.oracle.truffle.api.TruffleLanguage.ContextPolicy; +import com.oracle.truffle.api.instrumentation.ProvidedTags; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.Source; import org.pkl.core.ast.builder.AstBuilder; @@ -36,6 +37,7 @@ import org.pkl.parser.syntax.Module; version = "0.31.0-dev", characterMimeTypes = VmLanguage.MIME_TYPE, contextPolicy = ContextPolicy.SHARED) +@ProvidedTags(PklTags.Expression.class) public final class VmLanguage extends TruffleLanguage { public static final String MIME_TYPE = "application/x-pkl"; @@ -51,7 +53,7 @@ public final class VmLanguage extends TruffleLanguage { @Override protected VmContext createContext(Env env) { - return new VmContext(); + return new VmContext(this, env); } @Override 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 018ee41d..29d8859f 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ public final class VmStackOverflowException extends VmException { e, true, new Object[0], + null, List.of(), null, null, 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 672f6187..0df25e43 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,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.Collections; import java.util.Deque; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import org.pkl.core.StackFrame; +import org.pkl.core.util.AnsiStringBuilder; import org.pkl.core.util.Nullable; import org.pkl.parser.Lexer; @@ -29,38 +32,40 @@ public final class VmUndefinedValueException extends VmEvalException { private final @Nullable Object receiver; public VmUndefinedValueException( - String message, + @Nullable String message, @Nullable Throwable cause, boolean isExternalMessage, Object[] messageArguments, + @Nullable BiConsumer messageBuilder, List programValues, @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint, + @Nullable BiConsumer hintBuilder, @Nullable Object receiver, - Map insertedStackFrames) { + @Nullable Map insertedStackFrames) { super( message, cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, - insertedStackFrames); + hintBuilder, + insertedStackFrames == null ? Collections.emptyMap() : insertedStackFrames); this.receiver = receiver; } public VmUndefinedValueException fillInHint(Deque path, Object topLevelValue) { - if (hint != null) return this; + if (hintBuilder != null) return this; + var builder = new StringBuilder(); var memberKey = getMessageArguments()[0]; path.push(memberKey); - var builder = new StringBuilder(); builder.append("The above error occurred when rendering path `"); renderPath(builder, path); builder.append('`'); @@ -72,7 +77,7 @@ public final class VmUndefinedValueException extends VmEvalException { .append('`'); } builder.append('.'); - hint = builder.toString(); + this.hintBuilder = (b, ignored) -> b.append(builder.toString()); return this; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTracker.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTracker.java new file mode 100644 index 00000000..772d2954 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTracker.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.instrumentation.EventBinding; +import com.oracle.truffle.api.instrumentation.ExecutionEventNode; +import com.oracle.truffle.api.instrumentation.ExecutionEventNodeFactory; +import com.oracle.truffle.api.instrumentation.Instrumenter; +import com.oracle.truffle.api.instrumentation.SourceSectionFilter; +import com.oracle.truffle.api.nodes.Node; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import org.pkl.core.util.Nullable; + +public final class VmValueTracker implements AutoCloseable { + + private final EventBinding binding; + + private final Map> values = new IdentityHashMap<>(); + + public VmValueTracker(Instrumenter instrumenter) { + binding = + instrumenter.attachExecutionEventFactory( + SourceSectionFilter.newBuilder().tagIs(PklTags.Expression.class).build(), + context -> + new ExecutionEventNode() { + @Override + @TruffleBoundary + protected void onReturnValue(VirtualFrame frame, @Nullable Object result) { + if (result == null) { + return; + } + var node = context.getInstrumentedNode(); + var myValues = values.getOrDefault(node, new ArrayList<>()); + myValues.add(result); + values.put(node, myValues); + } + }); + } + + public Map> values() { + return values; + } + + @Override + public void close() { + binding.dispose(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTrackerFactory.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTrackerFactory.java new file mode 100644 index 00000000..e1114194 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTrackerFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.instrumentation.Instrumenter; + +public final class VmValueTrackerFactory { + + private final Instrumenter instrumenter; + + public VmValueTrackerFactory(Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + @TruffleBoundary + public VmValueTracker create() { + return new VmValueTracker(instrumenter); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmWrappedEvalException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmWrappedEvalException.java index 59880849..014dd21b 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmWrappedEvalException.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmWrappedEvalException.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import org.pkl.core.StackFrame; +import org.pkl.core.util.AnsiStringBuilder; import org.pkl.core.util.Nullable; public class VmWrappedEvalException extends VmEvalException { @@ -28,15 +30,16 @@ public class VmWrappedEvalException extends VmEvalException { private final VmException wrappedException; public VmWrappedEvalException( - String message, + @Nullable String message, @Nullable Throwable cause, boolean isExternalMessage, Object[] messageArguments, + @Nullable BiConsumer messageBuilder, List programValues, @Nullable Node location, @Nullable SourceSection sourceSection, @Nullable String memberName, - @Nullable String hint, + @Nullable BiConsumer hintBuilder, Map insertedStackFrames, VmException wrappedException) { super( @@ -44,11 +47,12 @@ public class VmWrappedEvalException extends VmEvalException { cause, isExternalMessage, messageArguments, + messageBuilder, programValues, location, sourceSection, memberName, - hint, + hintBuilder, insertedStackFrames); this.wrappedException = wrappedException; } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalPropertyNode.java index fde7f016..bfdd252d 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/ExternalPropertyNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,4 +21,9 @@ public abstract class ExternalPropertyNode extends ExternalMemberNode { public interface Factory { ExternalPropertyNode create(ExpressionNode receiverNode); } + + @Override + public boolean isInstrumentable() { + return false; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/CollectionNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/CollectionNodes.java index 3da094c8..96a0e7ae 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/CollectionNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/CollectionNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; import org.pkl.core.ast.PklNode; import org.pkl.core.ast.expression.binary.LessThanNode; import org.pkl.core.ast.expression.binary.LessThanNodeGen; @@ -32,7 +33,8 @@ public final class CollectionNodes { private CollectionNodes() {} public abstract static class SortComparatorNode extends PklNode { - public abstract boolean executeWith(Object left, Object right, @Nullable VmFunction function); + public abstract boolean executeWith( + VirtualFrame frame, Object left, Object right, @Nullable VmFunction function); } public static final class CompareNode extends SortComparatorNode { @@ -42,9 +44,10 @@ public final class CollectionNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Override - public boolean executeWith(Object left, Object right, @Nullable VmFunction function) { + public boolean executeWith( + VirtualFrame frame, Object left, Object right, @Nullable VmFunction function) { assert function == null; - return lessThanNode.executeWith(left, right); + return lessThanNode.executeWith(frame, left, right); } } @@ -57,11 +60,12 @@ public final class CollectionNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Override - public boolean executeWith(Object left, Object right, @Nullable VmFunction selector) { + public boolean executeWith( + VirtualFrame frame, Object left, Object right, @Nullable VmFunction selector) { assert selector != null; var leftResult = applyLambdaNode.execute(selector, left); var rightResult = applyLambdaNode.execute(selector, right); - return lessThanNode.executeWith(leftResult, rightResult); + return lessThanNode.executeWith(frame, leftResult, rightResult); } } @@ -69,7 +73,8 @@ public final class CollectionNodes { @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create(); @Override - public boolean executeWith(Object left, Object right, @Nullable VmFunction comparator) { + public boolean executeWith( + VirtualFrame frame, Object left, Object right, @Nullable VmFunction comparator) { assert comparator != null; var result = applyLambdaNode.execute(comparator, left, right); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java index a36f952d..af33801b 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListNodes.java @@ -856,7 +856,7 @@ public final class ListNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self) { + protected Object eval(VirtualFrame frame, VmList self) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -864,7 +864,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (lessThanNode.executeWith(elem, result)) { + if (lessThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -881,7 +881,7 @@ public final class ListNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self) { + protected Object eval(VirtualFrame frame, VmList self) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -889,7 +889,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (lessThanNode.executeWith(elem, result)) { + if (lessThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -906,7 +906,7 @@ public final class ListNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self) { + protected Object eval(VirtualFrame frame, VmList self) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -914,7 +914,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (greaterThanNode.executeWith(elem, result)) { + if (greaterThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -931,7 +931,7 @@ public final class ListNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self) { + protected Object eval(VirtualFrame frame, VmList self) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -939,7 +939,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (greaterThanNode.executeWith(elem, result)) { + if (greaterThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -958,7 +958,7 @@ public final class ListNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmList self, VmFunction function) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -968,7 +968,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (lessThanNode.executeWith(elemValue, resultValue)) { + if (lessThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -988,7 +988,7 @@ public final class ListNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmList self, VmFunction function) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -998,7 +998,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (greaterThanNode.executeWith(elemValue, resultValue)) { + if (greaterThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -1018,7 +1018,7 @@ public final class ListNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmList self, VmFunction function) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -1028,7 +1028,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (lessThanNode.executeWith(elemValue, resultValue)) { + if (lessThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -1048,7 +1048,7 @@ public final class ListNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmList self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmList self, VmFunction function) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -1058,7 +1058,7 @@ public final class ListNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (greaterThanNode.executeWith(elemValue, resultValue)) { + if (greaterThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -1161,8 +1161,8 @@ public final class ListNodes { @Child private CompareNode compareNode = new CompareNode(); @Specialization - protected VmList eval(VmList self) { - return VmList.create(MergeSort.sort(self.toArray(), compareNode, null)); + protected VmList eval(VirtualFrame frame, VmList self) { + return VmList.create(MergeSort.sort(frame, self.toArray(), compareNode, null)); } } @@ -1170,8 +1170,8 @@ public final class ListNodes { @Child private CompareByNode compareByNode = new CompareByNode(); @Specialization - protected VmList eval(VmList self, VmFunction selector) { - return VmList.create(MergeSort.sort(self.toArray(), compareByNode, selector)); + protected VmList eval(VirtualFrame frame, VmList self, VmFunction selector) { + return VmList.create(MergeSort.sort(frame, self.toArray(), compareByNode, selector)); } } @@ -1179,8 +1179,8 @@ public final class ListNodes { @Child private CompareWithNode compareWithNode = new CompareWithNode(); @Specialization - protected VmList eval(VmList self, VmFunction function) { - return VmList.create(MergeSort.sort(self.toArray(), compareWithNode, function)); + protected VmList eval(VirtualFrame frame, VmList self, VmFunction function) { + return VmList.create(MergeSort.sort(frame, self.toArray(), compareWithNode, function)); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MergeSort.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MergeSort.java index 3b9cdc22..d7ad0e8e 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MergeSort.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MergeSort.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package org.pkl.core.stdlib.base; +import com.oracle.truffle.api.frame.VirtualFrame; import org.pkl.core.runtime.VmFunction; import org.pkl.core.stdlib.base.CollectionNodes.SortComparatorNode; import org.pkl.core.util.Nullable; @@ -26,7 +27,10 @@ final class MergeSort { private MergeSort() {} public static Object[] sort( - Object[] array, SortComparatorNode comparator, @Nullable VmFunction function) { + VirtualFrame frame, + Object[] array, + SortComparatorNode comparator, + @Nullable VmFunction function) { var length = array.length; var temp = new Object[length]; @@ -35,14 +39,14 @@ final class MergeSort { for (var start = 0; start < length; start += INITIAL_MERGE_SORT_STRIDE) { var end = Math.min(start + INITIAL_MERGE_SORT_STRIDE, length); - insertionSort(array, start, end, comparator, function); + insertionSort(frame, array, start, end, comparator, function); } for (var stride = INITIAL_MERGE_SORT_STRIDE; stride < length; stride *= 2) { for (var start = 0; start < length - stride; start += stride + stride) { var end = Math.min(start + stride + stride, length); var mid = start + stride; // start of second half - merge(array, temp, start, mid, end, comparator, function); + merge(frame, array, temp, start, mid, end, comparator, function); } } @@ -50,6 +54,7 @@ final class MergeSort { } private static void merge( + VirtualFrame frame, Object[] array, Object[] temp, int start, @@ -58,7 +63,7 @@ final class MergeSort { SortComparatorNode comparator, @Nullable VmFunction function) { - if (comparator.executeWith(array[mid - 1], array[mid], function)) { + if (comparator.executeWith(frame, array[mid - 1], array[mid], function)) { return; // already sorted } @@ -70,12 +75,13 @@ final class MergeSort { for (var k = start; k < end; k++) { if (i >= mid) array[k] = temp[j++]; else if (j >= end) array[k] = temp[i++]; - else if (comparator.executeWith(temp[j], temp[i], function)) array[k] = temp[j++]; + else if (comparator.executeWith(frame, temp[j], temp[i], function)) array[k] = temp[j++]; else array[k] = temp[i++]; } } private static void insertionSort( + VirtualFrame frame, Object[] array, int start, int end, @@ -83,7 +89,9 @@ final class MergeSort { @Nullable VmFunction function) { for (var i = start; i < end; i++) { - for (var j = i; j > start && comparator.executeWith(array[j], array[j - 1], function); j--) { + for (var j = i; + j > start && comparator.executeWith(frame, array[j], array[j - 1], function); + j--) { Object swap = array[j]; array[j] = array[j - 1]; array[j - 1] = swap; diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java index 76f250d2..f189fe3f 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/SetNodes.java @@ -18,6 +18,7 @@ package org.pkl.core.stdlib.base; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.LoopNode; import org.pkl.core.ast.expression.binary.GreaterThanNode; import org.pkl.core.ast.expression.binary.GreaterThanNodeGen; @@ -632,7 +633,7 @@ public final class SetNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self) { + protected Object eval(VirtualFrame frame, VmSet self) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -640,7 +641,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (lessThanNode.executeWith(elem, result)) { + if (lessThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -657,7 +658,7 @@ public final class SetNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self) { + protected Object eval(VirtualFrame frame, VmSet self) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -665,7 +666,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (lessThanNode.executeWith(elem, result)) { + if (lessThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -684,7 +685,7 @@ public final class SetNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmSet self, VmFunction function) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -694,7 +695,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (lessThanNode.executeWith(elemValue, resultValue)) { + if (lessThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -714,7 +715,7 @@ public final class SetNodes { LessThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmSet self, VmFunction function) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -724,7 +725,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (lessThanNode.executeWith(elemValue, resultValue)) { + if (lessThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -800,7 +801,7 @@ public final class SetNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self) { + protected Object eval(VirtualFrame frame, VmSet self) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -808,7 +809,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (greaterThanNode.executeWith(elem, result)) { + if (greaterThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -825,7 +826,7 @@ public final class SetNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self) { + protected Object eval(VirtualFrame frame, VmSet self) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -833,7 +834,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); - if (greaterThanNode.executeWith(elem, result)) { + if (greaterThanNode.executeWith(frame, elem, result)) { result = elem; } } @@ -852,7 +853,7 @@ public final class SetNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmSet self, VmFunction function) { self.checkNonEmpty(); var iterator = self.iterator(); @@ -862,7 +863,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (greaterThanNode.executeWith(elemValue, resultValue)) { + if (greaterThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -882,7 +883,7 @@ public final class SetNodes { GreaterThanNodeGen.create(VmUtils.unavailableSourceSection(), null, null); @Specialization - protected Object eval(VmSet self, VmFunction function) { + protected Object eval(VirtualFrame frame, VmSet self, VmFunction function) { if (self.isEmpty()) return VmNull.withoutDefault(); var iterator = self.iterator(); @@ -892,7 +893,7 @@ public final class SetNodes { while (iterator.hasNext()) { var elem = iterator.next(); var elemValue = applyLambdaNode.execute(function, elem); - if (greaterThanNode.executeWith(elemValue, resultValue)) { + if (greaterThanNode.executeWith(frame, elemValue, resultValue)) { result = elem; resultValue = elemValue; } @@ -965,8 +966,8 @@ public final class SetNodes { @Child private CompareNode compareNode = new CompareNode(); @Specialization - protected VmList eval(VmSet self) { - return VmList.create(MergeSort.sort(self.toArray(), compareNode, null)); + protected VmList eval(VirtualFrame frame, VmSet self) { + return VmList.create(MergeSort.sort(frame, self.toArray(), compareNode, null)); } } @@ -974,8 +975,8 @@ public final class SetNodes { @Child private CompareByNode compareByNode = new CompareByNode(); @Specialization - protected VmList eval(VmSet self, VmFunction selector) { - return VmList.create(MergeSort.sort(self.toArray(), compareByNode, selector)); + protected VmList eval(VirtualFrame frame, VmSet self, VmFunction selector) { + return VmList.create(MergeSort.sort(frame, self.toArray(), compareByNode, selector)); } } @@ -983,8 +984,8 @@ public final class SetNodes { @Child private CompareWithNode compareWithNode = new CompareWithNode(); @Specialization - protected VmList eval(VmSet self, VmFunction function) { - return VmList.create(MergeSort.sort(self.toArray(), compareWithNode, function)); + protected VmList eval(VirtualFrame frame, VmSet self, VmFunction function) { + return VmList.create(MergeSort.sort(frame, self.toArray(), compareWithNode, function)); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/test/TestNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/test/TestNodes.java index b40643d6..bebb40ed 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/test/TestNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/test/TestNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,8 @@ import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.PklName; public final class TestNodes { - private static final VmExceptionRenderer noStackTraceExceptionRenderer = - new VmExceptionRenderer(null, false); + private static final VmExceptionRenderer testNodeRenderer = + new VmExceptionRenderer(null, false, false); private TestNodes() {} @@ -64,7 +64,7 @@ public final class TestNodes { @TruffleBoundary private static String render(VmException e) { - return noStackTraceExceptionRenderer + return testNodeRenderer .render(e) .lines() .skip(1) // remove meaningless header line diff --git a/pkl-core/src/main/java/org/pkl/core/util/AnsiStringBuilder.java b/pkl-core/src/main/java/org/pkl/core/util/AnsiStringBuilder.java index 2e4ffe97..13d1871a 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/AnsiStringBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/util/AnsiStringBuilder.java @@ -108,6 +108,19 @@ public final class AnsiStringBuilder { return this; } + public AnsiStringBuilder append(EnumSet codes, Runnable runnable) { + if (!usingColor) { + runnable.run(); + return this; + } + var prevDeclaredCodes = declaredCodes; + declaredCodes = EnumSet.copyOf(codes); + declaredCodes.addAll(prevDeclaredCodes); + runnable.run(); + declaredCodes = prevDeclaredCodes; + return this; + } + /** Provides a runnable where anything appended is not affected by the existing context. */ public AnsiStringBuilder appendSandboxed(Runnable runnable) { if (!usingColor) { diff --git a/pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java b/pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java index 3b54d050..49019a90 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java +++ b/pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java @@ -16,7 +16,6 @@ package org.pkl.core.util; import java.util.EnumSet; -import java.util.Set; import org.pkl.core.util.AnsiStringBuilder.AnsiCode; public final class AnsiTheme { @@ -24,7 +23,7 @@ public final class AnsiTheme { public static final AnsiCode ERROR_MESSAGE_HINT = AnsiCode.YELLOW; public static final AnsiCode ERROR_HEADER = AnsiCode.RED; - public static final Set ERROR_MESSAGE = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD); + public static final EnumSet ERROR_MESSAGE = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD); public static final AnsiCode STACK_FRAME = AnsiCode.FAINT; public static final AnsiCode STACK_TRACE_MARGIN = AnsiCode.YELLOW; diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints1.pkl new file mode 100644 index 00000000..8e05ef33 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints1.pkl @@ -0,0 +1 @@ +foo: String(this == "foo") = "bar" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints10.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints10.pkl new file mode 100644 index 00000000..8377d0ed --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints10.pkl @@ -0,0 +1,3 @@ +local list = List(1, 2, 3) + +foo: Dynamic(this == new Dynamic { ...list }) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints11.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints11.pkl new file mode 100644 index 00000000..cbf6a132 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints11.pkl @@ -0,0 +1,4 @@ +local list = List(1) + +foo: Dynamic(this == new Dynamic { for (elem in list) { elem + 1 } }) + diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints12.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints12.pkl new file mode 100644 index 00000000..3ea336ce --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints12.pkl @@ -0,0 +1 @@ +foo: String(true, this.length > 5) = "bob" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints13.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints13.pkl new file mode 100644 index 00000000..73394616 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints13.pkl @@ -0,0 +1,3 @@ +name: String = "Bub" + +foo: String(this == "Hello, \(name)") = "Hello, Patty" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints14.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints14.pkl new file mode 100644 index 00000000..65df6323 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints14.pkl @@ -0,0 +1,9 @@ +open class Foo { + name: String = "Foo" +} + +class Bar extends Foo { + bub: String(startsWith(super.name)) = "Bubby" +} + +bar: Bar diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints15.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints15.pkl new file mode 100644 index 00000000..149935bb --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints15.pkl @@ -0,0 +1 @@ +foo: String(this == "foo") | String(this == "qux") = "bar" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints16.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints16.pkl new file mode 100644 index 00000000..f81c47d7 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints16.pkl @@ -0,0 +1,10 @@ +class Bird { + name: String? +} + +// this will create `NullPropagatingOperationNode` and `InvokeVirtualMethodNode` for +// `name?.contains("Bob")`. +// this tests that we don't have two duplicate values in the diagram. +p: Bird(name?.contains("Bob") ?? false) = new { + name = "Pigeon" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints17.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints17.pkl new file mode 100644 index 00000000..055973eb --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints17.pkl @@ -0,0 +1,4 @@ +local name = "Bob" +local function greet(name: String) = "Hello, \(name)" + +foo: Any(greet(name) == "Hello, \(name)!") = null diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints2.pkl new file mode 100644 index 00000000..4e2cd490 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints2.pkl @@ -0,0 +1,7 @@ +foo: String(startsWith(bub.name)) = "Huzzah" + +bub: Person = new { name = "Bub" } + +class Person { + name: String +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints3.pkl new file mode 100644 index 00000000..ed70c48f --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints3.pkl @@ -0,0 +1,10 @@ +foo: Int(this + + two + / three + == four) = 4 + +local two = 2 + +local three = 3 + +local four = 4 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints4.pkl new file mode 100644 index 00000000..9c3caad4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints4.pkl @@ -0,0 +1,3 @@ +foo: List(fold(0, (a, b) -> a + b) == 5) = myList + +myList = List(1, 2, 3) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints5.pkl new file mode 100644 index 00000000..8de4f737 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints5.pkl @@ -0,0 +1,3 @@ +foo: String(isCapitalized) = "hello" + +local isCapitalized = (it: String) -> it == it.capitalize() diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints6.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints6.pkl new file mode 100644 index 00000000..cf36f038 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints6.pkl @@ -0,0 +1,5 @@ +foo: String(isCapitalized) = "hello" + +local isCapitalized = (it: String) -> + let (first = it.take(1).toUpperCase()) + it == "\(first)\(it.drop(1))" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints7.pkl new file mode 100644 index 00000000..911df0b9 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints7.pkl @@ -0,0 +1 @@ +foo: UInt8 = -5 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints8.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints8.pkl new file mode 100644 index 00000000..aabb4419 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints8.pkl @@ -0,0 +1 @@ +foo: NonNull = null diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9.pkl new file mode 100644 index 00000000..8ea0c3fc --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9.pkl @@ -0,0 +1,10 @@ +// stdlib constructors are treated as literals +foo: Int( + IntSeq(1, 5).toList().contains(this) + || Map(1, 2, 3, 4).values.contains(this) + || List(1, 2, 3, 4).contains(this) + || Set(1, 2, 3, 4).contains(this) + || Pair(1, 2).second == this + || Bytes(1, 2, 3, 4).toList().contains(this) + || Regex("1234").toString().length == this + ) = 10 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9a.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9a.pkl new file mode 100644 index 00000000..5ce65b7b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/power/typeConstraints9a.pkl @@ -0,0 +1,12 @@ +local num = 1 + +// stdlib constructors that have a non-literal are not treated as literals +foo: Int( + IntSeq(num, 5).toList().contains(this) + || Map(num, 2, 3, 4).values.contains(this) + || List(num, 2, 3, 4).contains(this) + || Set(num, 2, 3, 4).contains(this) + || Pair(num, 2).second == this + || Bytes(num, 2, 3, 4).toList().contains(this) + || Regex("\(num)234").toString().length == this + ) = 10 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/badProjectDeps4/PklProject b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/badProjectDeps4/PklProject index 35200a12..b532f4c1 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/badProjectDeps4/PklProject +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/badProjectDeps4/PklProject @@ -1,5 +1,10 @@ amends "pkl:Project" dependencies { - ["badLocalProject"] = import("../badLocalProject/PklProject") + ["badLocalProject"] = import("PklProject") } + +// hack: override the project file URI so that our tests render the same error message +// across different machines (it will truncate at 100 chars, and otherwise would be influenced by +// where the pkl project is placed within the file tree). +projectFileUri = "file:///$snippetsDir/projects/badProjectDeps4/PklProject" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/classes/constraints5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/classes/constraints5.err index a8c15af9..7216aa97 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/classes/constraints5.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/classes/constraints5.err @@ -2,6 +2,11 @@ Type constraint `this >= min` violated. Value: 3 + this >= min + │ │ │ + 3 │ 4 + false + x | max: Int(this >= min) ^^^^^^^^^^^ at constraints5#Gauge.max (file:///$snippetsDir/input/classes/constraints5.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/classes/unionTypesErrorDifferent2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/classes/unionTypesErrorDifferent2.err index 8ac2e5fe..be5f9e17 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/classes/unionTypesErrorDifferent2.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/classes/unionTypesErrorDifferent2.err @@ -10,6 +10,10 @@ at unionTypesErrorDifferent2#X.a (file:///$snippetsDir/input/classes/unionTypesE Type constraint `length > 3` violated. Value: List(1, 2, 3) + length > 3 + │ │ + 3 false + x | a = List(1, 2, 3) ^^^^^^^^^^^^^ at unionTypesErrorDifferent2#res1.a (file:///$snippetsDir/input/classes/unionTypesErrorDifferent2.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/classes/wrongType6.err b/pkl-core/src/test/files/LanguageSnippetTests/output/classes/wrongType6.err index 5ec145dc..09349ba3 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/classes/wrongType6.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/classes/wrongType6.err @@ -2,6 +2,11 @@ Type constraint `!isEmpty` violated. Value: List() + !isEmpty + ││ + │true + false + x | names: List(!isEmpty) ^^^^^^^^ at wrongType6#Person.names (file:///$snippetsDir/input/classes/wrongType6.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails1.err index f2590461..8e029c38 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails1.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails1.err @@ -2,6 +2,13 @@ Type constraint `firstOneIsSandy` violated. Value: new Listing { new Bird { name = "Bob" }; new Bird { name = ? } } + (it: Listing) -> it[0].name == "Sandy" + │ │ │ │ + │ │ │ false + │ │ "Bob" + │ new Bird { name = "Bob" } + new Listing { new Bird { name = "Bob" }; new Bird { name = ? } } + x | birds: Listing(firstOneIsSandy) = new { ^^^^^^^^^^^^^^^ at constraintDetails1#birds (file:///$snippetsDir/input/errors/constraintDetails1.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails2.err index 7ca7df89..d2c6cba6 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails2.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails2.err @@ -3,6 +3,17 @@ Type constraint `let (myself: Listing = this) myself[0].name == "Sandy"` violated. Value: new Listing { new Bird { name = "Bob" }; new Bird { name = ? } } + let (myself: Listing = this) + │ + new Listing { new Bird { name = "Bob" }; new Bird { name = ? } } + + myself[0].name == "Sandy" + │ │ │ │ + │ │ │ false + │ │ "Bob" + │ new Bird { name = "Bob" } + new Listing { new Bird { name = "Bob" }; new Bird { name = ? } } + x | let (myself: Listing = this) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at constraintDetails2#birds (file:///$snippetsDir/input/errors/constraintDetails2.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails3.err index 2239f172..f847d7de 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails3.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/constraintDetails3.err @@ -2,6 +2,11 @@ Type constraint `toList().every((it: Listing) -> it[0].name == "Bob")` violated. Value: new Listing { new Listing { new Bird { name = "Eagle" }; new Bird { name = ? ... + toList().every((it: Listing) -> it[0].name == "Bob") + │ │ + │ false + List(new Listing { new Bird { name = "Eagle" }; new Bird { name = ? } }) + x | foo: Listing(toList().every((it: Listing) -> it[0].name == "Bob")) = new { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at constraintDetails3#foo (file:///$snippetsDir/input/errors/constraintDetails3.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias2.err index 56f4b730..28455cdf 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias2.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias2.err @@ -2,6 +2,10 @@ Type constraint `isBetween(0, 65535)` violated. Value: -1 + isBetween(0, 65535) + │ + false + xxxx | typealias UInt16 = Int(isBetween(0, 65535)) ^^^^^^^^^^^^^^^^^^^ at intrinsifiedTypeAlias2#res1 (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias3.err index 773a8a8f..ff7c4f96 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias3.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/intrinsifiedTypeAlias3.err @@ -2,6 +2,10 @@ Type constraint `!(this is Null)` violated. Value: null + !(this is Null) + │ + false + xx | typealias NonNull = Any(!(this is Null)) ^^^^^^^^^^^^^^^ at intrinsifiedTypeAlias3#res1 (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes1.err index 9a7b70ac..c54d1f2b 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes1.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes1.err @@ -2,6 +2,10 @@ Type constraint `isBetween(0, 255)` violated. Value: 65535 + isBetween(0, 255) + │ + false + xxxx | typealias UInt8 = Int(isBetween(0, 255)) ^^^^^^^^^^^^^^^^^ at invalidBytes1#foo (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes2.err index 8891ae6b..9ef6f1cb 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes2.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes2.err @@ -2,6 +2,10 @@ Type constraint `isBetween(0, 255)` violated. Value: 4095 + isBetween(0, 255) + │ + false + xxxx | typealias UInt8 = Int(isBetween(0, 255)) ^^^^^^^^^^^^^^^^^ at invalidBytes2#res (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes3.err index 9945f4f4..e906a074 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes3.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes3.err @@ -2,6 +2,10 @@ Type constraint `isBetween(0, 255)` violated. Value: 65535 + isBetween(0, 255) + │ + false + xxxx | typealias UInt8 = Int(isBetween(0, 255)) ^^^^^^^^^^^^^^^^^ at invalidBytes3#bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes4.err index afc60559..ced9008a 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes4.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/invalidBytes4.err @@ -2,6 +2,10 @@ Type constraint `isBetween(0, 255)` violated. Value: 65535 + isBetween(0, 255) + │ + false + xxxx | typealias UInt8 = Int(isBetween(0, 255)) ^^^^^^^^^^^^^^^^^ at pkl.base#List.toBytes (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 index 5362f45a..b29f5f70 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError3.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError3.err @@ -2,6 +2,11 @@ Type constraint `!isEmpty` violated. Value: "" + !isEmpty + ││ + │true + false + x | res: Listing = new Listing { ^^^^^^^^ at listingTypeCheckError3#res (file:///$snippetsDir/input/errors/listingTypeCheckError3.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError4.err index 5f2d3918..5bc79790 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError4.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError4.err @@ -2,6 +2,10 @@ Type constraint `endsWith("ga")` violated. Value: "hola" + endsWith("ga") + │ + false + x | res: Listing = new Listing { ^^^^^^^^^^^^^^ at listingTypeCheckError4#res (file:///$snippetsDir/input/errors/listingTypeCheckError4.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError6.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError6.err index 07fd1783..1c303f75 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError6.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError6.err @@ -2,6 +2,10 @@ Type constraint `length == 5` violated. Value: "hi" + length == 5 + │ │ + 2 false + xx | res: Mapping, String> = bar ^^^^^^^^^^^ at mappingTypeCheckError6#res (file:///$snippetsDir/input/errors/mappingTypeCheckError6.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError7.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError7.err index c49d8561..e8b032bb 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError7.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError7.err @@ -2,6 +2,10 @@ Type constraint `length == 5` violated. Value: "hi" + length == 5 + │ │ + 2 false + x | res = new Mapping, String> { ^^^^^^^^^^^ at mappingTypeCheckError7#res (file:///$snippetsDir/input/errors/mappingTypeCheckError7.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints1.err new file mode 100644 index 00000000..0514c634 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints1.err @@ -0,0 +1,24 @@ +–– Pkl Error –– +Type constraint `this == "foo"` violated. +Value: "bar" + + this == "foo" + │ │ + │ false + "bar" + +x | foo: String(this == "foo") = "bar" + ^^^^^^^^^^^^^ +at typeConstraints1#foo (file:///$snippetsDir/input/errors/power/typeConstraints1.pkl) + +x | foo: String(this == "foo") = "bar" + ^^^^^ +at typeConstraints1#foo (file:///$snippetsDir/input/errors/power/typeConstraints1.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints10.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints10.err new file mode 100644 index 00000000..0568a93e --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints10.err @@ -0,0 +1,26 @@ +–– Pkl Error –– +Type constraint `this == new Dynamic { ...list }` violated. +Value: new Dynamic {} + + this == new Dynamic { ...list } + │ │ │ │ + │ │ │ List(1, 2, 3) + │ │ new Dynamic { 1; 2; 3 } + │ false + new Dynamic {} + +x | foo: Dynamic(this == new Dynamic { ...list }) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints10#foo (file:///$snippetsDir/input/errors/power/typeConstraints10.pkl) + +x | foo: Dynamic(this == new Dynamic { ...list }) + ^^^ +at typeConstraints10#foo (file:///$snippetsDir/input/errors/power/typeConstraints10.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints11.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints11.err new file mode 100644 index 00000000..9fa8173a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints11.err @@ -0,0 +1,25 @@ +–– Pkl Error –– +Type constraint `this == new Dynamic { for (elem in list) { elem + 1 } }` violated. +Value: new Dynamic {} + + this == new Dynamic { for (elem in list) { elem + 1 } } + │ │ │ │ + │ │ new Dynamic { 2 } List(1) + │ false + new Dynamic {} + +x | foo: Dynamic(this == new Dynamic { for (elem in list) { elem + 1 } }) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints11#foo (file:///$snippetsDir/input/errors/power/typeConstraints11.pkl) + +x | foo: Dynamic(this == new Dynamic { for (elem in list) { elem + 1 } }) + ^^^ +at typeConstraints11#foo (file:///$snippetsDir/input/errors/power/typeConstraints11.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints12.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints12.err new file mode 100644 index 00000000..106acca7 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints12.err @@ -0,0 +1,24 @@ +–– Pkl Error –– +Type constraint `this.length > 5` violated. +Value: "bob" + + this.length > 5 + │ │ │ + │ 3 false + "bob" + +x | foo: String(true, this.length > 5) = "bob" + ^^^^^^^^^^^^^^^ +at typeConstraints12#foo (file:///$snippetsDir/input/errors/power/typeConstraints12.pkl) + +x | foo: String(true, this.length > 5) = "bob" + ^^^^^ +at typeConstraints12#foo (file:///$snippetsDir/input/errors/power/typeConstraints12.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints13.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints13.err new file mode 100644 index 00000000..26f1ea5c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints13.err @@ -0,0 +1,26 @@ +–– Pkl Error –– +Type constraint `this == "Hello, \(name)"` violated. +Value: "Hello, Patty" + + this == "Hello, \(name)" + │ │ │ │ + │ │ │ "Bub" + │ │ "Hello, Bub" + │ false + "Hello, Patty" + +x | foo: String(this == "Hello, \(name)") = "Hello, Patty" + ^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints13#foo (file:///$snippetsDir/input/errors/power/typeConstraints13.pkl) + +x | foo: String(this == "Hello, \(name)") = "Hello, Patty" + ^^^^^^^^^^^^^^ +at typeConstraints13#foo (file:///$snippetsDir/input/errors/power/typeConstraints13.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints14.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints14.err new file mode 100644 index 00000000..35e058e2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints14.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Type constraint `startsWith(super.name)` violated. +Value: "Bubby" + + startsWith(super.name) + │ │ + false "Foo" + +x | bub: String(startsWith(super.name)) = "Bubby" + ^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints14#Bar.bub (file:///$snippetsDir/input/errors/power/typeConstraints14.pkl) + +x | bub: String(startsWith(super.name)) = "Bubby" + ^^^^^^^ +at typeConstraints14#Bar.bub (file:///$snippetsDir/input/errors/power/typeConstraints14.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints15.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints15.err new file mode 100644 index 00000000..134b351e --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints15.err @@ -0,0 +1,37 @@ +–– Pkl Error –– +Expected value of type `String(this == "foo") | String(this == "qux")`, but got a different `String`. +Value: "bar" + +x | foo: String(this == "foo") | String(this == "qux") = "bar" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints15#foo (file:///$snippetsDir/input/errors/power/typeConstraints15.pkl) + +* Value is not of type `String(this == "foo")` because: + Type constraint `this == "foo"` violated. + Value: "bar" + + this == "foo" + │ │ + │ false + "bar" + +* Value is not of type `String(this == "qux")` because: + Type constraint `this == "qux"` violated. + Value: "bar" + + this == "qux" + │ │ + │ false + "bar" + +x | foo: String(this == "foo") | String(this == "qux") = "bar" + ^^^^^ +at typeConstraints15#foo (file:///$snippetsDir/input/errors/power/typeConstraints15.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints16.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints16.err new file mode 100644 index 00000000..d4561099 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints16.err @@ -0,0 +1,24 @@ +–– Pkl Error –– +Type constraint `name?.contains("Bob") ?? false` violated. +Value: new Bird { name = "Pigeon" } + + name?.contains("Bob") ?? false + │ │ │ + │ false false + "Pigeon" + +x | p: Bird(name?.contains("Bob") ?? false) = new { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints16#p (file:///$snippetsDir/input/errors/power/typeConstraints16.pkl) + +x | p: Bird(name?.contains("Bob") ?? false) = new { + ^^^^^ +at typeConstraints16#p (file:///$snippetsDir/input/errors/power/typeConstraints16.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints17.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints17.err new file mode 100644 index 00000000..9ba63257 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints17.err @@ -0,0 +1,27 @@ +–– Pkl Error –– +Type constraint `greet(name) == "Hello, \(name)!"` violated. +Value: null + + greet(name) == "Hello, \(name)!" + │ │ │ │ │ + │ │ │ │ "Bob" + │ │ │ "Hello, Bob!" + │ │ false + │ "Bob" + "Hello, Bob" + +x | foo: Any(greet(name) == "Hello, \(name)!") = null + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints17#foo (file:///$snippetsDir/input/errors/power/typeConstraints17.pkl) + +x | foo: Any(greet(name) == "Hello, \(name)!") = null + ^^^^ +at typeConstraints17#foo (file:///$snippetsDir/input/errors/power/typeConstraints17.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints2.err new file mode 100644 index 00000000..699fd7af --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints2.err @@ -0,0 +1,24 @@ +–– Pkl Error –– +Type constraint `startsWith(bub.name)` violated. +Value: "Huzzah" + + startsWith(bub.name) + │ │ │ + false │ "Bub" + new Person { name = "Bub" } + +x | foo: String(startsWith(bub.name)) = "Huzzah" + ^^^^^^^^^^^^^^^^^^^^ +at typeConstraints2#foo (file:///$snippetsDir/input/errors/power/typeConstraints2.pkl) + +x | foo: String(startsWith(bub.name)) = "Huzzah" + ^^^^^^^^ +at typeConstraints2#foo (file:///$snippetsDir/input/errors/power/typeConstraints2.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints3.err new file mode 100644 index 00000000..ddcfcc32 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints3.err @@ -0,0 +1,41 @@ +–– Pkl Error –– +Type constraint `this + + two + / three + == four` violated. +Value: 4 + + this + │ + 4 + + + two + │ │ + │ 2 + 4.666666666666667 + + / three + │ │ + │ 3 + 0.6666666666666666 + + == four + │ │ + │ 4 + false + +x | foo: Int(this + ^^^^ +at typeConstraints3#foo (file:///$snippetsDir/input/errors/power/typeConstraints3.pkl) + +x | == four) = 4 + ^ +at typeConstraints3#foo (file:///$snippetsDir/input/errors/power/typeConstraints3.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints4.err new file mode 100644 index 00000000..2d588dde --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints4.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Type constraint `fold(0, (a, b) -> a + b) == 5` violated. +Value: List(1, 2, 3) + + fold(0, (a, b) -> a + b) == 5 + │ │ + 6 false + +x | foo: List(fold(0, (a, b) -> a + b) == 5) = myList + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints4#foo (file:///$snippetsDir/input/errors/power/typeConstraints4.pkl) + +x | foo: List(fold(0, (a, b) -> a + b) == 5) = myList + ^^^^^^ +at typeConstraints4#foo (file:///$snippetsDir/input/errors/power/typeConstraints4.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints5.err new file mode 100644 index 00000000..7dbcd32b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints5.err @@ -0,0 +1,26 @@ +–– Pkl Error –– +Type constraint `isCapitalized` violated. +Value: "hello" + + (it: String) -> it == it.capitalize() + │ │ │ │ + │ │ │ "Hello" + │ │ "hello" + │ false + "hello" + +x | foo: String(isCapitalized) = "hello" + ^^^^^^^^^^^^^ +at typeConstraints5#foo (file:///$snippetsDir/input/errors/power/typeConstraints5.pkl) + +x | foo: String(isCapitalized) = "hello" + ^^^^^^^ +at typeConstraints5#foo (file:///$snippetsDir/input/errors/power/typeConstraints5.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints6.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints6.err new file mode 100644 index 00000000..b1fe0877 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints6.err @@ -0,0 +1,32 @@ +–– Pkl Error –– +Type constraint `isCapitalized` violated. +Value: "hello" + + (it: String) -> + let (first = it.take(1).toUpperCase()) + │ │ │ + │ "h" "H" + "hello" + + it == "\(first)\(it.drop(1))" + │ │ │ │ │ │ + │ │ │ "H" │ "ello" + │ │ "Hello" "hello" + │ false + "hello" + +x | foo: String(isCapitalized) = "hello" + ^^^^^^^^^^^^^ +at typeConstraints6#foo (file:///$snippetsDir/input/errors/power/typeConstraints6.pkl) + +x | foo: String(isCapitalized) = "hello" + ^^^^^^^ +at typeConstraints6#foo (file:///$snippetsDir/input/errors/power/typeConstraints6.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints7.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints7.err new file mode 100644 index 00000000..3dd87ec6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints7.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Type constraint `isBetween(0, 255)` violated. +Value: -5 + + isBetween(0, 255) + │ + false + +xxxx | typealias UInt8 = Int(isBetween(0, 255)) + ^^^^^^^^^^^^^^^^^ +at typeConstraints7#foo (pkl:base) + +x | foo: UInt8 = -5 + ^^ +at typeConstraints7#foo (file:///$snippetsDir/input/errors/power/typeConstraints7.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints8.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints8.err new file mode 100644 index 00000000..162e9ceb --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints8.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Type constraint `!(this is Null)` violated. +Value: null + + !(this is Null) + │ + false + +xx | typealias NonNull = Any(!(this is Null)) + ^^^^^^^^^^^^^^^ +at typeConstraints8#foo (pkl:base) + +x | foo: NonNull = null + ^^^^ +at typeConstraints8#foo (file:///$snippetsDir/input/errors/power/typeConstraints8.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9.err new file mode 100644 index 00000000..d355a0fa --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9.err @@ -0,0 +1,59 @@ +–– Pkl Error –– +Type constraint `IntSeq(1, 5).toList().contains(this) + || Map(1, 2, 3, 4).values.contains(this) + || List(1, 2, 3, 4).contains(this) + || Set(1, 2, 3, 4).contains(this) + || Pair(1, 2).second == this + || Bytes(1, 2, 3, 4).toList().contains(this) + || Regex("1234").toString().length == this` violated. +Value: 10 + + IntSeq(1, 5).toList().contains(this) + │ │ │ + │ false 10 + List(1, 2, 3, 4, 5) + + || Map(1, 2, 3, 4).values.contains(this) + │ │ │ │ + false │ false 10 + List(2, 4) + + || List(1, 2, 3, 4).contains(this) + │ │ │ + false false 10 + + || Set(1, 2, 3, 4).contains(this) + │ │ │ + false false 10 + + || Pair(1, 2).second == this + │ │ │ │ + false 2 │ 10 + false + + || Bytes(1, 2, 3, 4).toList().contains(this) + │ │ │ │ + false │ false 10 + List(1, 2, 3, 4) + + || Regex("1234").toString().length == this + │ │ │ │ │ + false │ 13 │ 10 + │ false + "Regex(\"1234\")" + +x | IntSeq(1, 5).toList().contains(this) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints9#foo (file:///$snippetsDir/input/errors/power/typeConstraints9.pkl) + +xx | ) = 10 + ^^ +at typeConstraints9#foo (file:///$snippetsDir/input/errors/power/typeConstraints9.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9a.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9a.err new file mode 100644 index 00000000..e7a608de --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/power/typeConstraints9a.err @@ -0,0 +1,67 @@ +–– Pkl Error –– +Type constraint `IntSeq(num, 5).toList().contains(this) + || Map(num, 2, 3, 4).values.contains(this) + || List(num, 2, 3, 4).contains(this) + || Set(num, 2, 3, 4).contains(this) + || Pair(num, 2).second == this + || Bytes(num, 2, 3, 4).toList().contains(this) + || Regex("\(num)234").toString().length == this` violated. +Value: 10 + + IntSeq(num, 5).toList().contains(this) + │ │ │ │ │ + │ 1 │ false 10 + IntSeq(1, 5) List(1, 2, 3, 4, 5) + + || Map(num, 2, 3, 4).values.contains(this) + │ │ │ │ │ │ + │ │ 1 │ false 10 + │ Map(1, 2, 3, 4) List(2, 4) + false + + || List(num, 2, 3, 4).contains(this) + │ │ │ │ │ + │ │ 1 false 10 + │ List(1, 2, 3, 4) + false + + || Set(num, 2, 3, 4).contains(this) + │ │ │ │ │ + │ │ 1 false 10 + │ Set(1, 2, 3, 4) + false + + || Pair(num, 2).second == this + │ │ │ │ │ │ + │ │ 1 2 │ 10 + │ Pair(1, 2) false + false + + || Bytes(num, 2, 3, 4).toList().contains(this) + │ │ │ │ │ │ + │ │ 1 │ false 10 + │ Bytes(1, 2, 3, 4) List(1, 2, 3, 4) + false + + || Regex("\(num)234").toString().length == this + │ │ │ │ │ │ │ │ + │ │ │ 1 │ 13 │ 10 + │ │ "1234" │ false + │ Regex("1234") "Regex(\"1234\")" + false + +x | IntSeq(num, 5).toList().contains(this) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at typeConstraints9a#foo (file:///$snippetsDir/input/errors/power/typeConstraints9a.pkl) + +xx | ) = 10 + ^^ +at typeConstraints9a#foo (file:///$snippetsDir/input/errors/power/typeConstraints9a.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/typedModuleProperties3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/typedModuleProperties3.err index 4c6e4944..d7eba49c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/typedModuleProperties3.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/typedModuleProperties3.err @@ -2,6 +2,11 @@ Type constraint `!isEmpty` violated. Value: "" + !isEmpty + ││ + │true + false + x | res1: String(!isEmpty) = "" ^^^^^^^^ at typedModuleProperties3#res1 (file:///$snippetsDir/input/modules/typedModuleProperties3.pkl) 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 3241b2b8..d29b1584 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 @@ -1,6 +1,6 @@ –– Pkl Error –– Expected value of type `*RemoteDependency | Project(isValidLoadDependency)`, but got a different `pkl.Project`. -Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetting... +Value: new ModuleClass { package = ?; tests = ?; dependencies { ["badLocalProject"] ... xxx | dependencies: Mapping ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -8,8 +8,32 @@ at pkl.Project#dependencies (pkl:Project) * Value is not of type `Project(isValidLoadDependency)` because: Type constraint `isValidLoadDependency` violated. - Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetti... + Value: new ModuleClass { package = ?; tests = ?; dependencies { ["badLocalProject"... -x | ["badLocalProject"] = import("../badLocalProject/PklProject") - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + (it: Project) -> + isUriLocal(projectFileUri, it.projectFileUri) + │ │ │ │ + true │ │ "file:///$snippetsDir/projects/badProjectDeps4/PklProject" + │ new ModuleClass { package = ?; tests = ?; dependencies { ["badLocalProject"] = ? }; evaluatorSett... + "file:///$snippetsDir/projects/badProjectDeps4/PklProject" + + && it.projectFileUri.endsWith("/PklProject") + │ │ │ │ + │ │ │ true + │ │ "file:///$snippetsDir/projects/badProjectDeps4/PklProject" + │ new ModuleClass { package = ?; tests = ?; dependencies { ["badLocalProject"] = ? }; evaluatorSett... + true + + && it != module + │ │ │ + │ │ false + │ new ModuleClass { package = ?; tests = ?; dependencies { ["badLocalProject"] = ? }; evaluatorSett... + false + + && it.package != null + │ + false + +x | ["badLocalProject"] = import("PklProject") + ^^^^^^^^^^^^^^^^^^^^ at PklProject#dependencies["badLocalProject"] (file:///$snippetsDir/input/projects/badProjectDeps4/PklProject) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/types/typeAliasContext.err b/pkl-core/src/test/files/LanguageSnippetTests/output/types/typeAliasContext.err index e809c982..98b69f0b 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/types/typeAliasContext.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/types/typeAliasContext.err @@ -2,6 +2,10 @@ Type constraint `endsWith(lastName)` violated. Value: "Jimmy Bird" + endsWith(lastName) + │ │ + false "Birdo" + x | typealias Birds = Listing ^^^^^^^^^^^^^^^^^^ at typeAliasContext#res (file:///$snippetsDir/input/types/helpers/originalTypealias.pkl) 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 924e5977..4ccee610 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,10 +88,26 @@ class EvaluateTestsTest { assertThat(res.failures.size).isEqualTo(2) val fail1 = res.failures[0] - assertThat(fail1.message).isEqualTo("1 == 2 (repl:text)") + assertThat(fail1.message) + .isEqualTo( + """ + 1 == 2 (repl:text) + │ + false + """ + .trimIndent() + ) val fail2 = res.failures[1] - assertThat(fail2.message).isEqualTo(""""foo" == "bar" (repl:text)""") + assertThat(fail2.message) + .isEqualTo( + """ + "foo" == "bar" (repl:text) + │ + false + """ + .trimIndent() + ) } @Test diff --git a/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt index 72d03e9a..ee55795a 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -192,7 +192,7 @@ class StackTraceRendererTest { val loop = StackTraceRenderer.StackFrameLoop(loopFrames, 1) val frames = listOf(createFrame("bar", 1), createFrame("baz", 2), loop) val formatter = AnsiStringBuilder(false) - renderer.doRender(frames, null, formatter, "", true) + renderer.doRender(frames, null, null, formatter, "", true) val renderedFrames = formatter.toString() assertThat(renderedFrames) .isEqualTo( 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 e088f578..1594e1b3 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -123,7 +123,11 @@ class TestsTest : AbstractTest() { ✔ divide numbers ✘ fail 4 == 9 (file:///file, line x) + │ + false "foo" == "bar" (file:///file, line x) + │ + false examples ✔ user 0 ✘ user 1 @@ -233,14 +237,19 @@ class TestsTest : AbstractTest() { assertThat(report) .isEqualTo( + // language=xml """ - 4 == 9 (file:///file, line x) - "foo" == "bar" (file:///file, line x) + 4 == 9 (file:///file, line x) + │ + false + "foo" == "bar" (file:///file, line x) + │ + false @@ -259,7 +268,7 @@ class TestsTest : AbstractTest() { - + """ .trimIndent() ) diff --git a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java index ff6c74f2..cf76a8b5 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java +++ b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/pkl-parser/src/main/java/org/pkl/parser/Span.java b/pkl-parser/src/main/java/org/pkl/parser/Span.java index 460b2b3b..b7012d58 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/Span.java +++ b/pkl-parser/src/main/java/org/pkl/parser/Span.java @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,10 @@ public record Span(int charIndex, int length) { return charIndex + length - 1; } + public int stopIndexExclusive() { + return charIndex + length; + } + public Span stopSpan() { return new Span(charIndex + length - 1, 1); } @@ -42,4 +46,9 @@ public record Span(int charIndex, int length) { public Span grow(int amount) { return new Span(charIndex, length + amount); } + + /** Tells if {@code other} is entirely within this span. */ + public boolean contains(Span other) { + return charIndex <= other.charIndex && other.charIndex + other.length <= charIndex + length; + } } diff --git a/pkl-parser/src/main/java/org/pkl/parser/Token.java b/pkl-parser/src/main/java/org/pkl/parser/Token.java index b20d8130..d80467b8 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/Token.java +++ b/pkl-parser/src/main/java/org/pkl/parser/Token.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -191,6 +191,35 @@ public enum Token { }; } + public boolean isOperator() { + return switch (this) { + case POW, + STAR, + DIV, + INT_DIV, + MOD, + PLUS, + MINUS, + GT, + GTE, + LT, + LTE, + IS, + AS, + EQUAL, + NOT_EQUAL, + AND, + OR, + PIPE, + COALESCE, + DOT, + QDOT, + LBRACK -> + true; + default -> false; + }; + } + public boolean isAffix() { return switch (this) { case LINE_COMMENT, BLOCK_COMMENT, SEMICOLON -> true; diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/Expr.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/Expr.java index 6b4da467..d9a3ccb8 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/syntax/Expr.java +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/Expr.java @@ -127,7 +127,22 @@ public abstract sealed class Expr extends AbstractNode { } } - public static final class SingleLineStringLiteralExpr extends Expr { + public abstract static sealed class StringLiteralExpr extends Expr + permits SingleLineStringLiteralExpr, MultiLineStringLiteralExpr { + public abstract List getParts(); + + public StringLiteralExpr(Span span, List parts) { + super(span, parts); + } + + public final boolean hasInterpolation() { + var children = children(); + assert children != null; + return children.size() > 1; + } + } + + public static final class SingleLineStringLiteralExpr extends StringLiteralExpr { private final Span startDelimiterSpan; private final Span endDelimiterSpan; @@ -158,7 +173,7 @@ public abstract sealed class Expr extends AbstractNode { } } - public static final class MultiLineStringLiteralExpr extends Expr { + public static final class MultiLineStringLiteralExpr extends StringLiteralExpr { private final Span startDelimiterSpan; private final Span endDelimiterSpan;