Add flag to turn power assertions on/off (#1419)

This commit is contained in:
Islon Scherer
2026-02-03 09:35:16 +01:00
committed by GitHub
parent 7b7b51c0ae
commit f0449c8330
29 changed files with 345 additions and 72 deletions

View File

@@ -454,6 +454,16 @@ output {
---- ----
==== ====
[[power-assertions-eval]]
.--power-assertions, --no-power-assertions
[%collapsible]
====
Default: enabled +
Enable or disable power assertions for detailed assertion failure messages.
When enabled, type constraint failures will show intermediate values in the assertion expression.
Use `--no-power-assertions` to disable this feature if you prefer simpler output or better performance.
====
This command also takes <<common-options, common options>>. This command also takes <<common-options, common options>>.
[[command-server]] [[command-server]]
@@ -524,6 +534,16 @@ Force generation of expected examples. +
The old expected files will be deleted if present. The old expected files will be deleted if present.
==== ====
[[power-assertions-test]]
.--power-assertions, --no-power-assertions
[%collapsible]
====
Default: enabled +
Enable or disable power assertions for detailed assertion failure messages.
When enabled, test failures will show intermediate values in the assertion expression, making it easier to understand why a test failed.
Use `--no-power-assertions` to disable this feature if you prefer simpler output.
====
This command also takes <<common-options, common options>>. This command also takes <<common-options, common options>>.
[[command-repl]] [[command-repl]]

View File

@@ -322,6 +322,16 @@ Default: `false` +
Whether to ignore expected example files and generate them again. Whether to ignore expected example files and generate them again.
==== ====
[[power-assertions-test]]
.powerAssertions: Property<Boolean>
[%collapsible]
====
Default: `true` (for test tasks) +
Example: `powerAssertions = false` +
Enable or disable power assertions for detailed assertion failure messages.
When enabled, test failures will show intermediate values in the assertion expression, making it easier to understand why a test failed.
====
Common properties: Common properties:
include::../partials/gradle-modules-properties.adoc[] include::../partials/gradle-modules-properties.adoc[]

View File

@@ -118,3 +118,13 @@ The left-hand side describes the source prefix, and the right-hand describes the
This option is commonly used to enable package mirroring. This option is commonly used to enable package mirroring.
The above example will rewrite URL `\https://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0` to `\https://my.internal.mirror/pkl-k8s/k8s@1.0.0`. The above example will rewrite URL `\https://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0` to `\https://my.internal.mirror/pkl-k8s/k8s@1.0.0`.
==== ====
.powerAssertions: Property<Boolean>
[%collapsible]
====
Default: `false` (except for test tasks, which default to `true`) +
Example: `powerAssertions = true` +
Enable power assertions for detailed assertion failure messages.
When enabled, type constraint failures will show intermediate values in the assertion expression.
This option has a performance impact when constraint failures occur.
====

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -74,10 +74,23 @@ class EvalCommand : ModulesCommand(name = "eval", helpLink = helpLink) {
private val testMode: Boolean by private val testMode: Boolean by
option(names = arrayOf("--test-mode"), help = "Internal test mode", hidden = true).flag() option(names = arrayOf("--test-mode"), help = "Internal test mode", hidden = true).flag()
private val powerAssertionsEnabled: Boolean by
option(
names = arrayOf("--power-assertions"),
help = "Enable power assertions for detailed assertion failure messages.",
)
.flag("--no-power-assertions", default = true, defaultForHelp = "enabled")
override fun run() { override fun run() {
val options = val options =
CliEvaluatorOptions( CliEvaluatorOptions(
base = baseOptions.baseOptions(modules, projectOptions, testMode = testMode), base =
baseOptions.baseOptions(
modules,
projectOptions,
testMode = testMode,
powerAssertionsEnabled = powerAssertionsEnabled,
),
outputPath = outputPath, outputPath = outputPath,
outputFormat = baseOptions.format, outputFormat = baseOptions.format,
moduleOutputSeparator = moduleOutputSeparator, moduleOutputSeparator = moduleOutputSeparator,

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.convert import com.github.ajalt.clikt.parameters.arguments.convert
import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.groups.provideDelegate import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import java.net.URI import java.net.URI
import org.pkl.cli.CliTestRunner import org.pkl.cli.CliTestRunner
import org.pkl.commons.cli.commands.BaseCommand import org.pkl.commons.cli.commands.BaseCommand
@@ -43,9 +45,21 @@ class TestCommand : BaseCommand(name = "test", helpLink = helpLink) {
private val testOptions by TestOptions() private val testOptions by TestOptions()
private val powerAssertionsEnabled: Boolean by
option(
names = arrayOf("--power-assertions"),
help = "Enable power assertions for detailed assertion failure messages.",
)
.flag("--no-power-assertions", default = true, defaultForHelp = "enabled")
override fun run() { override fun run() {
CliTestRunner( CliTestRunner(
options = baseOptions.baseOptions(modules, projectOptions), options =
baseOptions.baseOptions(
modules,
projectOptions,
powerAssertionsEnabled = powerAssertionsEnabled,
),
testOptions = testOptions.cliTestOptions, testOptions = testOptions.cliTestOptions,
) )
.run() .run()

View File

@@ -52,7 +52,12 @@ class CliTestRunnerTest {
val input = tempDir.resolve("test.pkl").writeString(code).toString() val input = tempDir.resolve("test.pkl").writeString(code).toString()
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
runner.run() runner.run()
@@ -89,7 +94,12 @@ class CliTestRunnerTest {
val input = tempDir.resolve("test.pkl").writeString(code).toString() val input = tempDir.resolve("test.pkl").writeString(code).toString()
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
assertThatCode { runner.run() }.hasMessage("Tests failed.") assertThatCode { runner.run() }.hasMessage("Tests failed.")
@@ -131,7 +141,12 @@ class CliTestRunnerTest {
val input = tempDir.resolve("test.pkl").writeString(code).toString() val input = tempDir.resolve("test.pkl").writeString(code).toString()
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
assertThatCode { runner.run() }.hasMessage("Tests failed.") assertThatCode { runner.run() }.hasMessage("Tests failed.")
@@ -173,7 +188,12 @@ class CliTestRunnerTest {
val input = tempDir.resolve("test.pkl").writeString(code).toString() val input = tempDir.resolve("test.pkl").writeString(code).toString()
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
assertThatCode { runner.run() }.hasMessage("Tests failed.") assertThatCode { runner.run() }.hasMessage("Tests failed.")
@@ -229,7 +249,12 @@ class CliTestRunnerTest {
) )
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
assertThatCode { runner.run() }.hasMessage("Tests failed.") assertThatCode { runner.run() }.hasMessage("Tests failed.")
@@ -275,7 +300,12 @@ class CliTestRunnerTest {
.trimIndent() .trimIndent()
val input = tempDir.resolve("test.pkl").writeString(code).toString() val input = tempDir.resolve("test.pkl").writeString(code).toString()
val noopWriter = noopWriter() val noopWriter = noopWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions(junitDir = tempDir) val testOpts = CliTestOptions(junitDir = tempDir)
val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter) val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter)
assertThatCode { runner.run() }.hasMessageContaining("failed") assertThatCode { runner.run() }.hasMessageContaining("failed")
@@ -320,7 +350,12 @@ class CliTestRunnerTest {
.trimIndent() .trimIndent()
val input = tempDir.resolve("test.pkl").writeString(code).toString() val input = tempDir.resolve("test.pkl").writeString(code).toString()
val noopWriter = noopWriter() val noopWriter = noopWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions(junitDir = tempDir) val testOpts = CliTestOptions(junitDir = tempDir)
val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter) val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter)
assertThatCode { runner.run() }.hasMessageContaining("failed") assertThatCode { runner.run() }.hasMessageContaining("failed")
@@ -474,6 +509,7 @@ class CliTestRunnerTest {
CliBaseOptions( CliBaseOptions(
sourceModules = listOf(input1.toUri(), input2.toUri()), sourceModules = listOf(input1.toUri(), input2.toUri()),
settings = URI("pkl:settings"), settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
) )
val testOpts = CliTestOptions(junitDir = tempDir, junitAggregateReports = true) val testOpts = CliTestOptions(junitDir = tempDir, junitAggregateReports = true)
val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter) val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter)
@@ -601,7 +637,12 @@ class CliTestRunnerTest {
) )
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
assertThatCode { runner.run() }.hasMessage("Tests failed.") assertThatCode { runner.run() }.hasMessage("Tests failed.")
@@ -640,7 +681,12 @@ class CliTestRunnerTest {
val input = tempDir.resolve("test.pkl").writeString(code).toString() val input = tempDir.resolve("test.pkl").writeString(code).toString()
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) val opts =
CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
val exception = assertThrows<CliException> { runner.run() } val exception = assertThrows<CliException> { runner.run() }
@@ -680,7 +726,11 @@ class CliTestRunnerTest {
val out = StringWriter() val out = StringWriter()
val err = StringWriter() val err = StringWriter()
val opts = val opts =
CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) CliBaseOptions(
sourceModules = listOf(input.toUri()),
settings = URI("pkl:settings"),
powerAssertionsEnabled = true,
)
val testOpts = CliTestOptions() val testOpts = CliTestOptions()
val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err)
runner.run() runner.run()

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -152,6 +152,9 @@ data class CliBaseOptions(
/** Defines options for the formatting of calls to the trace() method. */ /** Defines options for the formatting of calls to the trace() method. */
val traceMode: TraceMode? = null, val traceMode: TraceMode? = null,
/** Whether power assertions are enabled. */
val powerAssertionsEnabled: Boolean = false,
) { ) {
companion object { companion object {

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -102,6 +102,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
cliOptions.timeout, cliOptions.timeout,
stackFrameTransformer, stackFrameTransformer,
envVars, envVars,
cliOptions.powerAssertionsEnabled,
) )
} }
@@ -308,5 +309,6 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
.setTimeout(cliOptions.timeout) .setTimeout(cliOptions.timeout)
.setModuleCacheDir(moduleCacheDir) .setModuleCacheDir(moduleCacheDir)
.setTraceMode(traceMode) .setTraceMode(traceMode)
.setPowerAssertionsEnabled(cliOptions.powerAssertionsEnabled)
} }
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -316,6 +316,7 @@ class BaseOptions : OptionGroup() {
modules: List<URI>, modules: List<URI>,
projectOptions: ProjectOptions? = null, projectOptions: ProjectOptions? = null,
testMode: Boolean = false, testMode: Boolean = false,
powerAssertionsEnabled: Boolean = false,
): CliBaseOptions { ): CliBaseOptions {
return CliBaseOptions( return CliBaseOptions(
sourceModules = modules, sourceModules = modules,
@@ -343,6 +344,7 @@ class BaseOptions : OptionGroup() {
externalModuleReaders = externalModuleReaders, externalModuleReaders = externalModuleReaders,
externalResourceReaders = externalResourceReaders, externalResourceReaders = externalResourceReaders,
traceMode = traceMode, traceMode = traceMode,
powerAssertionsEnabled = powerAssertionsEnabled,
) )
} }
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -120,7 +120,8 @@ public class Analyzer {
? null ? null
: new ProjectDependenciesManager( : new ProjectDependenciesManager(
projectDependencies, moduleResolver, securityManager), projectDependencies, moduleResolver, securityManager),
traceMode)); traceMode,
false));
}); });
} }
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -70,6 +70,8 @@ public final class EvaluatorBuilder {
private TraceMode traceMode = TraceMode.COMPACT; private TraceMode traceMode = TraceMode.COMPACT;
private boolean powerAssertionsEnabled = false;
private EvaluatorBuilder() {} private EvaluatorBuilder() {}
/** /**
@@ -468,6 +470,17 @@ public final class EvaluatorBuilder {
return this.traceMode; return this.traceMode;
} }
/** Sets whether power assertions are enabled. */
public EvaluatorBuilder setPowerAssertionsEnabled(boolean powerAssertions) {
this.powerAssertionsEnabled = powerAssertions;
return this;
}
/** Returns whether power assertions are enabled. */
public boolean getPowerAssertionsEnabled() {
return powerAssertionsEnabled;
}
/** /**
* Given a project, sets its dependencies, and also applies any evaluator settings if set. * Given a project, sets its dependencies, and also applies any evaluator settings if set.
* *
@@ -578,6 +591,7 @@ public final class EvaluatorBuilder {
moduleCacheDir, moduleCacheDir,
dependencies, dependencies,
outputFormat, outputFormat,
traceMode); traceMode,
powerAssertionsEnabled);
} }
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -86,7 +86,8 @@ public final class EvaluatorImpl implements Evaluator {
@Nullable Path moduleCacheDir, @Nullable Path moduleCacheDir,
@Nullable DeclaredDependencies projectDependencies, @Nullable DeclaredDependencies projectDependencies,
@Nullable String outputFormat, @Nullable String outputFormat,
TraceMode traceMode) { TraceMode traceMode,
boolean powerAssertions) {
securityManager = manager; securityManager = manager;
frameTransformer = transformer; frameTransformer = transformer;
@@ -115,7 +116,8 @@ public final class EvaluatorImpl implements Evaluator {
? null ? null
: new ProjectDependenciesManager( : new ProjectDependenciesManager(
projectDependencies, moduleResolver, securityManager), projectDependencies, moduleResolver, securityManager),
traceMode)); traceMode,
powerAssertions));
}); });
this.timeout = timeout; this.timeout = timeout;
// NOTE: would probably make sense to share executor between evaluators // NOTE: would probably make sense to share executor between evaluators

View File

@@ -29,6 +29,7 @@ import org.pkl.core.ast.lambda.ApplyVmFunction1Node;
import org.pkl.core.runtime.BaseModule; import org.pkl.core.runtime.BaseModule;
import org.pkl.core.runtime.VmContext; import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmFunction; import org.pkl.core.runtime.VmFunction;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.runtime.VmUtils; import org.pkl.core.runtime.VmUtils;
@NodeChild(value = "bodyNode", type = ExpressionNode.class) @NodeChild(value = "bodyNode", type = ExpressionNode.class)
@@ -54,13 +55,26 @@ public abstract class TypeConstraintNode extends PklNode {
if (!result) { if (!result) {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
try (var valueTracker = VmContext.get(this).getValueTrackerFactory().create()) { var vmContext = VmContext.get(this);
getBodyNode().executeGeneric(frame); var localContext = VmLanguage.get(this).localContext.get();
// Use power assertions if enabled and not in type test or already instrumenting.
// This prevents `is` checks from triggering instrumentation, but allows them to
// participate if instrumentation is already active.
var usePowerAssertions =
vmContext.getPowerAssertionsEnabled()
&& (!localContext.isInTypeTest() || localContext.hasActiveTracker());
if (usePowerAssertions) {
try (var valueTracker = vmContext.getValueTrackerFactory().create()) {
getBodyNode().executeGeneric(frame);
throw new VmTypeMismatchException.Constraint(
sourceSection,
frame.getAuxiliarySlot(customThisSlot),
sourceSection,
valueTracker.values());
}
} else {
throw new VmTypeMismatchException.Constraint( throw new VmTypeMismatchException.Constraint(
sourceSection, sourceSection, frame.getAuxiliarySlot(customThisSlot), sourceSection, null);
frame.getAuxiliarySlot(customThisSlot),
sourceSection,
valueTracker.values());
} }
} }
} }
@@ -76,10 +90,26 @@ public abstract class TypeConstraintNode extends PklNode {
var result = applyNode.executeBoolean(function, value); var result = applyNode.executeBoolean(function, value);
if (!result) { if (!result) {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
try (var valueTracker = VmContext.get(this).getValueTrackerFactory().create()) { var vmContext = VmContext.get(this);
applyNode.executeBoolean(function, value); var localContext = VmLanguage.get(this).localContext.get();
// Use power assertions if enabled and not in type test or already instrumenting.
// This prevents `is` checks from triggering instrumentation, but allows them to
// participate if instrumentation is already active.
var usePowerAssertions =
vmContext.getPowerAssertionsEnabled()
&& (!localContext.isInTypeTest() || localContext.hasActiveTracker());
if (usePowerAssertions) {
try (var valueTracker = vmContext.getValueTrackerFactory().create()) {
applyNode.executeBoolean(function, value);
throw new VmTypeMismatchException.Constraint(
sourceSection,
value,
function.getRootNode().getSourceSection(),
valueTracker.values());
}
} else {
throw new VmTypeMismatchException.Constraint( throw new VmTypeMismatchException.Constraint(
sourceSection, value, function.getRootNode().getSourceSection(), valueTracker.values()); sourceSection, value, function.getRootNode().getSourceSection(), null);
} }
} }
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@NodeInfo(shortName = "is") @NodeInfo(shortName = "is")
@@ -55,11 +56,16 @@ public final class TypeTestNode extends ExpressionNode {
// TODO: throw if typeNode is FunctionTypeNode (it's impossible to check) // TODO: throw if typeNode is FunctionTypeNode (it's impossible to check)
// https://github.com/apple/pkl/issues/639 // https://github.com/apple/pkl/issues/639
Object value = valueNode.executeGeneric(frame); Object value = valueNode.executeGeneric(frame);
var localContext = VmLanguage.get(this).localContext.get();
boolean wasInTypeTest = localContext.isInTypeTest();
localContext.setInTypeTest(true);
try { try {
typeNode.executeEagerly(frame, value); typeNode.executeEagerly(frame, value);
return true; return true;
} catch (VmTypeMismatchException e) { } catch (VmTypeMismatchException e) {
return false; return false;
} finally {
localContext.setInTypeTest(wasInTypeTest);
} }
} }
} }

View File

@@ -171,13 +171,13 @@ public abstract class VmTypeMismatchException extends ControlFlowException {
public static final class Constraint extends VmTypeMismatchException { public static final class Constraint extends VmTypeMismatchException {
private final SourceSection constraintBodySourceSection; private final SourceSection constraintBodySourceSection;
private final Map<Node, List<Object>> trackedValues; private final @Nullable Map<Node, List<Object>> trackedValues;
public Constraint( public Constraint(
SourceSection sourceSection, SourceSection sourceSection,
Object actualValue, Object actualValue,
SourceSection constraintBodySourceSection, SourceSection constraintBodySourceSection,
Map<Node, List<Object>> trackedValues) { @Nullable Map<Node, List<Object>> trackedValues) {
super(sourceSection, actualValue); super(sourceSection, actualValue);
this.constraintBodySourceSection = constraintBodySourceSection; this.constraintBodySourceSection = constraintBodySourceSection;
this.trackedValues = trackedValues; this.trackedValues = trackedValues;
@@ -194,7 +194,7 @@ public abstract class VmTypeMismatchException extends ControlFlowException {
.append(indent) .append(indent)
.append("Value: ") .append("Value: ")
.append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue));
if (!withPowerAssertions) { if (!withPowerAssertions || trackedValues == null) {
return; return;
} }
builder.append("\n\n"); builder.append("\n\n");

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -81,7 +81,8 @@ public final class Project {
SecurityManager securityManager, SecurityManager securityManager,
@Nullable java.time.Duration timeout, @Nullable java.time.Duration timeout,
StackFrameTransformer stackFrameTransformer, StackFrameTransformer stackFrameTransformer,
Map<String, String> envVars) { Map<String, String> envVars,
boolean powerAssertionsEnabled) {
try (var evaluator = try (var evaluator =
EvaluatorBuilder.unconfigured() EvaluatorBuilder.unconfigured()
.setSecurityManager(securityManager) .setSecurityManager(securityManager)
@@ -92,11 +93,29 @@ public final class Project {
.addResourceReader(ResourceReaders.file()) .addResourceReader(ResourceReaders.file())
.addEnvironmentVariables(envVars) .addEnvironmentVariables(envVars)
.setTimeout(timeout) .setTimeout(timeout)
.setPowerAssertionsEnabled(powerAssertionsEnabled)
.build()) { .build()) {
return load(evaluator, ModuleSource.path(path)); return load(evaluator, ModuleSource.path(path));
} }
} }
/**
* Loads Project data from the given {@link Path}.
*
* <p>Evaluates a module's {@code output.value} to allow for embedding a project within a
* template.
*
* @throws PklException if an error occurred while evaluating the project file.
*/
public static Project loadFromPath(
Path path,
SecurityManager securityManager,
@Nullable java.time.Duration timeout,
StackFrameTransformer stackFrameTransformer,
Map<String, String> envVars) {
return loadFromPath(path, securityManager, timeout, stackFrameTransformer, envVars, false);
}
/** Convenience method to load a project with the default stack frame transformer. */ /** Convenience method to load a project with the default stack frame transformer. */
public static Project loadFromPath( public static Project loadFromPath(
Path path, SecurityManager securityManager, @Nullable java.time.Duration timeout) { Path path, SecurityManager securityManager, @Nullable java.time.Duration timeout) {

View File

@@ -122,7 +122,8 @@ public class ReplServer implements AutoCloseable {
outputFormat, outputFormat,
packageResolver, packageResolver,
projectDependenciesManager, projectDependenciesManager,
traceMode)); traceMode,
true));
}); });
language = languageRef.get(); language = languageRef.get();
} }

View File

@@ -66,6 +66,14 @@ import org.pkl.parser.syntax.StringPart.StringInterpolation;
public class PowerAssertions { public class PowerAssertions {
private PowerAssertions() {} private PowerAssertions() {}
/**
* Power assertions can be enabled/disabled via CLI flags (--power-assertions /
* --no-power-assertions) or via EvaluatorBuilder.setPowerAssertions().
*/
public static boolean isEnabled() {
return VmContext.get(null).getPowerAssertionsEnabled();
}
private static final VmValueRenderer vmValueRenderer = VmValueRenderer.singleLine(100); private static final VmValueRenderer vmValueRenderer = VmValueRenderer.singleLine(100);
private static final Parser parser = new Parser(); private static final Parser parser = new Parser();

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -51,7 +51,8 @@ public abstract class StdLibModule {
null, null,
null, null,
null, null,
TraceMode.COMPACT)); TraceMode.COMPACT,
false));
var language = VmLanguage.get(null); var language = VmLanguage.get(null);
var moduleKey = ModuleKeys.standardLibrary(uri); var moduleKey = ModuleKeys.standardLibrary(uri);
var source = VmUtils.loadSource((ResolvedModuleKey) moduleKey); var source = VmUtils.loadSource((ResolvedModuleKey) moduleKey);

View File

@@ -41,6 +41,7 @@ import org.pkl.core.util.AnsiTheme;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.MutableBoolean; import org.pkl.core.util.MutableBoolean;
import org.pkl.core.util.MutableReference; import org.pkl.core.util.MutableReference;
import org.pkl.core.util.Nullable;
/** Runs test results examples and facts. */ /** Runs test results examples and facts. */
public final class TestRunner { public final class TestRunner {
@@ -112,14 +113,20 @@ public final class TestRunner {
try { try {
var factValue = VmUtils.readMember(listing, idx); var factValue = VmUtils.readMember(listing, idx);
if (factValue == Boolean.FALSE) { if (factValue == Boolean.FALSE) {
try (var valueTracker = valueTrackerFactory.create()) { if (PowerAssertions.isEnabled()) {
listing.cachedValues.clear(); try (var valueTracker = valueTrackerFactory.create()) {
VmUtils.readMember(listing, idx); listing.cachedValues.clear();
VmUtils.readMember(listing, idx);
var failure =
factFailure(
member.getSourceSection(),
getDisplayUri(member),
valueTracker.values());
resultBuilder.addFailure(failure);
}
} else {
var failure = var failure =
factFailure( factFailure(member.getSourceSection(), getDisplayUri(member), null);
member.getSourceSection(),
getDisplayUri(member),
valueTracker.values());
resultBuilder.addFailure(failure); resultBuilder.addFailure(failure);
} }
} else { } else {
@@ -408,17 +415,25 @@ public final class TestRunner {
} }
private Failure factFailure( private Failure factFailure(
SourceSection sourceSection, String location, Map<Node, List<Object>> trackedValues) { SourceSection sourceSection,
String location,
@Nullable Map<Node, List<Object>> trackedValues) {
var sb = new AnsiStringBuilder(useColor); var sb = new AnsiStringBuilder(useColor);
PowerAssertions.render( if (trackedValues != null) {
sb, PowerAssertions.render(
"", sb,
sourceSection, "",
trackedValues, sourceSection,
(it) -> { trackedValues,
it.append(" "); (it) -> {
appendLocation(it, location); it.append(" ");
}); appendLocation(it, location);
});
} else {
sb.append(sourceSection.getCharacters());
sb.append(" ");
appendLocation(sb, location);
}
return new Failure("Fact Failure", sb.toString()); return new Failure("Fact Failure", sb.toString());
} }

View File

@@ -38,7 +38,8 @@ public final class VmContext {
private final VmValueTrackerFactory valueTrackerFactory; private final VmValueTrackerFactory valueTrackerFactory;
public VmContext(VmLanguage vmLanguage, Env env) { public VmContext(VmLanguage vmLanguage, Env env) {
this.valueTrackerFactory = new VmValueTrackerFactory(env.lookup(Instrumenter.class)); this.valueTrackerFactory =
new VmValueTrackerFactory(env.lookup(Instrumenter.class), vmLanguage);
} }
@LateInit private Holder holder; @LateInit private Holder holder;
@@ -59,6 +60,7 @@ public final class VmContext {
private final @Nullable PackageResolver packageResolver; private final @Nullable PackageResolver packageResolver;
private final @Nullable ProjectDependenciesManager projectDependenciesManager; private final @Nullable ProjectDependenciesManager projectDependenciesManager;
private final TraceMode traceMode; private final TraceMode traceMode;
private final boolean powerAssertions;
public Holder( public Holder(
StackFrameTransformer frameTransformer, StackFrameTransformer frameTransformer,
@@ -73,7 +75,8 @@ public final class VmContext {
@Nullable String outputFormat, @Nullable String outputFormat,
@Nullable PackageResolver packageResolver, @Nullable PackageResolver packageResolver,
@Nullable ProjectDependenciesManager projectDependenciesManager, @Nullable ProjectDependenciesManager projectDependenciesManager,
TraceMode traceMode) { TraceMode traceMode,
boolean powerAssertions) {
this.frameTransformer = frameTransformer; this.frameTransformer = frameTransformer;
this.securityManager = securityManager; this.securityManager = securityManager;
@@ -95,6 +98,7 @@ public final class VmContext {
this.packageResolver = packageResolver; this.packageResolver = packageResolver;
this.projectDependenciesManager = projectDependenciesManager; this.projectDependenciesManager = projectDependenciesManager;
this.traceMode = traceMode; this.traceMode = traceMode;
this.powerAssertions = powerAssertions;
} }
} }
@@ -159,6 +163,10 @@ public final class VmContext {
return holder.traceMode; return holder.traceMode;
} }
public boolean getPowerAssertionsEnabled() {
return holder.powerAssertions;
}
public VmValueTrackerFactory getValueTrackerFactory() { public VmValueTrackerFactory getValueTrackerFactory() {
return valueTrackerFactory; return valueTrackerFactory;
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -19,6 +19,15 @@ package org.pkl.core.runtime;
public class VmLocalContext { public class VmLocalContext {
private boolean shouldEagerTypecheck = false; private boolean shouldEagerTypecheck = false;
/** Whether we are currently inside a type test ({@code is} check). */
private boolean inTypeTest = false;
/**
* Number of active {@link VmValueTracker} instances. Used to determine if instrumentation is
* already active.
*/
private int activeTrackerDepth = 0;
public VmLocalContext() {} public VmLocalContext() {}
public void shouldEagerTypecheck(boolean shouldEagerTypecheck) { public void shouldEagerTypecheck(boolean shouldEagerTypecheck) {
@@ -28,4 +37,24 @@ public class VmLocalContext {
public boolean shouldEagerTypecheck() { public boolean shouldEagerTypecheck() {
return this.shouldEagerTypecheck; return this.shouldEagerTypecheck;
} }
public void setInTypeTest(boolean inTypeTest) {
this.inTypeTest = inTypeTest;
}
public boolean isInTypeTest() {
return inTypeTest;
}
public void enterTracker() {
activeTrackerDepth++;
}
public void exitTracker() {
activeTrackerDepth--;
}
public boolean hasActiveTracker() {
return activeTrackerDepth > 0;
}
} }

View File

@@ -32,10 +32,13 @@ import org.pkl.core.util.Nullable;
public final class VmValueTracker implements AutoCloseable { public final class VmValueTracker implements AutoCloseable {
private final EventBinding<ExecutionEventNodeFactory> binding; private final EventBinding<ExecutionEventNodeFactory> binding;
private final VmLocalContext localContext;
private final Map<Node, List<Object>> values = new IdentityHashMap<>(); private final Map<Node, List<Object>> values = new IdentityHashMap<>();
public VmValueTracker(Instrumenter instrumenter) { public VmValueTracker(Instrumenter instrumenter, VmLocalContext localContext) {
this.localContext = localContext;
localContext.enterTracker();
binding = binding =
instrumenter.attachExecutionEventFactory( instrumenter.attachExecutionEventFactory(
SourceSectionFilter.newBuilder().tagIs(PklTags.Expression.class).build(), SourceSectionFilter.newBuilder().tagIs(PklTags.Expression.class).build(),
@@ -61,6 +64,7 @@ public final class VmValueTracker implements AutoCloseable {
@Override @Override
public void close() { public void close() {
localContext.exitTracker();
binding.dispose(); binding.dispose();
} }
} }

View File

@@ -21,13 +21,15 @@ import com.oracle.truffle.api.instrumentation.Instrumenter;
public final class VmValueTrackerFactory { public final class VmValueTrackerFactory {
private final Instrumenter instrumenter; private final Instrumenter instrumenter;
private final VmLanguage language;
public VmValueTrackerFactory(Instrumenter instrumenter) { public VmValueTrackerFactory(Instrumenter instrumenter, VmLanguage language) {
this.instrumenter = instrumenter; this.instrumenter = instrumenter;
this.language = language;
} }
@TruffleBoundary @TruffleBoundary
public VmValueTracker create() { public VmValueTracker create() {
return new VmValueTracker(instrumenter); return new VmValueTracker(instrumenter, language.localContext.get());
} }
} }

View File

@@ -28,7 +28,7 @@ import org.pkl.core.ModuleSource.*
class EvaluateTestsTest { class EvaluateTestsTest {
private val evaluator = Evaluator.preconfigured() private val evaluator = EvaluatorBuilder.preconfigured().setPowerAssertionsEnabled(true).build()
@Test @Test
fun `test successful module`() { fun `test successful module`() {

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -178,6 +178,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
.addCertificates(FileTestUtils.selfSignedCertificate) .addCertificates(FileTestUtils.selfSignedCertificate)
.buildLazily() .buildLazily()
) )
.setPowerAssertionsEnabled(true)
} }
override val testClass: KClass<*> = LanguageSnippetTests::class override val testClass: KClass<*> = LanguageSnippetTests::class
@@ -200,6 +201,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
null, null,
StackFrameTransformers.empty, StackFrameTransformers.empty,
mapOf(), mapOf(),
true, // enable power assertions for tests
) )
securityManager = null securityManager = null
applyFromProject(project) applyFromProject(project)

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -146,6 +146,10 @@ public abstract class BasePklTask extends DefaultTask {
@Optional @Optional
public abstract MapProperty<URI, URI> getHttpRewrites(); public abstract MapProperty<URI, URI> getHttpRewrites();
@Input
@Optional
public abstract Property<Boolean> getPowerAssertions();
/** /**
* There are issues with using native libraries in Gradle plugins. As a workaround for now, make * There are issues with using native libraries in Gradle plugins. As a workaround for now, make
* Truffle use an un-optimized runtime. * Truffle use an un-optimized runtime.
@@ -202,7 +206,8 @@ public abstract class BasePklTask extends DefaultTask {
getHttpRewrites().getOrNull(), getHttpRewrites().getOrNull(),
Map.of(), Map.of(),
Map.of(), Map.of(),
null); null,
getPowerAssertions().getOrElse(false));
} }
return cachedOptions; return cachedOptions;
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -166,7 +166,8 @@ public abstract class ModulesTask extends BasePklTask {
getHttpRewrites().getOrNull(), getHttpRewrites().getOrNull(),
Map.of(), Map.of(),
Map.of(), Map.of(),
null); null,
getPowerAssertions().getOrElse(false));
} }
return cachedOptions; return cachedOptions;
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@ public abstract class TestTask extends ModulesTask {
public TestTask() { public TestTask() {
this.getJunitAggregateSuiteName().convention("pkl-tests"); this.getJunitAggregateSuiteName().convention("pkl-tests");
this.getPowerAssertions().convention(true);
} }
@Override @Override