From 7598cf271df52dff38d5adce19def4f78c1e8ebc Mon Sep 17 00:00:00 2001 From: Jen Basch Date: Thu, 16 Jul 2026 21:07:44 -0700 Subject: [PATCH] Add feature flagging system --- bench/src/jmh/java/org/pkl/core/ListSort.java | 5 +- .../pages/message-passing-api.adoc | 41 ++++++++++++++- .../pkl-cli/partials/cli-common-options.adoc | 14 ++++++ docs/src/test/kotlin/DocSnippetTests.kt | 1 + .../src/main/kotlin/org/pkl/cli/CliRepl.kt | 1 + .../org/pkl/cli/repl/ReplMessagesTest.kt | 3 +- .../org/pkl/commons/cli/CliBaseOptions.kt | 3 ++ .../kotlin/org/pkl/commons/cli/CliCommand.kt | 21 +++++++- .../pkl/commons/cli/commands/BaseOptions.kt | 39 ++++++++++++++- .../org/pkl/commons/cli/CliCommandTest.kt | 5 ++ .../config/java/ConfigEvaluatorBuilder.java | 13 +++++ .../src/main/java/org/pkl/core/Analyzer.java | 8 ++- .../java/org/pkl/core/EvaluatorBuilder.java | 50 ++++++++++++++++++- .../main/java/org/pkl/core/EvaluatorImpl.java | 6 ++- .../main/java/org/pkl/core/FeatureFlag.java | 46 +++++++++++++++++ .../src/main/java/org/pkl/core/Logger.java | 16 ++++-- .../src/main/java/org/pkl/core/Loggers.java | 26 +++++----- .../PklEvaluatorSettings.java | 13 +++-- .../messaging/AbstractMessagePackDecoder.java | 21 ++------ .../messaging/AbstractMessagePackEncoder.java | 6 ++- .../java/org/pkl/core/project/Project.java | 6 ++- .../java/org/pkl/core/repl/ReplServer.java | 6 ++- .../org/pkl/core/runtime/StdLibModule.java | 6 ++- .../java/org/pkl/core/runtime/VmContext.java | 10 +++- .../test/kotlin/org/pkl/core/AnalyzerTest.kt | 4 ++ .../kotlin/org/pkl/core/ReplServerTest.kt | 2 + .../org/pkl/core/project/ProjectTest.kt | 2 + .../java/org/pkl/gradle/task/BasePklTask.java | 7 ++- .../java/org/pkl/gradle/task/ModulesTask.java | 3 +- .../kotlin/org/pkl/server/ClientLogger.kt | 11 ++-- .../src/main/kotlin/org/pkl/server/Server.kt | 7 +++ .../pkl/server/ServerMessagePackDecoder.kt | 15 ++++-- .../pkl/server/ServerMessagePackEncoder.kt | 9 ++++ .../kotlin/org/pkl/server/ServerMessages.kt | 1 + .../org/pkl/server/AbstractServerTest.kt | 2 + .../pkl/server/ServerMessagePackCodecTest.kt | 1 + stdlib/EvaluatorSettings.pkl | 13 +++++ 37 files changed, 374 insertions(+), 69 deletions(-) create mode 100644 pkl-core/src/main/java/org/pkl/core/FeatureFlag.java diff --git a/bench/src/jmh/java/org/pkl/core/ListSort.java b/bench/src/jmh/java/org/pkl/core/ListSort.java index e98aa6859..4b78d0fce 100644 --- a/bench/src/jmh/java/org/pkl/core/ListSort.java +++ b/bench/src/jmh/java/org/pkl/core/ListSort.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. @@ -53,7 +53,8 @@ public class ListSort { IoUtils.getCurrentWorkingDir(), StackFrameTransformers.defaultTransformer, false, - TraceMode.COMPACT); + TraceMode.COMPACT, + Map.of()); private static final List list = new ArrayList<>(100000); static { diff --git a/docs/modules/bindings-specification/pages/message-passing-api.adoc b/docs/modules/bindings-specification/pages/message-passing-api.adoc index c05149b9c..79b067c3d 100644 --- a/docs/modules/bindings-specification/pages/message-passing-api.adoc +++ b/docs/modules/bindings-specification/pages/message-passing-api.adoc @@ -126,6 +126,28 @@ project: Project? /// Added in Pkl 0.26.0. http: Http? +/// Register external module readers. +/// +/// Added in Pkl 0.27.0. +externalModuleReaders: Mapping? + +/// Register external resource readers. +/// +/// Added in Pkl 0.27.0. +externalResourceReaders: Mapping? + +/// Dictates the rendering of calls to the trace() method within Pkl. +/// +/// Added in Pkl 0.30.0. +traceMode: ("compact" | "pretty")? + +/// Feature flags to enable or disable. +/// +/// Specifying a feature flag unknown to the current version of Pkl will result in a warning. +/// +/// Added in Pkl 0.33.0. +featureFlags: Mapping? + class ClientResourceReader { /// The URI scheme this reader is responsible for reading. scheme: String @@ -177,7 +199,7 @@ class Project { projectFileUri: String /// The dependencies of this project. - dependencies: Mapping + dependencies: Mapping } class RemoteDependency { @@ -256,6 +278,23 @@ class Proxy { /// ``` noProxy: Listing(isDistinct) } + +class ExternalReader { + /// The executable to launch as the external reader process. + /// + /// Must be either an absolute path to the executable or an executable name to resolve against the operating system's `$PATH`. + executable: String + + /// Arguments to pass to [executable]. + arguments: Listing? + + /// External reader process working directory. + /// + /// Default value: the working directory of the Pkl process. + /// + /// Added in Pkl 0.32.0. + workingDir: String? +} ---- <1> link:{uri-messagepack-bin}[bin format] diff --git a/docs/modules/pkl-cli/partials/cli-common-options.adoc b/docs/modules/pkl-cli/partials/cli-common-options.adoc index 13d94bcd0..c70893aa2 100644 --- a/docs/modules/pkl-cli/partials/cli-common-options.adoc +++ b/docs/modules/pkl-cli/partials/cli-common-options.adoc @@ -182,3 +182,17 @@ Default: `compact` + Specifies how `trace()` output is formatted. Possible options are `compact` and `pretty`. ==== + +.--feature +[%collapsible] +==== +Default: (none) + +Example: `magic=true` + + +Enable or disable feature flags. + +Available feature flags and their default values are provided in the full command line help. +Specifying a feature flag unknown to the current version of Pkl will result in a warning. + +Providing only the name of a flag is equivalent to setting its value to `true`. +==== diff --git a/docs/src/test/kotlin/DocSnippetTests.kt b/docs/src/test/kotlin/DocSnippetTests.kt index 0e68c38e9..8b12c64ee 100644 --- a/docs/src/test/kotlin/DocSnippetTests.kt +++ b/docs/src/test/kotlin/DocSnippetTests.kt @@ -114,6 +114,7 @@ class DocSnippetTestsEngine : HierarchicalTestEngine? = null, ) { 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 c34611d90..0f9731312 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 @@ -214,6 +214,22 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { cliOptions.traceMode ?: evaluatorSettings?.traceMode ?: TraceMode.COMPACT } + private val featureFlags: Map by lazy { + cliOptions.featureFlags?.asFeatureFlags("cli") + ?: evaluatorSettings?.featureFlags?.asFeatureFlags("PklProject") + ?: emptyMap() + } + + protected fun Map.asFeatureFlags(context: String): Map = + mapNotNull { entry -> + FeatureFlag.parse(entry.key)?.let { + return@mapNotNull it to entry.value + } + logger.warn("Unrecognized feature flag named `${entry.key}`", "pkl:#${context}") + return@mapNotNull null + } + .toMap() + private fun HttpClient.Builder.addDefaultCliCertificates() { val caCertsDir = IoUtils.getPklHomeDir().resolve("cacerts") var certsAdded = false @@ -259,6 +275,8 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { } } + val logger: Logger = Loggers.stdErr() + protected fun moduleKeyFactories(modulePathResolver: ModulePathResolver): List { return buildList { externalModuleReaders.forEach { (key, value) -> @@ -310,10 +328,11 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { .addModuleKeyFactories(moduleKeyFactories(modulePathResolver)) .addResourceReaders(resourceReaders(modulePathResolver)) .setColor(useColor) - .setLogger(Loggers.stdErr()) + .setLogger(logger) .setTimeout(cliOptions.timeout) .setModuleCacheDir(moduleCacheDir) .setTraceMode(traceMode) .setPowerAssertionsEnabled(cliOptions.powerAssertionsEnabled) + .setFeatureFlags(featureFlags) } } 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 359ae10ca..b99a32286 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 @@ -18,6 +18,7 @@ package org.pkl.commons.cli.commands import com.github.ajalt.clikt.completion.CompletionCandidates import com.github.ajalt.clikt.parameters.groups.OptionGroup import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.transform.TransformContext import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.long @@ -27,10 +28,12 @@ import java.net.URI import java.net.URISyntaxException import java.nio.file.Path import java.time.Duration +import java.util.Locale import java.util.regex.Pattern import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliException import org.pkl.commons.shlex +import org.pkl.core.FeatureFlag import org.pkl.core.evaluatorSettings.Color import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader import org.pkl.core.evaluatorSettings.TraceMode @@ -92,7 +95,7 @@ class BaseOptions : OptionGroup() { > { return splitPair(delimiter).convert { val cmd = shlex(it.second) - Pair(it.first, ExternalReader(cmd.first(), cmd.drop(1), null)) + it.first to ExternalReader(cmd.first(), cmd.drop(1), null) } } @@ -350,6 +353,39 @@ class BaseOptions : OptionGroup() { .enum { it.name.lowercase() } .single() + val featureFlags: Map by + option( + names = arrayOf("--feature"), + metavar = "[=]", + help = + "Feature flag enabled state. Omitting is equivalent to true. <${FeatureFlag.entries.joinToString(", ") { "${it.name.lowercase(Locale.ROOT)} (default: ${it.defaultValue()})" }}>", + ) + .convert { + val idx = it.indexOf('=') + return@convert if (idx < 0) it to true + else it.substring(0.. true + "false", + "f", + "0", + "no", + "n", + "off" -> false + else -> fail(context.localization.boolConversionError(value)) + } + } + // hidden option used by native tests private val testPort: Int by option(names = arrayOf("--test-port"), help = "Internal test option", hidden = true) @@ -391,6 +427,7 @@ class BaseOptions : OptionGroup() { externalResourceReaders = externalResourceReaders.ifEmpty { null }, traceMode = traceMode, powerAssertionsEnabled = powerAssertionsEnabled, + featureFlags = featureFlags, ) } } diff --git a/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt b/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt index d6c1ab755..cf8de241c 100644 --- a/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt +++ b/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt @@ -125,6 +125,10 @@ class CliCommandTest { ["foo"] { executable = "foo" } } traceMode = "pretty" + featureFlags { + ["foo"] = true + ["bar"] = false + } } """ .trimIndent() @@ -153,6 +157,7 @@ class CliCommandTest { assertThat(cliTest.myExternalModuleReaders).isEmpty() assertThat(cliTest.myExternalResourceReaders).isEmpty() assertThat(builder.traceMode).isEqualTo(TraceMode.COMPACT) + assertThat(builder.featureFlags).isEmpty() } // hygiene test to ensure new evaluator settings get covered by the above test diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorBuilder.java b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorBuilder.java index a5d1a4a35..9de84f0ad 100644 --- a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorBuilder.java +++ b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorBuilder.java @@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable; import org.pkl.config.java.mapper.ValueMapperBuilder; import org.pkl.core.EvaluatorBuilder; +import org.pkl.core.Logger; import org.pkl.core.SecurityManager; import org.pkl.core.StackFrameTransformer; import org.pkl.core.http.HttpClient; @@ -232,6 +233,18 @@ public ConfigEvaluatorBuilder applyFromProject(Project project) { return this; } + /** + * Sets the project for the evaluator, and applies any settings if set. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public ConfigEvaluatorBuilder applyFromProject(Project project, Logger logger) { + evaluatorBuilder.applyFromProject(project, logger); + return this; + } + /** * Sets an evaluation timeout to be enforced by the {@link ConfigEvaluator}'s {@code evaluate} * methods. 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 b3704ba94..1fd6764fe 100644 --- a/pkl-core/src/main/java/org/pkl/core/Analyzer.java +++ b/pkl-core/src/main/java/org/pkl/core/Analyzer.java @@ -48,6 +48,7 @@ public class Analyzer { private final ModuleResolver moduleResolver; private final HttpClient httpClient; private final TraceMode traceMode; + private final Map featureFlags; public Analyzer( StackFrameTransformer transformer, @@ -57,7 +58,8 @@ public Analyzer( @Nullable Path moduleCacheDir, @Nullable DeclaredDependencies projectDependencies, HttpClient httpClient, - TraceMode traceMode) { + TraceMode traceMode, + Map featureFlags) { this.transformer = transformer; this.color = color; this.securityManager = securityManager; @@ -66,6 +68,7 @@ public Analyzer( this.moduleResolver = new ModuleResolver(moduleKeyFactories); this.httpClient = httpClient; this.traceMode = traceMode; + this.featureFlags = featureFlags; } /** @@ -118,7 +121,8 @@ private Context createContext() { : new ProjectDependenciesManager( projectDependencies, moduleResolver, securityManager), traceMode, - false)); + false, + featureFlags)); }); } } 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 3f1636164..81c0fbf3c 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java @@ -72,6 +72,8 @@ public final class EvaluatorBuilder { private boolean powerAssertionsEnabled = false; + private final Map featureFlags = new HashMap<>(); + private EvaluatorBuilder() {} /** @@ -481,12 +483,44 @@ public boolean getPowerAssertionsEnabled() { return powerAssertionsEnabled; } + /** Adds the given feature flag, overriding any value previously set under the same flag. */ + public EvaluatorBuilder addFeatureFlag(FeatureFlag flag, boolean value) { + featureFlags.put(flag, value); + return this; + } + + /** Adds the given feature flags, overriding any value previously set under the same flags. */ + public EvaluatorBuilder addFeatureFlags(Map flags) { + featureFlags.putAll(flags); + return this; + } + + /** Removes any existing feature flags, then adds the given flags. */ + public EvaluatorBuilder setFeatureFlags(Map flags) { + featureFlags.clear(); + return addFeatureFlags(flags); + } + + /** Returns the current configured feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + /** * Given a project, sets its dependencies, and also applies any evaluator settings if set. * * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. */ public EvaluatorBuilder applyFromProject(Project project) { + return applyFromProject(project, Loggers.noop()); + } + + /** + * Given a project, sets its dependencies, and also applies any evaluator settings if set. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public EvaluatorBuilder applyFromProject(Project project, Logger logger) { this.dependencies = project.getDependencies(); var settings = project.getResolvedEvaluatorSettings(); if (securityManager != null) { @@ -567,6 +601,19 @@ public EvaluatorBuilder applyFromProject(Project project) { setTraceMode(settings.traceMode()); } + if (settings.featureFlags() != null) { + for (var entry : settings.featureFlags().entrySet()) { + var flag = FeatureFlag.parse(entry.getKey()); + if (flag == null) { + logger.warn( + String.format("Unrecognized feature flag named `%s`", entry.getKey()), + "pkl:#PklProject"); + continue; + } + addFeatureFlag(flag, entry.getValue()); + } + } + return this; } @@ -595,6 +642,7 @@ public Evaluator build() { dependencies, outputFormat, traceMode, - powerAssertionsEnabled); + powerAssertionsEnabled, + featureFlags); } } 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 7113d4c6f..9d7f746bc 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java @@ -92,7 +92,8 @@ public EvaluatorImpl( @Nullable DeclaredDependencies projectDependencies, @Nullable String outputFormat, TraceMode traceMode, - boolean powerAssertions) { + boolean powerAssertions, + Map featureFlags) { securityManager = manager; frameTransformer = transformer; @@ -127,7 +128,8 @@ public EvaluatorImpl( : new ProjectDependenciesManager( projectDependencies, moduleResolver, securityManager), traceMode, - powerAssertions)); + powerAssertions, + featureFlags)); }); this.timeout = timeout; // NOTE: would probably make sense to share executor between evaluators diff --git a/pkl-core/src/main/java/org/pkl/core/FeatureFlag.java b/pkl-core/src/main/java/org/pkl/core/FeatureFlag.java new file mode 100644 index 000000000..e14542662 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/FeatureFlag.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core; + +import java.util.Locale; +import org.jspecify.annotations.Nullable; + +public enum FeatureFlag { + ; // no feature flags yet! + + // keep in sync with pkl.EvaluatorSettings#KnownFeatureFlags + + // if the stdlib needs to be eval'd with different flags than the defaults, edit + // prg.pkl.core.runtime.StdLibModule.stdLibFeatureFlags + + FeatureFlag(boolean defaultValue) { + this.defaultValue = defaultValue; + } + + public static @Nullable FeatureFlag parse(String name) { + try { + return FeatureFlag.valueOf(name.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + + private final boolean defaultValue; + + public boolean defaultValue() { + return defaultValue; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Logger.java b/pkl-core/src/main/java/org/pkl/core/Logger.java index e65eb53a4..91df92b3a 100644 --- a/pkl-core/src/main/java/org/pkl/core/Logger.java +++ b/pkl-core/src/main/java/org/pkl/core/Logger.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,18 @@ @SuppressWarnings("unused") public interface Logger { /** Logs the given message on level TRACE. */ - default void trace(String message, StackFrame frame) {} + default void trace(String message, StackFrame frame) { + trace(message, frame.getModuleUri()); + } + + /** Logs the given message on level TRACE. */ + default void trace(String message, String frameUri) {} + + /** Logs the given message on level WARN. */ + default void warn(String message, StackFrame frame) { + warn(message, frame.getModuleUri()); + } /** Logs the given message on level WARN. */ - default void warn(String message, StackFrame frame) {} + default void warn(String message, String frameUri) {} } diff --git a/pkl-core/src/main/java/org/pkl/core/Loggers.java b/pkl-core/src/main/java/org/pkl/core/Loggers.java index 1078ed959..f9f783bcd 100644 --- a/pkl-core/src/main/java/org/pkl/core/Loggers.java +++ b/pkl-core/src/main/java/org/pkl/core/Loggers.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,12 +27,12 @@ private Loggers() {} public static Logger noop() { return new Logger() { @Override - public void trace(String message, StackFrame frame) { + public void trace(String message, String frameUri) { // do nothing } @Override - public void warn(String message, StackFrame frame) { + public void warn(String message, String frameUri) { // do nothing } }; @@ -48,14 +48,14 @@ public static Logger stdErr() { public static Logger stream(PrintStream stream) { return new Logger() { @Override - public void trace(String message, StackFrame frame) { - stream.println(formatMessage("TRACE", message, frame)); + public void trace(String message, String frameUri) { + stream.println(formatMessage("TRACE", message, frameUri)); stream.flush(); } @Override - public void warn(String message, StackFrame frame) { - stream.println(formatMessage("WARN", message, frame)); + public void warn(String message, String frameUri) { + stream.println(formatMessage("WARN", message, frameUri)); stream.flush(); } }; @@ -66,27 +66,27 @@ public void warn(String message, StackFrame frame) { public static Logger writer(PrintWriter writer) { return new Logger() { @Override - public void trace(String message, StackFrame frame) { - writer.println(formatMessage("TRACE", message, frame)); + public void trace(String message, String frameUri) { + writer.println(formatMessage("TRACE", message, frameUri)); writer.flush(); } @Override - public void warn(String message, StackFrame frame) { - writer.println(formatMessage("WARN", message, frame)); + public void warn(String message, String frameUri) { + writer.println(formatMessage("WARN", message, frameUri)); writer.flush(); } }; } - private static String formatMessage(String level, String message, StackFrame frame) { + private static String formatMessage(String level, String message, String frameUri) { return "pkl: " + level + ": " + message + (message.endsWith("\n") ? "" : " ") + "(" - + frame.getModuleUri() + + frameUri + ')'; } } diff --git a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java index 4fdfdecba..0f6fc0486 100644 --- a/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java +++ b/pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java @@ -52,7 +52,8 @@ public record PklEvaluatorSettings( @Nullable Http http, @Nullable Map externalModuleReaders, @Nullable Map externalResourceReaders, - @Nullable TraceMode traceMode) { + @Nullable TraceMode traceMode, + @Nullable Map featureFlags) { /** Initializes a {@link PklEvaluatorSettings} from a raw object representation. */ @SuppressWarnings("unchecked") @@ -102,6 +103,7 @@ public static PklEvaluatorSettings parse(Value input) { var color = (String) pSettings.get("color"); var traceMode = (String) pSettings.get("traceMode"); + var featureFlags = (Map) pSettings.get("featureFlags"); return new PklEvaluatorSettings( (Map) pSettings.get("externalProperties"), @@ -117,7 +119,8 @@ public static PklEvaluatorSettings parse(Value input) { Http.parse((Value) pSettings.get("http")), externalModuleReaders, externalResourceReaders, - traceMode == null ? null : TraceMode.valueOf(traceMode.toUpperCase(Locale.ROOT))); + traceMode == null ? null : TraceMode.valueOf(traceMode.toUpperCase(Locale.ROOT)), + featureFlags); } public record Http( @@ -261,7 +264,8 @@ && arePatternsEqual(allowedResources, that.allowedResources) && Objects.equals(timeout, that.timeout) && Objects.equals(rootDir, that.rootDir) && Objects.equals(http, that.http) - && Objects.equals(traceMode, that.traceMode); + && Objects.equals(traceMode, that.traceMode) + && Objects.equals(featureFlags, that.featureFlags); } private int hashPatterns(@Nullable List patterns) { @@ -287,7 +291,8 @@ public int hashCode() { timeout, rootDir, http, - traceMode); + traceMode, + featureFlags); result = 31 * result + hashPatterns(allowedModules); result = 31 * result + hashPatterns(allowedResources); return result; diff --git a/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackDecoder.java b/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackDecoder.java index a6780ba7c..87be45537 100644 --- a/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackDecoder.java +++ b/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackDecoder.java @@ -166,8 +166,8 @@ protected static long unpackLong(Map map, String key) throws Decod return value.asArrayValue().list().stream().map((it) -> it.asStringValue().asString()).toList(); } - protected static @Nullable Map unpackStringMapOrNull( - Map map, String key) { + protected static @Nullable Map unpackMapOrNull( + Map map, String key, Function mapper) { var value = getNullable(map, key); if (value == null) { return null; @@ -176,8 +176,7 @@ protected static long unpackLong(Map map, String key) throws Decod return value.asMapValue().entrySet().stream() .collect( Collectors.toMap( - (e) -> e.getKey().asStringValue().asString(), - (e) -> e.getValue().asStringValue().asString())); + (e) -> e.getKey().asStringValue().asString(), (e) -> mapper.apply(e.getValue()))); } protected static @Nullable List unpackStringListOrNull( @@ -203,18 +202,4 @@ protected static long unpackLong(Map map, String key) throws Decod } return result; } - - protected static @Nullable Map unpackStringMapOrNull( - Map map, String key, Function, T> mapper) { - var value = getNullable(map, key); - if (value == null) { - return null; - } - - return value.asMapValue().entrySet().stream() - .collect( - Collectors.toMap( - (e) -> e.getKey().asStringValue().asString(), - (e) -> mapper.apply(e.getValue().asMapValue().map()))); - } } diff --git a/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackEncoder.java b/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackEncoder.java index 29633324b..bcfaee2d8 100644 --- a/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackEncoder.java +++ b/pkl-core/src/main/java/org/pkl/core/messaging/AbstractMessagePackEncoder.java @@ -94,7 +94,8 @@ protected void packMapHeader( @Nullable Object valueD, @Nullable Object valueE, @Nullable Object valueF, - @Nullable Object valueG) + @Nullable Object valueG, + @Nullable Object valueH) throws IOException { packer.packMapHeader( size @@ -113,7 +114,8 @@ protected void packMapHeader( + (valueD != null ? 1 : 0) + (valueE != null ? 1 : 0) + (valueF != null ? 1 : 0) - + (valueG != null ? 1 : 0)); + + (valueG != null ? 1 : 0) + + (valueH != null ? 1 : 0)); } protected void packKeyValue(String name, @Nullable Integer value) throws IOException { 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 0c66dd39f..5e8c71476 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 @@ -221,7 +221,8 @@ private static List> findImportCycle(ModuleSource moduleSource) { builder.getModuleCacheDir(), builder.getProjectDependencies(), builder.getHttpClient(), - builder.getTraceMode()); + builder.getTraceMode(), + builder.getFeatureFlags()); var importGraph = analyzer.importGraph(moduleSource.getUri()); var ret = ImportGraphUtils.findImportCycles(importGraph); // we only care about cycles in the same scheme as `moduleSource` @@ -562,7 +563,8 @@ public EvaluatorSettings( null, null, null, - traceMode); + traceMode, + Map.of()); } @Deprecated(forRemoval = true) 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 3d9503919..694b342cc 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 @@ -91,7 +91,8 @@ public ReplServer( Path workingDir, StackFrameTransformer frameTransformer, boolean color, - TraceMode traceMode) { + TraceMode traceMode, + Map featureFlags) { this.workingDir = workingDir; this.securityManager = securityManager; @@ -127,7 +128,8 @@ public ReplServer( packageResolver, projectDependenciesManager, traceMode, - true)); + true, + featureFlags)); }); language = languageRef.get(); } 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 df8bdec3a..c5019e508 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 @@ -21,6 +21,7 @@ import java.net.URI; import java.util.List; import java.util.Map; +import org.pkl.core.FeatureFlag; import org.pkl.core.Loggers; import org.pkl.core.SecurityManagers; import org.pkl.core.StackFrameTransformers; @@ -33,6 +34,8 @@ import org.pkl.core.resource.ResourceReaders; public abstract class StdLibModule { + private static final Map stdLibFeatureFlags = Map.of(); + @TruffleBoundary protected static void loadModule(URI uri, VmTyped instance) { doLoad(uri, instance); @@ -64,7 +67,8 @@ private static void doLoad(URI uri, VmTyped instance) { null, null, TraceMode.COMPACT, - false)); + false, + stdLibFeatureFlags)); 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/VmContext.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java index e267e5aeb..2093be7c3 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 @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.Nullable; +import org.pkl.core.FeatureFlag; import org.pkl.core.Logger; import org.pkl.core.SecurityManager; import org.pkl.core.StackFrameTransformer; @@ -61,6 +62,7 @@ public static final class Holder { private final @Nullable ProjectDependenciesManager projectDependenciesManager; private final TraceMode traceMode; private final boolean powerAssertions; + private final Map featureFlags; public Holder( StackFrameTransformer frameTransformer, @@ -76,7 +78,8 @@ public Holder( @Nullable PackageResolver packageResolver, @Nullable ProjectDependenciesManager projectDependenciesManager, TraceMode traceMode, - boolean powerAssertions) { + boolean powerAssertions, + Map featureFlags) { this.frameTransformer = frameTransformer; this.securityManager = securityManager; @@ -99,6 +102,7 @@ public Holder( this.projectDependenciesManager = projectDependenciesManager; this.traceMode = traceMode; this.powerAssertions = powerAssertions; + this.featureFlags = featureFlags; } } @@ -171,4 +175,8 @@ public boolean getPowerAssertionsEnabled() { public VmValueTrackerFactory getValueTrackerFactory() { return valueTrackerFactory; } + + public boolean getFeatureFlag(FeatureFlag flag) { + return holder.featureFlags.getOrDefault(flag, flag.defaultValue()); + } } diff --git a/pkl-core/src/test/kotlin/org/pkl/core/AnalyzerTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/AnalyzerTest.kt index dc20d9050..e5ab023ea 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/AnalyzerTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/AnalyzerTest.kt @@ -39,6 +39,7 @@ class AnalyzerTest { null, HttpClient.dummyClient(), TraceMode.COMPACT, + emptyMap(), ) @Test @@ -118,6 +119,7 @@ class AnalyzerTest { null, HttpClient.dummyClient(), TraceMode.COMPACT, + emptyMap(), ) PackageServer.populateCacheDir(tempDir.resolve("packages")) val file1 = @@ -194,6 +196,7 @@ class AnalyzerTest { project.dependencies, HttpClient.dummyClient(), TraceMode.COMPACT, + emptyMap(), ) val file1 = tempDir @@ -307,6 +310,7 @@ class AnalyzerTest { project.dependencies, HttpClient.dummyClient(), TraceMode.COMPACT, + emptyMap(), ) val result = analyzer.importGraph(mainPkl.toUri()) val birdUri = URI("projectpackage://localhost:0/birds@1.0.0#/bird.pkl") diff --git a/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt index b76ca3500..c4f4df569 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt @@ -47,6 +47,7 @@ class ReplServerTest { StackFrameTransformers.defaultTransformer, false, TraceMode.COMPACT, + emptyMap(), ) @Test @@ -214,6 +215,7 @@ class ReplServerTest { StackFrameTransformers.defaultTransformer, true, TraceMode.COMPACT, + emptyMap(), ) val responses = server.handleRequest(ReplRequest.Eval("id", "5.ms", false, false)) assertThat(responses).hasSize(1) diff --git a/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt index b37c0d09c..41eb0335d 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt @@ -76,6 +76,7 @@ class ProjectTest { null, null, null, + null, ) val expectedAnnotations = listOf( @@ -201,6 +202,7 @@ class ProjectTest { null, null, null, + null, ) ) } 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 f842e6ed4..d5375a992 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 @@ -179,6 +179,10 @@ public Provider getEvalRootDirPath() { @Nested public abstract MapProperty getExternalResourceReaders(); + @Input + @Optional + public abstract MapProperty getFeatureFlags(); + /** * There are issues with using native libraries in Gradle plugins. As a workaround for now, make * Truffle use an un-optimized runtime. @@ -236,7 +240,8 @@ protected CliBaseOptions getCliBaseOptions() { toExternalReaderMap(getExternalModuleReaders().get().values()), toExternalReaderMap(getExternalResourceReaders().get().values()), null, - getPowerAssertions().getOrElse(false)); + getPowerAssertions().getOrElse(false), + getFeatureFlags().getOrNull()); } @Internal 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 38d64180e..ff2c2e3ba 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 @@ -170,6 +170,7 @@ protected CliBaseOptions getCliBaseOptions() { toExternalReaderMap(getExternalModuleReaders().get().values()), toExternalReaderMap(getExternalResourceReaders().get().values()), null, - getPowerAssertions().getOrElse(false)); + getPowerAssertions().getOrElse(false), + getFeatureFlags().getOrNull()); } } diff --git a/pkl-server/src/main/kotlin/org/pkl/server/ClientLogger.kt b/pkl-server/src/main/kotlin/org/pkl/server/ClientLogger.kt index d60c84fe6..da720249e 100644 --- a/pkl-server/src/main/kotlin/org/pkl/server/ClientLogger.kt +++ b/pkl-server/src/main/kotlin/org/pkl/server/ClientLogger.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. @@ -16,18 +16,17 @@ package org.pkl.server import org.pkl.core.Logger -import org.pkl.core.StackFrame import org.pkl.core.messaging.MessageTransport internal class ClientLogger( private val evaluatorId: Long, private val transport: MessageTransport, ) : Logger { - override fun trace(message: String, frame: StackFrame) { - transport.send(LogMessage(evaluatorId, 0, message, frame.moduleUri)) + override fun trace(message: String, frameUri: String) { + transport.send(LogMessage(evaluatorId, 0, message, frameUri)) } - override fun warn(message: String, frame: StackFrame) { - transport.send(LogMessage(evaluatorId, 1, message, frame.moduleUri)) + override fun warn(message: String, frameUri: String) { + transport.send(LogMessage(evaluatorId, 1, message, frameUri)) } } diff --git a/pkl-server/src/main/kotlin/org/pkl/server/Server.kt b/pkl-server/src/main/kotlin/org/pkl/server/Server.kt index 6a6cc6987..b897fd685 100644 --- a/pkl-server/src/main/kotlin/org/pkl/server/Server.kt +++ b/pkl-server/src/main/kotlin/org/pkl/server/Server.kt @@ -228,6 +228,13 @@ class Server(private val transport: MessageTransport) : AutoCloseable { } outputFormat = message.outputFormat message.traceMode?.let { traceMode = it } + message.featureFlags?.forEach { name, value -> + FeatureFlag.parse(name)?.let { + addFeatureFlag(it, value) + return@forEach + } + logger.warn("Unrecognized feature flag named `${name}`", "pkl:#server") + } build() } } catch (e: IllegalArgumentException) { diff --git a/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackDecoder.kt b/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackDecoder.kt index 5c4fc31e5..26518b33e 100644 --- a/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackDecoder.kt +++ b/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackDecoder.kt @@ -34,6 +34,8 @@ class ServerMessagePackDecoder(unpacker: MessageUnpacker) : BaseMessagePackDecod override fun decodeMessage(msgType: Message.Type, map: Map): Message? { return when (msgType) { Message.Type.CREATE_EVALUATOR_REQUEST -> + // Changes here should be reflected in + // docs/modules/bindings-specification/pages/message-passing-api.adoc CreateEvaluatorRequest( get(map, "requestId").asIntegerValue().asLong(), unpackStringListOrNull(map, "allowedModules"), @@ -41,17 +43,22 @@ class ServerMessagePackDecoder(unpacker: MessageUnpacker) : BaseMessagePackDecod unpackListOrNull(map, "clientModuleReaders") { unpackModuleReaderSpec(it)!! }, unpackListOrNull(map, "clientResourceReaders") { unpackResourceReaderSpec(it)!! }, unpackStringListOrNull(map, "modulePaths", Path::of), - unpackStringMapOrNull(map, "env"), - unpackStringMapOrNull(map, "properties"), + unpackMapOrNull(map, "env") { it.asStringValue().asString() }, + unpackMapOrNull(map, "properties") { it.asStringValue().asString() }, unpackLongOrNull(map, "timeoutSeconds", Duration::ofSeconds), unpackStringOrNull(map, "rootDir", Path::of), unpackStringOrNull(map, "cacheDir", Path::of), unpackStringOrNull(map, "outputFormat"), map.unpackProject(), map.unpackHttp(), - unpackStringMapOrNull(map, "externalModuleReaders", ::unpackExternalReader), - unpackStringMapOrNull(map, "externalResourceReaders", ::unpackExternalReader), + unpackMapOrNull(map, "externalModuleReaders") { + unpackExternalReader(it.asMapValue().map()) + }, + unpackMapOrNull(map, "externalResourceReaders") { + unpackExternalReader(it.asMapValue().map()) + }, unpackStringOrNull(map, "traceMode") { TraceMode.valueOf(it.uppercase()) }, + unpackMapOrNull(map, "featureFlags") { it.asBooleanValue().boolean }, ) Message.Type.CREATE_EVALUATOR_RESPONSE -> CreateEvaluatorResponse( diff --git a/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackEncoder.kt b/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackEncoder.kt index e3dac1fef..03c6f47c1 100644 --- a/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackEncoder.kt +++ b/pkl-server/src/main/kotlin/org/pkl/server/ServerMessagePackEncoder.kt @@ -127,6 +127,7 @@ class ServerMessagePackEncoder(packer: MessagePacker) : BaseMessagePackEncoder(p msg.externalModuleReaders, msg.externalResourceReaders, msg.traceMode, + msg.featureFlags, ) packKeyValue("requestId", msg.requestId()) packKeyValue("allowedModules", msg.allowedModules) @@ -179,6 +180,14 @@ class ServerMessagePackEncoder(packer: MessagePacker) : BaseMessagePackEncoder(p if (msg.traceMode != null) { packKeyValue("traceMode", msg.traceMode.toString()) } + if (msg.featureFlags != null) { + packer.packString("featureFlags") + packer.packMapHeader(msg.featureFlags.size) + for ((flag, value) in msg.featureFlags) { + packer.packString(flag) + packer.packBoolean(value) + } + } return } Message.Type.CREATE_EVALUATOR_RESPONSE -> { diff --git a/pkl-server/src/main/kotlin/org/pkl/server/ServerMessages.kt b/pkl-server/src/main/kotlin/org/pkl/server/ServerMessages.kt index 9b978da65..a1750cdb8 100644 --- a/pkl-server/src/main/kotlin/org/pkl/server/ServerMessages.kt +++ b/pkl-server/src/main/kotlin/org/pkl/server/ServerMessages.kt @@ -42,6 +42,7 @@ data class CreateEvaluatorRequest( val externalModuleReaders: Map?, val externalResourceReaders: Map?, val traceMode: TraceMode?, + val featureFlags: Map?, ) : Message.Client.Request { override fun type(): Message.Type = Message.Type.CREATE_EVALUATOR_REQUEST diff --git a/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt b/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt index bfc8b3b43..12df67299 100644 --- a/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt +++ b/pkl-server/src/test/kotlin/org/pkl/server/AbstractServerTest.kt @@ -93,6 +93,7 @@ abstract class AbstractServerTest { externalModuleReaders = null, externalResourceReaders = null, traceMode = null, + featureFlags = null, ) @Test @@ -1171,6 +1172,7 @@ abstract class AbstractServerTest { null, null, null, + null, ) send(message) diff --git a/pkl-server/src/test/kotlin/org/pkl/server/ServerMessagePackCodecTest.kt b/pkl-server/src/test/kotlin/org/pkl/server/ServerMessagePackCodecTest.kt index 0d4443a04..5ba0e512b 100644 --- a/pkl-server/src/test/kotlin/org/pkl/server/ServerMessagePackCodecTest.kt +++ b/pkl-server/src/test/kotlin/org/pkl/server/ServerMessagePackCodecTest.kt @@ -103,6 +103,7 @@ class ServerMessagePackCodecTest { externalModuleReaders = mapOf("external" to externalReader, "external2" to externalReader), externalResourceReaders = mapOf("external" to externalReader), traceMode = TraceMode.PRETTY, + featureFlags = mapOf("foo" to true, "bar" to false), ) ) } diff --git a/stdlib/EvaluatorSettings.pkl b/stdlib/EvaluatorSettings.pkl index 6ebf5f2a5..a439c252d 100644 --- a/stdlib/EvaluatorSettings.pkl +++ b/stdlib/EvaluatorSettings.pkl @@ -122,6 +122,19 @@ externalResourceReaders: Mapping? @Since { version = "0.30.0" } traceMode: ("compact" | "pretty")? +/// Feature flags controlling Pkl evaluation behavior. +/// +/// Accepted flags and their default values may vary between Pkl releases. +/// Unrecognized flags will result in a warning. +@Since { version = "0.33.0" } +featureFlags: Mapping? + +/// Feature flag names known to the current version of Pkl. +/// +/// Provided for informational purposes and enhanced editor completion for [featureFlags]. +@Since { version = "0.33.0" } +typealias KnownFeatureFlags = nothing + /// These evaluator settings, whose settings are resolved against [enclosingUri] using OS rules for /// [os]. @Since { version = "0.32.0" }