From f0449c83303a857e63121c70ef901de246baacca Mon Sep 17 00:00:00 2001 From: Islon Scherer Date: Tue, 3 Feb 2026 09:35:16 +0100 Subject: [PATCH] Add flag to turn power assertions on/off (#1419) --- docs/modules/pkl-cli/pages/index.adoc | 20 ++++++ docs/modules/pkl-gradle/pages/index.adoc | 10 +++ .../partials/gradle-common-properties.adoc | 10 +++ .../org/pkl/cli/commands/EvalCommand.kt | 17 ++++- .../org/pkl/cli/commands/TestCommand.kt | 18 ++++- .../kotlin/org/pkl/cli/CliTestRunnerTest.kt | 70 ++++++++++++++++--- .../org/pkl/commons/cli/CliBaseOptions.kt | 5 +- .../kotlin/org/pkl/commons/cli/CliCommand.kt | 4 +- .../pkl/commons/cli/commands/BaseOptions.kt | 4 +- .../src/main/java/org/pkl/core/Analyzer.java | 5 +- .../java/org/pkl/core/EvaluatorBuilder.java | 18 ++++- .../main/java/org/pkl/core/EvaluatorImpl.java | 8 ++- .../pkl/core/ast/type/TypeConstraintNode.java | 48 ++++++++++--- .../org/pkl/core/ast/type/TypeTestNode.java | 8 ++- .../ast/type/VmTypeMismatchException.java | 6 +- .../java/org/pkl/core/project/Project.java | 23 +++++- .../java/org/pkl/core/repl/ReplServer.java | 3 +- .../org/pkl/core/runtime/PowerAssertions.java | 8 +++ .../org/pkl/core/runtime/StdLibModule.java | 5 +- .../java/org/pkl/core/runtime/TestRunner.java | 49 ++++++++----- .../java/org/pkl/core/runtime/VmContext.java | 12 +++- .../org/pkl/core/runtime/VmLocalContext.java | 31 +++++++- .../org/pkl/core/runtime/VmValueTracker.java | 6 +- .../core/runtime/VmValueTrackerFactory.java | 6 +- .../kotlin/org/pkl/core/EvaluateTestsTest.kt | 2 +- .../pkl/core/LanguageSnippetTestsEngine.kt | 4 +- .../java/org/pkl/gradle/task/BasePklTask.java | 9 ++- .../java/org/pkl/gradle/task/ModulesTask.java | 5 +- .../java/org/pkl/gradle/task/TestTask.java | 3 +- 29 files changed, 345 insertions(+), 72 deletions(-) diff --git a/docs/modules/pkl-cli/pages/index.adoc b/docs/modules/pkl-cli/pages/index.adoc index bdf7b967..75783cb7 100644 --- a/docs/modules/pkl-cli/pages/index.adoc +++ b/docs/modules/pkl-cli/pages/index.adoc @@ -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 <>. [[command-server]] @@ -524,6 +534,16 @@ Force generation of expected examples. + 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 <>. [[command-repl]] diff --git a/docs/modules/pkl-gradle/pages/index.adoc b/docs/modules/pkl-gradle/pages/index.adoc index 9cfcc206..89c11a10 100644 --- a/docs/modules/pkl-gradle/pages/index.adoc +++ b/docs/modules/pkl-gradle/pages/index.adoc @@ -322,6 +322,16 @@ Default: `false` + Whether to ignore expected example files and generate them again. ==== +[[power-assertions-test]] +.powerAssertions: Property +[%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: include::../partials/gradle-modules-properties.adoc[] diff --git a/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc b/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc index 9a95cd43..cf53c996 100644 --- a/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc +++ b/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc @@ -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. 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 +[%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. +==== diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt index 56624110..14dc6fa7 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.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. @@ -74,10 +74,23 @@ class EvalCommand : ModulesCommand(name = "eval", helpLink = helpLink) { private val testMode: Boolean by 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() { val options = CliEvaluatorOptions( - base = baseOptions.baseOptions(modules, projectOptions, testMode = testMode), + base = + baseOptions.baseOptions( + modules, + projectOptions, + testMode = testMode, + powerAssertionsEnabled = powerAssertionsEnabled, + ), outputPath = outputPath, outputFormat = baseOptions.format, moduleOutputSeparator = moduleOutputSeparator, diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt index 4fb5bc16..5fa21d0f 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.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. @@ -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.multiple 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 org.pkl.cli.CliTestRunner 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 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() { CliTestRunner( - options = baseOptions.baseOptions(modules, projectOptions), + options = + baseOptions.baseOptions( + modules, + projectOptions, + powerAssertionsEnabled = powerAssertionsEnabled, + ), testOptions = testOptions.cliTestOptions, ) .run() 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 9287baa1..f377c977 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt @@ -52,7 +52,12 @@ class CliTestRunnerTest { val input = tempDir.resolve("test.pkl").writeString(code).toString() val out = StringWriter() val err = StringWriter() - val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val opts = + CliBaseOptions( + sourceModules = listOf(input.toUri()), + settings = URI("pkl:settings"), + powerAssertionsEnabled = true, + ) val testOpts = CliTestOptions() val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) runner.run() @@ -89,7 +94,12 @@ class CliTestRunnerTest { val input = tempDir.resolve("test.pkl").writeString(code).toString() val out = StringWriter() val err = StringWriter() - val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val opts = + CliBaseOptions( + sourceModules = listOf(input.toUri()), + settings = URI("pkl:settings"), + powerAssertionsEnabled = true, + ) val testOpts = CliTestOptions() val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) assertThatCode { runner.run() }.hasMessage("Tests failed.") @@ -131,7 +141,12 @@ class CliTestRunnerTest { val input = tempDir.resolve("test.pkl").writeString(code).toString() val out = StringWriter() val err = StringWriter() - val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val opts = + CliBaseOptions( + sourceModules = listOf(input.toUri()), + settings = URI("pkl:settings"), + powerAssertionsEnabled = true, + ) val testOpts = CliTestOptions() val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) assertThatCode { runner.run() }.hasMessage("Tests failed.") @@ -173,7 +188,12 @@ class CliTestRunnerTest { val input = tempDir.resolve("test.pkl").writeString(code).toString() val out = StringWriter() val err = StringWriter() - val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val opts = + CliBaseOptions( + sourceModules = listOf(input.toUri()), + settings = URI("pkl:settings"), + powerAssertionsEnabled = true, + ) val testOpts = CliTestOptions() val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) assertThatCode { runner.run() }.hasMessage("Tests failed.") @@ -229,7 +249,12 @@ class CliTestRunnerTest { ) val out = 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 runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) assertThatCode { runner.run() }.hasMessage("Tests failed.") @@ -275,7 +300,12 @@ class CliTestRunnerTest { .trimIndent() val input = tempDir.resolve("test.pkl").writeString(code).toString() 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 runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter) assertThatCode { runner.run() }.hasMessageContaining("failed") @@ -320,7 +350,12 @@ class CliTestRunnerTest { .trimIndent() val input = tempDir.resolve("test.pkl").writeString(code).toString() 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 runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter) assertThatCode { runner.run() }.hasMessageContaining("failed") @@ -474,6 +509,7 @@ class CliTestRunnerTest { CliBaseOptions( sourceModules = listOf(input1.toUri(), input2.toUri()), settings = URI("pkl:settings"), + powerAssertionsEnabled = true, ) val testOpts = CliTestOptions(junitDir = tempDir, junitAggregateReports = true) val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter) @@ -601,7 +637,12 @@ class CliTestRunnerTest { ) val out = 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 runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) assertThatCode { runner.run() }.hasMessage("Tests failed.") @@ -640,7 +681,12 @@ class CliTestRunnerTest { val input = tempDir.resolve("test.pkl").writeString(code).toString() val out = StringWriter() val err = StringWriter() - val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val opts = + CliBaseOptions( + sourceModules = listOf(input.toUri()), + settings = URI("pkl:settings"), + powerAssertionsEnabled = true, + ) val testOpts = CliTestOptions() val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) val exception = assertThrows { runner.run() } @@ -680,7 +726,11 @@ class CliTestRunnerTest { val out = StringWriter() val err = StringWriter() 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 runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) runner.run() diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt index 0c06a826..6fccb3c4 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.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. @@ -152,6 +152,9 @@ data class CliBaseOptions( /** Defines options for the formatting of calls to the trace() method. */ val traceMode: TraceMode? = null, + + /** Whether power assertions are enabled. */ + val powerAssertionsEnabled: Boolean = false, ) { companion object { diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt index fa694b1d..af4836c2 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.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. @@ -102,6 +102,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { cliOptions.timeout, stackFrameTransformer, envVars, + cliOptions.powerAssertionsEnabled, ) } @@ -308,5 +309,6 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { .setTimeout(cliOptions.timeout) .setModuleCacheDir(moduleCacheDir) .setTraceMode(traceMode) + .setPowerAssertionsEnabled(cliOptions.powerAssertionsEnabled) } } diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt index cd735325..0a32dab7 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.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. @@ -316,6 +316,7 @@ class BaseOptions : OptionGroup() { modules: List, projectOptions: ProjectOptions? = null, testMode: Boolean = false, + powerAssertionsEnabled: Boolean = false, ): CliBaseOptions { return CliBaseOptions( sourceModules = modules, @@ -343,6 +344,7 @@ class BaseOptions : OptionGroup() { externalModuleReaders = externalModuleReaders, externalResourceReaders = externalResourceReaders, traceMode = traceMode, + powerAssertionsEnabled = powerAssertionsEnabled, ) } } diff --git a/pkl-core/src/main/java/org/pkl/core/Analyzer.java b/pkl-core/src/main/java/org/pkl/core/Analyzer.java index a5983029..74518098 100644 --- a/pkl-core/src/main/java/org/pkl/core/Analyzer.java +++ b/pkl-core/src/main/java/org/pkl/core/Analyzer.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. @@ -120,7 +120,8 @@ public class Analyzer { ? null : new ProjectDependenciesManager( projectDependencies, moduleResolver, securityManager), - traceMode)); + traceMode, + false)); }); } } diff --git a/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java b/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java index efc284f8..ed1acb65 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.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. @@ -70,6 +70,8 @@ public final class EvaluatorBuilder { private TraceMode traceMode = TraceMode.COMPACT; + private boolean powerAssertionsEnabled = false; + private EvaluatorBuilder() {} /** @@ -468,6 +470,17 @@ public final class EvaluatorBuilder { 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. * @@ -578,6 +591,7 @@ public final class EvaluatorBuilder { moduleCacheDir, dependencies, outputFormat, - traceMode); + traceMode, + powerAssertionsEnabled); } } diff --git a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java index 6d72b79d..cca9ee5d 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.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,7 +86,8 @@ public final class EvaluatorImpl implements Evaluator { @Nullable Path moduleCacheDir, @Nullable DeclaredDependencies projectDependencies, @Nullable String outputFormat, - TraceMode traceMode) { + TraceMode traceMode, + boolean powerAssertions) { securityManager = manager; frameTransformer = transformer; @@ -115,7 +116,8 @@ public final class EvaluatorImpl implements Evaluator { ? null : new ProjectDependenciesManager( projectDependencies, moduleResolver, securityManager), - traceMode)); + traceMode, + powerAssertions)); }); this.timeout = timeout; // NOTE: would probably make sense to share executor between evaluators 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 2f6a7b9b..bc6d44ca 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 @@ -29,6 +29,7 @@ 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.VmLanguage; import org.pkl.core.runtime.VmUtils; @NodeChild(value = "bodyNode", type = ExpressionNode.class) @@ -54,13 +55,26 @@ public abstract class TypeConstraintNode extends PklNode { if (!result) { CompilerDirectives.transferToInterpreterAndInvalidate(); - try (var valueTracker = VmContext.get(this).getValueTrackerFactory().create()) { - getBodyNode().executeGeneric(frame); + var vmContext = VmContext.get(this); + 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( - sourceSection, - frame.getAuxiliarySlot(customThisSlot), - sourceSection, - valueTracker.values()); + sourceSection, frame.getAuxiliarySlot(customThisSlot), sourceSection, null); } } } @@ -76,10 +90,26 @@ public abstract class TypeConstraintNode extends PklNode { var result = applyNode.executeBoolean(function, value); if (!result) { CompilerDirectives.transferToInterpreterAndInvalidate(); - try (var valueTracker = VmContext.get(this).getValueTrackerFactory().create()) { - applyNode.executeBoolean(function, value); + var vmContext = VmContext.get(this); + 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( - sourceSection, value, function.getRootNode().getSourceSection(), valueTracker.values()); + sourceSection, value, function.getRootNode().getSourceSection(), null); } } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java index 4dc889d3..816f95ad 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java @@ -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,6 +20,7 @@ import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmLanguage; import org.pkl.core.util.Nullable; @NodeInfo(shortName = "is") @@ -55,11 +56,16 @@ public final class TypeTestNode extends ExpressionNode { // TODO: throw if typeNode is FunctionTypeNode (it's impossible to check) // https://github.com/apple/pkl/issues/639 Object value = valueNode.executeGeneric(frame); + var localContext = VmLanguage.get(this).localContext.get(); + boolean wasInTypeTest = localContext.isInTypeTest(); + localContext.setInTypeTest(true); try { typeNode.executeEagerly(frame, value); return true; } catch (VmTypeMismatchException e) { return false; + } finally { + localContext.setInTypeTest(wasInTypeTest); } } } 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 b9182acd..8b6e2459 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 @@ -171,13 +171,13 @@ public abstract class VmTypeMismatchException extends ControlFlowException { public static final class Constraint extends VmTypeMismatchException { private final SourceSection constraintBodySourceSection; - private final Map> trackedValues; + private final @Nullable Map> trackedValues; public Constraint( SourceSection sourceSection, Object actualValue, SourceSection constraintBodySourceSection, - Map> trackedValues) { + @Nullable Map> trackedValues) { super(sourceSection, actualValue); this.constraintBodySourceSection = constraintBodySourceSection; this.trackedValues = trackedValues; @@ -194,7 +194,7 @@ public abstract class VmTypeMismatchException extends ControlFlowException { .append(indent) .append("Value: ") .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); - if (!withPowerAssertions) { + if (!withPowerAssertions || trackedValues == null) { return; } builder.append("\n\n"); diff --git a/pkl-core/src/main/java/org/pkl/core/project/Project.java b/pkl-core/src/main/java/org/pkl/core/project/Project.java index 5757b439..7653e914 100644 --- a/pkl-core/src/main/java/org/pkl/core/project/Project.java +++ b/pkl-core/src/main/java/org/pkl/core/project/Project.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. @@ -81,7 +81,8 @@ public final class Project { SecurityManager securityManager, @Nullable java.time.Duration timeout, StackFrameTransformer stackFrameTransformer, - Map envVars) { + Map envVars, + boolean powerAssertionsEnabled) { try (var evaluator = EvaluatorBuilder.unconfigured() .setSecurityManager(securityManager) @@ -92,11 +93,29 @@ public final class Project { .addResourceReader(ResourceReaders.file()) .addEnvironmentVariables(envVars) .setTimeout(timeout) + .setPowerAssertionsEnabled(powerAssertionsEnabled) .build()) { return load(evaluator, ModuleSource.path(path)); } } + /** + * Loads Project data from the given {@link Path}. + * + *

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 envVars) { + return loadFromPath(path, securityManager, timeout, stackFrameTransformer, envVars, false); + } + /** Convenience method to load a project with the default stack frame transformer. */ public static Project loadFromPath( Path path, SecurityManager securityManager, @Nullable java.time.Duration timeout) { 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 8e000924..4cbe5608 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 @@ -122,7 +122,8 @@ public class ReplServer implements AutoCloseable { outputFormat, packageResolver, projectDependenciesManager, - traceMode)); + traceMode, + true)); }); language = languageRef.get(); } 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 index a6901f87..aa1dbd79 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/PowerAssertions.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/PowerAssertions.java @@ -66,6 +66,14 @@ import org.pkl.parser.syntax.StringPart.StringInterpolation; public class 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 Parser parser = new Parser(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/StdLibModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/StdLibModule.java index 689a3bd1..41ac27e2 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/StdLibModule.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/StdLibModule.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. @@ -51,7 +51,8 @@ public abstract class StdLibModule { null, null, null, - TraceMode.COMPACT)); + TraceMode.COMPACT, + false)); var language = VmLanguage.get(null); var moduleKey = ModuleKeys.standardLibrary(uri); var source = VmUtils.loadSource((ResolvedModuleKey) moduleKey); 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 c613dcec..e98ceeea 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 @@ -41,6 +41,7 @@ import org.pkl.core.util.AnsiTheme; import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.MutableBoolean; import org.pkl.core.util.MutableReference; +import org.pkl.core.util.Nullable; /** Runs test results examples and facts. */ public final class TestRunner { @@ -112,14 +113,20 @@ public final class TestRunner { try { var factValue = VmUtils.readMember(listing, idx); if (factValue == Boolean.FALSE) { - try (var valueTracker = valueTrackerFactory.create()) { - listing.cachedValues.clear(); - VmUtils.readMember(listing, idx); + if (PowerAssertions.isEnabled()) { + 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 { var failure = - factFailure( - member.getSourceSection(), - getDisplayUri(member), - valueTracker.values()); + factFailure(member.getSourceSection(), getDisplayUri(member), null); resultBuilder.addFailure(failure); } } else { @@ -408,17 +415,25 @@ public final class TestRunner { } private Failure factFailure( - SourceSection sourceSection, String location, Map> trackedValues) { + SourceSection sourceSection, + String location, + @Nullable Map> trackedValues) { var sb = new AnsiStringBuilder(useColor); - PowerAssertions.render( - sb, - "", - sourceSection, - trackedValues, - (it) -> { - it.append(" "); - appendLocation(it, location); - }); + if (trackedValues != null) { + PowerAssertions.render( + sb, + "", + sourceSection, + trackedValues, + (it) -> { + it.append(" "); + appendLocation(it, location); + }); + } else { + sb.append(sourceSection.getCharacters()); + sb.append(" "); + appendLocation(sb, location); + } return new Failure("Fact Failure", sb.toString()); } 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 4402cbe3..155de1e1 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 @@ -38,7 +38,8 @@ public final class VmContext { private final VmValueTrackerFactory valueTrackerFactory; 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; @@ -59,6 +60,7 @@ public final class VmContext { private final @Nullable PackageResolver packageResolver; private final @Nullable ProjectDependenciesManager projectDependenciesManager; private final TraceMode traceMode; + private final boolean powerAssertions; public Holder( StackFrameTransformer frameTransformer, @@ -73,7 +75,8 @@ public final class VmContext { @Nullable String outputFormat, @Nullable PackageResolver packageResolver, @Nullable ProjectDependenciesManager projectDependenciesManager, - TraceMode traceMode) { + TraceMode traceMode, + boolean powerAssertions) { this.frameTransformer = frameTransformer; this.securityManager = securityManager; @@ -95,6 +98,7 @@ public final class VmContext { this.packageResolver = packageResolver; this.projectDependenciesManager = projectDependenciesManager; this.traceMode = traceMode; + this.powerAssertions = powerAssertions; } } @@ -159,6 +163,10 @@ public final class VmContext { return holder.traceMode; } + public boolean getPowerAssertionsEnabled() { + return holder.powerAssertions; + } + public VmValueTrackerFactory getValueTrackerFactory() { return valueTrackerFactory; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.java index 1ec7551e..1125e863 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.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. @@ -19,6 +19,15 @@ package org.pkl.core.runtime; public class VmLocalContext { 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 void shouldEagerTypecheck(boolean shouldEagerTypecheck) { @@ -28,4 +37,24 @@ public class VmLocalContext { public boolean 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; + } } 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 index 772d2954..a6944fcc 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTracker.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTracker.java @@ -32,10 +32,13 @@ import org.pkl.core.util.Nullable; public final class VmValueTracker implements AutoCloseable { private final EventBinding binding; + private final VmLocalContext localContext; private final Map> values = new IdentityHashMap<>(); - public VmValueTracker(Instrumenter instrumenter) { + public VmValueTracker(Instrumenter instrumenter, VmLocalContext localContext) { + this.localContext = localContext; + localContext.enterTracker(); binding = instrumenter.attachExecutionEventFactory( SourceSectionFilter.newBuilder().tagIs(PklTags.Expression.class).build(), @@ -61,6 +64,7 @@ public final class VmValueTracker implements AutoCloseable { @Override public void close() { + localContext.exitTracker(); 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 index e1114194..235cc2aa 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTrackerFactory.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueTrackerFactory.java @@ -21,13 +21,15 @@ import com.oracle.truffle.api.instrumentation.Instrumenter; public final class VmValueTrackerFactory { private final Instrumenter instrumenter; + private final VmLanguage language; - public VmValueTrackerFactory(Instrumenter instrumenter) { + public VmValueTrackerFactory(Instrumenter instrumenter, VmLanguage language) { this.instrumenter = instrumenter; + this.language = language; } @TruffleBoundary public VmValueTracker create() { - return new VmValueTracker(instrumenter); + return new VmValueTracker(instrumenter, language.localContext.get()); } } 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 4ccee610..18191b45 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt @@ -28,7 +28,7 @@ import org.pkl.core.ModuleSource.* class EvaluateTestsTest { - private val evaluator = Evaluator.preconfigured() + private val evaluator = EvaluatorBuilder.preconfigured().setPowerAssertionsEnabled(true).build() @Test fun `test successful module`() { diff --git a/pkl-core/src/test/kotlin/org/pkl/core/LanguageSnippetTestsEngine.kt b/pkl-core/src/test/kotlin/org/pkl/core/LanguageSnippetTestsEngine.kt index 19e3d9b7..53d2eba1 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/LanguageSnippetTestsEngine.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/LanguageSnippetTestsEngine.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. @@ -178,6 +178,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() { .addCertificates(FileTestUtils.selfSignedCertificate) .buildLazily() ) + .setPowerAssertionsEnabled(true) } override val testClass: KClass<*> = LanguageSnippetTests::class @@ -200,6 +201,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() { null, StackFrameTransformers.empty, mapOf(), + true, // enable power assertions for tests ) securityManager = null applyFromProject(project) diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java index 273bb83a..1a68dbff 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.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. @@ -146,6 +146,10 @@ public abstract class BasePklTask extends DefaultTask { @Optional public abstract MapProperty getHttpRewrites(); + @Input + @Optional + public abstract Property getPowerAssertions(); + /** * There are issues with using native libraries in Gradle plugins. As a workaround for now, make * Truffle use an un-optimized runtime. @@ -202,7 +206,8 @@ public abstract class BasePklTask extends DefaultTask { getHttpRewrites().getOrNull(), Map.of(), Map.of(), - null); + null, + getPowerAssertions().getOrElse(false)); } return cachedOptions; } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java index 878a259c..c4ff215e 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.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. @@ -166,7 +166,8 @@ public abstract class ModulesTask extends BasePklTask { getHttpRewrites().getOrNull(), Map.of(), Map.of(), - null); + null, + getPowerAssertions().getOrElse(false)); } return cachedOptions; } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/TestTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/TestTask.java index 1d73b390..59fdb1b9 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/TestTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/TestTask.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. @@ -41,6 +41,7 @@ public abstract class TestTask extends ModulesTask { public TestTask() { this.getJunitAggregateSuiteName().convention("pkl-tests"); + this.getPowerAssertions().convention(true); } @Override