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 extends Tag> 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