From f434cbd34012ee3a4f57ecde9047cc13e35b8f32 Mon Sep 17 00:00:00 2001 From: Sasan Hezarkhani Date: Wed, 13 May 2026 17:53:27 -0700 Subject: [PATCH] Add Scoped per-evaluation external properties to config evaluators --- .../java-binding/pages/pkl-config-java.adoc | 5 + .../org/pkl/config/java/ConfigEvaluator.java | 43 +++++- .../pkl/config/java/ConfigEvaluatorImpl.java | 21 +++ .../pkl/config/java/ConfigEvaluatorTest.java | 41 ++++++ .../src/main/java/org/pkl/core/Evaluator.java | 48 +++++++ .../main/java/org/pkl/core/EvaluatorImpl.java | 133 ++++++++++++------ .../org/pkl/core/runtime/ResourceManager.java | 12 +- .../java/org/pkl/core/runtime/VmContext.java | 41 ++++++ 8 files changed, 299 insertions(+), 45 deletions(-) diff --git a/docs/modules/java-binding/pages/pkl-config-java.adoc b/docs/modules/java-binding/pages/pkl-config-java.adoc index bad98738f..78d42292c 100644 --- a/docs/modules/java-binding/pages/pkl-config-java.adoc +++ b/docs/modules/java-binding/pages/pkl-config-java.adoc @@ -141,6 +141,11 @@ Similar methods exist for sets, maps, and other generic types. A `ConfigEvaluator` caches module sources and evaluation results. To clear the cache, for example to evaluate the same module again, close the evaluator and create a new one. +When only external properties need to vary per call, use the `evaluate`, `evaluateOutputValue`, or +`evaluateExpression` overloads that accept a `Map` of external properties. +Those properties override the evaluator's configured external properties for that evaluation only, +and the call does not reuse cached module or resource evaluation results that could contain stale +`read("prop:...")` values. For a ready-to-go example with full source code, see link:{uri-config-java-example}[config-java] in the _pkl-jvm-examples_ repository. diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluator.java b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluator.java index b0f7b37cf..9c51a39be 100644 --- a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluator.java +++ b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluator.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. @@ -15,6 +15,7 @@ */ package org.pkl.config.java; +import java.util.Map; import org.pkl.config.java.mapper.ValueMapper; import org.pkl.core.ModuleSource; @@ -41,12 +42,52 @@ static ConfigEvaluator preconfigured() { /** Evaluates the given module source into a {@link Config} tree. */ Config evaluate(ModuleSource moduleSource); + /** + * Evaluates the given module source into a {@link Config} tree with the given external properties + * overlaid onto the evaluator's configured external properties for this evaluation only. + * + *

To avoid stale {@code read("prop:...")} values, this evaluation does not reuse the + * evaluator's module and resource evaluation caches. + */ + default Config evaluate(ModuleSource moduleSource, Map externalProperties) { + throw new UnsupportedOperationException( + "Per-evaluation external properties are not supported by this evaluator."); + } + /** Evaluates the given module's {@code output.value} property into a {@link Config} tree. */ Config evaluateOutputValue(ModuleSource moduleSource); + /** + * Evaluates the given module's {@code output.value} property into a {@link Config} tree with the + * given external properties overlaid onto the evaluator's configured external properties for this + * evaluation only. + * + *

To avoid stale {@code read("prop:...")} values, this evaluation does not reuse the + * evaluator's module and resource evaluation caches. + */ + default Config evaluateOutputValue( + ModuleSource moduleSource, Map externalProperties) { + throw new UnsupportedOperationException( + "Per-evaluation external properties are not supported by this evaluator."); + } + /** Evaluates the Pkl expression represented as {@code expression} into a {@link Config} tree. */ Config evaluateExpression(ModuleSource moduleSource, String expression); + /** + * Evaluates the Pkl expression represented as {@code expression} into a {@link Config} tree with + * the given external properties overlaid onto the evaluator's configured external properties for + * this evaluation only. + * + *

To avoid stale {@code read("prop:...")} values, this evaluation does not reuse the + * evaluator's module and resource evaluation caches. + */ + default Config evaluateExpression( + ModuleSource moduleSource, String expression, Map externalProperties) { + throw new UnsupportedOperationException( + "Per-evaluation external properties are not supported by this evaluator."); + } + /** * Releases all resources held by this evaluator. If an {@code evaluate} method is currently * executing, this method blocks until cancellation of that execution has completed. diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorImpl.java b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorImpl.java index fe2a9a998..4fa72a28f 100644 --- a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorImpl.java +++ b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorImpl.java @@ -17,6 +17,7 @@ import static org.pkl.config.java.ConfigUtils.createConfig; +import java.util.Map; import org.pkl.config.java.mapper.ValueMapper; import org.pkl.core.Evaluator; import org.pkl.core.ModuleSource; @@ -36,18 +37,38 @@ public Config evaluate(ModuleSource moduleSource) { return new CompositeConfig("", mapper, module); } + @Override + public Config evaluate(ModuleSource moduleSource, Map externalProperties) { + var module = evaluator.evaluate(moduleSource, externalProperties); + return new CompositeConfig("", mapper, module); + } + @Override public Config evaluateOutputValue(ModuleSource moduleSource) { var value = evaluator.evaluateOutputValue(moduleSource); return createConfig(value, mapper); } + @Override + public Config evaluateOutputValue( + ModuleSource moduleSource, Map externalProperties) { + var value = evaluator.evaluateOutputValue(moduleSource, externalProperties); + return createConfig(value, mapper); + } + @Override public Config evaluateExpression(ModuleSource moduleSource, String expression) { var value = evaluator.evaluateExpression(moduleSource, expression); return createConfig(value, mapper); } + @Override + public Config evaluateExpression( + ModuleSource moduleSource, String expression, Map externalProperties) { + var value = evaluator.evaluateExpression(moduleSource, expression, externalProperties); + return createConfig(value, mapper); + } + @Override public ValueMapper getValueMapper() { return mapper; diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java index c1b993806..0911b6410 100644 --- a/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java +++ b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorTest.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.net.URI; +import java.util.Map; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.pkl.core.ModuleSource; @@ -50,4 +52,43 @@ public void evaluateExpression() { var address = addressConfig.as(Address.class); assertThat(address.street).isEqualTo("Fuzzy St."); } + + @Test + public void evaluateWithPerEvaluationExternalProperties() { + var source = + ModuleSource.create( + URI.create("file:///config-evaluator-external-properties.pkl"), + """ + configured = read("prop:configured") + request = read("prop:request") + output { + value { + configured = read("prop:configured") + request = read("prop:request") + } + } + """); + + try (var evaluator = + ConfigEvaluatorBuilder.preconfigured() + .addExternalProperty("configured", "configured") + .addExternalProperty("request", "default") + .build()) { + var first = evaluator.evaluate(source, Map.of("request", "one")); + assertThat(first.get("configured").as(String.class)).isEqualTo("configured"); + assertThat(first.get("request").as(String.class)).isEqualTo("one"); + + var second = evaluator.evaluate(source, Map.of("request", "two")); + assertThat(second.get("request").as(String.class)).isEqualTo("two"); + + var unscoped = evaluator.evaluate(source); + assertThat(unscoped.get("request").as(String.class)).isEqualTo("default"); + + var outputValue = evaluator.evaluateOutputValue(source, Map.of("request", "three")); + assertThat(outputValue.get("request").as(String.class)).isEqualTo("three"); + + var expression = evaluator.evaluateExpression(source, "request", Map.of("request", "four")); + assertThat(expression.as(String.class)).isEqualTo("four"); + } + } } diff --git a/pkl-core/src/main/java/org/pkl/core/Evaluator.java b/pkl-core/src/main/java/org/pkl/core/Evaluator.java index 137a48264..fefaea00a 100644 --- a/pkl-core/src/main/java/org/pkl/core/Evaluator.java +++ b/pkl-core/src/main/java/org/pkl/core/Evaluator.java @@ -47,6 +47,21 @@ static Evaluator preconfigured() { */ PModule evaluate(ModuleSource moduleSource); + /** + * Evaluates the module with the given external properties overlaid onto the evaluator's + * configured external properties for this evaluation only. + * + *

To avoid stale {@code read("prop:...")} values, this evaluation does not reuse the + * evaluator's module and resource evaluation caches. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + default PModule evaluate(ModuleSource moduleSource, Map externalProperties) { + throw new UnsupportedOperationException( + "Per-evaluation external properties are not supported by this evaluator."); + } + /** * Evaluates a module's {@code output.text} property. * @@ -72,6 +87,22 @@ static Evaluator preconfigured() { */ Object evaluateOutputValue(ModuleSource moduleSource); + /** + * Evaluates a module's {@code output.value} property with the given external properties overlaid + * onto the evaluator's configured external properties for this evaluation only. + * + *

To avoid stale {@code read("prop:...")} values, this evaluation does not reuse the + * evaluator's module and resource evaluation caches. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + default Object evaluateOutputValue( + ModuleSource moduleSource, Map externalProperties) { + throw new UnsupportedOperationException( + "Per-evaluation external properties are not supported by this evaluator."); + } + /** * Evaluates a module's {@code output.files} property. * @@ -174,6 +205,23 @@ static Evaluator preconfigured() { */ Object evaluateExpression(ModuleSource moduleSource, String expression); + /** + * Evaluates the Pkl expression represented as {@code expression} with the given external + * properties overlaid onto the evaluator's configured external properties for this evaluation + * only. + * + *

To avoid stale {@code read("prop:...")} values, this evaluation does not reuse the + * evaluator's module and resource evaluation caches. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + default Object evaluateExpression( + ModuleSource moduleSource, String expression, Map externalProperties) { + throw new UnsupportedOperationException( + "Per-evaluation external properties are not supported by this evaluator."); + } + /** * Evaluates the Pkl expression represented as {@code expression}, returning a byte array of the * pkl-binary-encoded representation of the result. 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 a5649a681..382cb1133 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java @@ -134,47 +134,58 @@ public EvaluatorImpl( @Override public PModule evaluate(ModuleSource moduleSource) { - return doEvaluate( - moduleSource, - (module) -> { - module.force(false); - return (PModule) module.export(); - }); + return doEvaluate(moduleSource, this::exportModule); + } + + @Override + public PModule evaluate(ModuleSource moduleSource, Map externalProperties) { + return doEvaluate(moduleSource, externalProperties, this::exportModule); + } + + private PModule exportModule(VmTyped module) { + module.force(false); + return (PModule) module.export(); } @Override public String evaluateOutputText(ModuleSource moduleSource) { - return doEvaluate( - moduleSource, - (module) -> { - var output = VmUtils.readModuleOutput(module); - return VmUtils.readTextProperty(output); - }); + return doEvaluate(moduleSource, this::readModuleOutputText); + } + + private String readModuleOutputText(VmTyped module) { + var output = VmUtils.readModuleOutput(module); + return VmUtils.readTextProperty(output); } public byte[] evaluateOutputBytes(ModuleSource moduleSource) { - return doEvaluate( - moduleSource, - (module) -> { - var output = VmUtils.readModuleOutput(module); - var vmBytes = VmUtils.readBytesProperty(output); - return vmBytes.export(); - }); + return doEvaluate(moduleSource, this::readModuleOutputBytes); + } + + private byte[] readModuleOutputBytes(VmTyped module) { + var output = VmUtils.readModuleOutput(module); + var vmBytes = VmUtils.readBytesProperty(output); + return vmBytes.export(); } @Override public Object evaluateOutputValue(ModuleSource moduleSource) { - return doEvaluate( - moduleSource, - (module) -> { - var output = VmUtils.readModuleOutput(module); - var value = VmUtils.readMember(output, Identifier.VALUE); - if (value instanceof VmValue vmValue) { - vmValue.force(false); - return vmValue.export(); - } - return value; - }); + return doEvaluate(moduleSource, this::readModuleOutputValue); + } + + @Override + public Object evaluateOutputValue( + ModuleSource moduleSource, Map externalProperties) { + return doEvaluate(moduleSource, externalProperties, this::readModuleOutputValue); + } + + private Object readModuleOutputValue(VmTyped module) { + var output = VmUtils.readModuleOutput(module); + var value = VmUtils.readMember(output, Identifier.VALUE); + if (value instanceof VmValue vmValue) { + vmValue.force(false); + return vmValue.export(); + } + return value; } @Override @@ -190,27 +201,44 @@ public Map evaluateOutputFiles(ModuleSource moduleSource) { @Override public Object evaluateExpression(ModuleSource moduleSource, String expression) { + return doEvaluateExpression(moduleSource, expression, null); + } + + @Override + public Object evaluateExpression( + ModuleSource moduleSource, String expression, Map externalProperties) { + return doEvaluateExpression(moduleSource, expression, externalProperties); + } + + private Object doEvaluateExpression( + ModuleSource moduleSource, + String expression, + @Nullable Map externalProperties) { // optimization: if the expression is `output.text`, `output.value` or `output.bytes` (the // common cases), read members directly instead of creating new truffle nodes. return switch (expression) { - case "output.text" -> evaluateOutputText(moduleSource); - case "output.value" -> evaluateOutputValue(moduleSource); - case "output.bytes" -> evaluateOutputBytes(moduleSource); + case "output.text" -> + doEvaluate(moduleSource, externalProperties, this::readModuleOutputText); + case "output.value" -> + doEvaluate(moduleSource, externalProperties, this::readModuleOutputValue); + case "output.bytes" -> + doEvaluate(moduleSource, externalProperties, this::readModuleOutputBytes); default -> doEvaluate( - moduleSource, - (module) -> { - var expressionResult = - VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver); - if (expressionResult instanceof VmValue value) { - value.force(false); - return value.export(); - } - return expressionResult; - }); + moduleSource, externalProperties, (module) -> evaluateExpression(module, expression)); }; } + private Object evaluateExpression(VmTyped module, String expression) { + var expressionResult = + VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver); + if (expressionResult instanceof VmValue value) { + value.force(false); + return value.export(); + } + return expressionResult; + } + private MessageBufferPacker getMessagePacker() { if (messagePacker == null) { messagePacker = MessagePack.newDefaultBufferPacker(); @@ -355,6 +383,10 @@ byte[] evaluateOutputBytes(VmTyped fileOutput) { } private T doEvaluate(Supplier supplier) { + return doEvaluate(null, supplier); + } + + private T doEvaluate(@Nullable Map externalProperties, Supplier supplier) { @Nullable TimeoutTask timeoutTask = null; logger.clear(); if (timeout != null) { @@ -364,6 +396,7 @@ private T doEvaluate(Supplier supplier) { } polyglotContext.enter(); + VmContext.@Nullable EvaluationScope evaluationScope = null; T evalResult; // There is a chance that a timeout is triggered just when evaluation completes on its own. // In this case, if evaluation completed normally or with an expected exception (VmException), @@ -374,6 +407,9 @@ private T doEvaluate(Supplier supplier) { // error, // report that instead of the timeout so as not to swallow a fundamental problem. try { + if (externalProperties != null) { + evaluationScope = VmContext.get(null).enterExternalPropertiesScope(externalProperties); + } evalResult = supplier.get(); } catch (VmStackOverflowException e) { if (VmUtils.isPklBug(e)) { @@ -413,6 +449,9 @@ private T doEvaluate(Supplier supplier) { throw e; } } finally { + if (evaluationScope != null) { + evaluationScope.close(); + } try { polyglotContext.leave(); } catch (IllegalStateException ignored) { @@ -425,7 +464,15 @@ private T doEvaluate(Supplier supplier) { } private T doEvaluate(ModuleSource moduleSource, Function doEvaluate) { + return doEvaluate(moduleSource, null, doEvaluate); + } + + private T doEvaluate( + ModuleSource moduleSource, + @Nullable Map externalProperties, + Function doEvaluate) { return doEvaluate( + externalProperties, () -> { var moduleKey = moduleResolver.resolve(moduleSource); var module = VmLanguage.get(null).loadModule(moduleKey); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java index bd70212f4..14599854f 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.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. @@ -56,6 +56,16 @@ public ResourceManager(SecurityManager securityManager, Collection new VmBytes(resource.bytes())); } + private ResourceManager(ResourceManager other) { + securityManager = other.securityManager; + resourceReaders.putAll(other.resourceReaders); + resourceFactory = other.resourceFactory; + } + + public ResourceManager withEmptyCache() { + return new ResourceManager(this); + } + @TruffleBoundary public ResourceReader getReader(URI resourceUri, Node readNode) { var reader = resourceReaders.get(resourceUri.getScheme()); 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 155de1e15..288797638 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 @@ -36,6 +36,10 @@ public final class VmContext { private static final ContextReference REFERENCE = ContextReference.create(VmLanguage.class); private final VmValueTrackerFactory valueTrackerFactory; + private final ThreadLocal<@Nullable Map> scopedExternalProperties = + new ThreadLocal<>(); + private final ThreadLocal<@Nullable ModuleCache> scopedModuleCache = new ThreadLocal<>(); + private final ThreadLocal<@Nullable ResourceManager> scopedResourceManager = new ThreadLocal<>(); public VmContext(VmLanguage vmLanguage, Env env) { this.valueTrackerFactory = @@ -111,7 +115,40 @@ public void initialize(Holder holder) { this.holder = holder; } + public EvaluationScope enterExternalPropertiesScope(Map externalProperties) { + var previousExternalProperties = scopedExternalProperties.get(); + var previousModuleCache = scopedModuleCache.get(); + var previousResourceManager = scopedResourceManager.get(); + + var props = new HashMap<>(holder.externalProperties); + props.putAll(externalProperties); + scopedExternalProperties.set(Map.copyOf(props)); + scopedModuleCache.set(new ModuleCache()); + scopedResourceManager.set(holder.resourceManager.withEmptyCache()); + + return () -> { + setOrRemove(scopedExternalProperties, previousExternalProperties); + setOrRemove(scopedModuleCache, previousModuleCache); + setOrRemove(scopedResourceManager, previousResourceManager); + }; + } + + private static void setOrRemove(ThreadLocal<@Nullable T> threadLocal, @Nullable T value) { + if (value == null) { + threadLocal.remove(); + } else { + threadLocal.set(value); + } + } + + public interface EvaluationScope extends AutoCloseable { + @Override + void close(); + } + public ModuleCache getModuleCache() { + var moduleCache = scopedModuleCache.get(); + if (moduleCache != null) return moduleCache; return holder.moduleCache; } @@ -136,6 +173,8 @@ public ModuleResolver getModuleResolver() { } public ResourceManager getResourceManager() { + var resourceManager = scopedResourceManager.get(); + if (resourceManager != null) return resourceManager; return holder.resourceManager; } @@ -148,6 +187,8 @@ public Map getEnvironmentVariables() { } public Map getExternalProperties() { + var externalProperties = scopedExternalProperties.get(); + if (externalProperties != null) return externalProperties; return holder.externalProperties; }