SPICE-0025: pkl run CLI framework (#1367)

This commit is contained in:
Jen Basch
2026-02-12 07:53:02 -08:00
committed by GitHub
parent 63a20dd453
commit 72a57af164
35 changed files with 4706 additions and 147 deletions

View File

@@ -0,0 +1,158 @@
/*
* 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.
* 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.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.pkl.core.util.Nullable;
public record CommandSpec(
String name,
@Nullable String helpText,
boolean hidden,
boolean noOp,
Iterable<Option> options,
List<CommandSpec> subcommands,
ApplyFunction apply) {
public sealed interface Option {
String name();
String[] getNames();
class MissingOption extends RuntimeException {
public MissingOption() {}
}
class BadValue extends RuntimeException {
public BadValue(String message) {
super(message);
}
public static BadValue invalid(String value, String type) {
return new BadValue(String.format("%s is not a valid %s", value, type));
}
public static BadValue badKeyValue(String value) {
return new BadValue(String.format("%s is not a valid key=value pair", value));
}
public static BadValue invalidChoice(String value, List<String> choices) {
return new BadValue(
String.format(
"invalid choice: %s. (choose from %s)", value, String.join(", ", choices)));
}
public static BadValue invalidChoice(String value, String choice) {
return new BadValue(String.format("invalid choice: %s. (choose from %s)", value, choice));
}
}
}
public abstract static sealed class CompletionCandidates {
public static final CompletionCandidates PATH = new StaticCompletionCandidates();
public static final class Fixed extends CompletionCandidates {
private final Set<String> values;
public Fixed(Set<String> values) {
this.values = values;
}
public Set<String> getValues() {
return values;
}
}
private static final class StaticCompletionCandidates extends CompletionCandidates {}
}
public record Flag(
String name,
@Nullable String helpText,
boolean showAsRequired,
BiFunction<String, URI, Object> transformEach,
Function<List<Object>, Object> transformAll,
@Nullable CompletionCandidates completionCandidates,
@Nullable String shortName,
String metavar,
boolean hidden,
@Nullable String defaultValue)
implements Option {
@Override
public String[] getNames() {
return shortName == null
? new String[] {"--" + name}
: new String[] {"--" + name, "-" + shortName};
}
}
public record BooleanFlag(
String name,
@Nullable String helpText,
@Nullable String shortName,
boolean hidden,
@Nullable Boolean defaultValue)
implements Option {
@Override
public String[] getNames() {
return shortName == null
? new String[] {"--" + name}
: new String[] {"--" + name, "-" + shortName};
}
}
public record CountedFlag(
String name, @Nullable String helpText, @Nullable String shortName, boolean hidden)
implements Option {
@Override
public String[] getNames() {
return shortName == null
? new String[] {"--" + name}
: new String[] {"--" + name, "-" + shortName};
}
}
public record Argument(
String name,
@Nullable String helpText,
BiFunction<String, URI, Object> transformEach,
Function<List<Object>, Object> transformAll,
@Nullable CompletionCandidates completionCandidates,
boolean repeated)
implements Option {
@Override
public String[] getNames() {
return new String[] {name};
}
}
public interface ApplyFunction {
State apply(Map<String, Object> options, @Nullable State parent);
}
public record State(Object contents, Function<Object, Result> reify) {
public Result evaluate() {
return reify.apply(contents);
}
}
public record Result(byte[] outputBytes, Map<String, FileOutput> outputFiles) {}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.pkl.core;
import java.util.Map;
import java.util.function.Consumer;
import org.pkl.core.runtime.VmEvalException;
/**
@@ -224,6 +225,20 @@ public interface Evaluator extends AutoCloseable {
*/
TestResults evaluateTest(ModuleSource moduleSource, boolean overwrite);
/**
* Parses the command module into a spec that describes the CLI options and subcommands.
*
* <p>This requires that the target module be a {@code "pkl:Command"} instance.
*
* <p>Unlike other evaluator methods, the resulting {@link CommandSpec} must be handled in a
* closure. This is because specs must be applied to parsed CLI options to produce command state
* that is eventually evaluated, which must happen in the active context of an evaluator.
*
* @throws PklException if an error occurs during evaluation
* @throws IllegalStateException if this evaluator has already been closed
*/
void evaluateCommand(ModuleSource moduleSource, Consumer<CommandSpec> run);
/**
* 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.

View File

@@ -20,11 +20,11 @@ import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.graalvm.polyglot.Context;
@@ -40,6 +40,7 @@ import org.pkl.core.packages.PackageResolver;
import org.pkl.core.project.DeclaredDependencies;
import org.pkl.core.resource.ResourceReader;
import org.pkl.core.runtime.BaseModule;
import org.pkl.core.runtime.CommandSpecParser;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.ModuleResolver;
import org.pkl.core.runtime.ResourceManager;
@@ -48,8 +49,6 @@ import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmException;
import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.runtime.VmMapping;
import org.pkl.core.runtime.VmNull;
import org.pkl.core.runtime.VmPklBinaryEncoder;
import org.pkl.core.runtime.VmStackOverflowException;
import org.pkl.core.runtime.VmTyped;
@@ -148,7 +147,7 @@ public final class EvaluatorImpl implements Evaluator {
return doEvaluate(
moduleSource,
(module) -> {
var output = readModuleOutput(module);
var output = VmUtils.readModuleOutput(module);
return VmUtils.readTextProperty(output);
});
}
@@ -157,7 +156,7 @@ public final class EvaluatorImpl implements Evaluator {
return doEvaluate(
moduleSource,
(module) -> {
var output = readModuleOutput(module);
var output = VmUtils.readModuleOutput(module);
var vmBytes = VmUtils.readBytesProperty(output);
return vmBytes.export();
});
@@ -168,7 +167,7 @@ public final class EvaluatorImpl implements Evaluator {
return doEvaluate(
moduleSource,
(module) -> {
var output = readModuleOutput(module);
var output = VmUtils.readModuleOutput(module);
var value = VmUtils.readMember(output, Identifier.VALUE);
if (value instanceof VmValue vmValue) {
vmValue.force(false);
@@ -183,20 +182,9 @@ public final class EvaluatorImpl implements Evaluator {
return doEvaluate(
moduleSource,
(module) -> {
var output = readModuleOutput(module);
var filesOrNull = VmUtils.readMember(output, Identifier.FILES);
if (filesOrNull instanceof VmNull) {
return Map.of();
}
var files = (VmMapping) filesOrNull;
var result = new LinkedHashMap<String, FileOutput>();
files.forceAndIterateMemberValues(
(key, member, value) -> {
assert member.isEntry();
result.put((String) key, new FileOutputImpl(this, (VmTyped) value));
return true;
});
return result;
var output = VmUtils.readModuleOutput(module);
return VmUtils.readFilesProperty(
output, (fileOutput) -> new FileOutputImpl(this, fileOutput));
});
}
@@ -239,10 +227,10 @@ public final class EvaluatorImpl implements Evaluator {
var expressionResult =
switch (expression) {
case "module" -> module;
case "output.text" -> VmUtils.readTextProperty(readModuleOutput(module));
case "output.text" -> VmUtils.readTextProperty(VmUtils.readModuleOutput(module));
case "output.value" ->
VmUtils.readMember(readModuleOutput(module), Identifier.VALUE);
case "output.bytes" -> VmUtils.readBytesProperty(readModuleOutput(module));
VmUtils.readMember(VmUtils.readModuleOutput(module), Identifier.VALUE);
case "output.bytes" -> VmUtils.readBytesProperty(VmUtils.readModuleOutput(module));
default ->
VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver);
};
@@ -289,12 +277,27 @@ public final class EvaluatorImpl implements Evaluator {
});
}
@Override
public void evaluateCommand(ModuleSource moduleSource, Consumer<CommandSpec> run) {
doEvaluate(
moduleSource,
(module) -> {
var commandRunner =
new CommandSpecParser(
moduleResolver,
securityManager,
(fileOutput) -> new FileOutputImpl(this, fileOutput));
run.accept(commandRunner.parse(module));
return null;
});
}
@Override
public <T> T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo<T> classInfo) {
return doEvaluate(
moduleSource,
(module) -> {
var output = readModuleOutput(module);
var output = VmUtils.readModuleOutput(module);
var value = VmUtils.readMember(output, Identifier.VALUE);
var valueClassInfo = VmUtils.getClass(value).getPClassInfo();
if (valueClassInfo.equals(classInfo)) {
@@ -367,6 +370,9 @@ public final class EvaluatorImpl implements Evaluator {
} catch (VmException e) {
handleTimeout(timeoutTask);
throw e.toPklException(frameTransformer, color);
} catch (PklException e) {
// evaluateCommand can throw PklException from the CLI layer, pass them through
throw e;
} catch (Exception e) {
throw new PklBugException(e);
} catch (ExceptionInInitializerError e) {
@@ -420,32 +426,6 @@ public final class EvaluatorImpl implements Evaluator {
"evaluationTimedOut", (timeout.getSeconds() + timeout.getNano() / 1_000_000_000d)));
}
private VmTyped readModuleOutput(VmTyped module) {
var value = VmUtils.readMember(module, Identifier.OUTPUT);
if (value instanceof VmTyped typedOutput
&& typedOutput.getVmClass().getPClassInfo() == PClassInfo.ModuleOutput) {
return typedOutput;
}
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
var builder =
new VmExceptionBuilder()
.evalError(
"invalidModuleOutput",
"output",
PClassInfo.ModuleOutput.getDisplayName(),
VmUtils.getClass(value).getPClassInfo().getDisplayName(),
moduleUri);
var outputMember = module.getMember(Identifier.OUTPUT);
assert outputMember != null;
var uriOfValueMember = outputMember.getSourceSection().getSource().getURI();
// If `output` was explicitly re-assigned, show that in the stack trace.
if (!uriOfValueMember.equals(PClassInfo.pklBaseUri)) {
builder.withSourceSection(outputMember.getBodySection()).withMemberName("output");
}
throw builder.build();
}
private VmException moduleOutputValueTypeMismatch(
VmTyped module, PClassInfo<?> expectedClassInfo, Object value, VmTyped output) {
var moduleUri = module.getModuleInfo().getModuleKey().getUri();

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -146,6 +146,10 @@ public final class VmModifier {
return (modifiers & (LOCAL | EXTERNAL | ABSTRACT)) != 0;
}
public static boolean isLocalOrExternalOrAbstractOrFixedOrConst(int modifiers) {
return (modifiers & (LOCAL | EXTERNAL | ABSTRACT | FIXED | CONST)) != 0;
}
public static boolean isConstOrFixed(int modifiers) {
return (modifiers & (CONST | FIXED)) != 0;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,6 +45,10 @@ public abstract class AmendModuleNode extends SpecializedObjectLiteralNode {
this.moduleInfo = moduleInfo;
}
public ModuleInfo getModuleInfo() {
return moduleInfo;
}
@Override
@TruffleBoundary
protected AmendModuleNode copy(ExpressionNode newParentNode) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.pkl.core.ast.member;
import com.oracle.truffle.api.source.SourceSection;
import java.util.ArrayList;
import java.util.List;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmClass;
@@ -53,6 +54,33 @@ public abstract class ClassMember extends Member {
return annotations;
}
public List<VmTyped> getAllAnnotations(boolean ascending) {
var annotations = new ArrayList<VmTyped>();
if (ascending) {
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
var p = clazz.getDeclaredProperty(getName());
if (p != null) {
annotations.addAll(p.getAnnotations());
}
}
} else {
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
}
return annotations;
}
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
if (clazz.getSuperclass() != null) {
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
}
var p = clazz.getDeclaredProperty(getName());
if (p != null) {
annotations.addAll(p.getAnnotations());
}
}
/** Returns the prototype of the class that declares this member. */
public final VmTyped getOwner() {
return owner;

View File

@@ -16,7 +16,6 @@
package org.pkl.core.ast.member;
import com.oracle.truffle.api.source.SourceSection;
import java.util.ArrayList;
import java.util.List;
import org.pkl.core.Member.SourceLocation;
import org.pkl.core.PClass;
@@ -54,33 +53,6 @@ public final class ClassProperty extends ClassMember {
this.initializer = initializer;
}
public List<VmTyped> getAllAnnotations(boolean ascending) {
var annotations = new ArrayList<VmTyped>();
if (ascending) {
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
var p = clazz.getDeclaredProperty(getName());
if (p != null) {
annotations.addAll(p.getAnnotations());
}
}
} else {
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
}
return annotations;
}
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
if (clazz.getSuperclass() != null) {
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
}
var p = clazz.getDeclaredProperty(getName());
if (p != null) {
annotations.addAll(p.getAnnotations());
}
}
public VmSet getAllModifierMirrors() {
var mods = 0;
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {

View File

@@ -57,6 +57,10 @@ import org.pkl.core.util.Nullable;
public abstract class TypeNode extends PklNode {
public interface ClassTypeNode {
VmClass getVmClass();
}
protected TypeNode(SourceSection sourceSection) {
super(sourceSection);
}
@@ -402,7 +406,8 @@ public abstract class TypeNode extends PklNode {
}
/** The `module` type for a final module. */
public static final class FinalModuleTypeNode extends ObjectSlotTypeNode {
public static final class FinalModuleTypeNode extends ObjectSlotTypeNode
implements ClassTypeNode {
private final VmClass moduleClass;
public FinalModuleTypeNode(SourceSection sourceSection, VmClass moduleClass) {
@@ -456,7 +461,8 @@ public abstract class TypeNode extends PklNode {
}
/** The `module` type for an open module. */
public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode {
public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode
implements ClassTypeNode {
private final VmClass moduleClass; // only used by getVmClass()
@Child private ExpressionNode getModuleNode;
@@ -641,7 +647,7 @@ public abstract class TypeNode extends PklNode {
* String/Boolean/Int/Float and their supertypes, only `VmValue`s can possibly pass its type
* check.
*/
public static final class FinalClassTypeNode extends ObjectSlotTypeNode {
public static final class FinalClassTypeNode extends ObjectSlotTypeNode implements ClassTypeNode {
private final VmClass clazz;
public FinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
@@ -697,7 +703,8 @@ public abstract class TypeNode extends PklNode {
* String/Boolean/Int/Float and their supertypes, only {@link VmValue}s can possibly pass its type
* check.
*/
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode {
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode
implements ClassTypeNode {
protected final VmClass clazz;
public NonFinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
@@ -1088,6 +1095,14 @@ public abstract class TypeNode extends PklNode {
return unionDefault;
}
public Set<String> getStringLiterals() {
return stringLiterals;
}
public @Nullable String getUnionDefault() {
return unionDefault;
}
}
public static final class CollectionTypeNode extends ObjectSlotTypeNode {
@@ -1421,6 +1436,10 @@ public abstract class TypeNode extends PklNode {
return BaseModule.getMapClass();
}
public TypeNode getKeyTypeNode() {
return keyTypeNode;
}
public TypeNode getValueTypeNode() {
return valueTypeNode;
}
@@ -2131,6 +2150,14 @@ public abstract class TypeNode extends PklNode {
protected boolean isParametric() {
return true;
}
public TypeNode getFirstTypeNode() {
return firstTypeNode;
}
public TypeNode getSecondTypeNode() {
return secondTypeNode;
}
}
public static class VarArgsTypeNode extends ObjectSlotTypeNode {
@@ -2313,6 +2340,10 @@ public abstract class TypeNode extends PklNode {
protected final boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
return consumer.accept(this);
}
public long getMask() {
return mask;
}
}
public static final class UIntTypeAliasTypeNode extends IntMaskSlotTypeNode {
@@ -2505,6 +2536,10 @@ public abstract class TypeNode extends PklNode {
aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes);
}
public TypeNode getAliasedTypeNode() {
return aliasedTypeNode;
}
@Override
public FrameSlotKind getFrameSlotKind() {
return aliasedTypeNode.getFrameSlotKind();
@@ -2670,6 +2705,10 @@ public abstract class TypeNode extends PklNode {
this.constraintNodes = constraintNodes;
}
public TypeNode getChildTypeNode() {
return childNode;
}
@Override
public FrameSlotKind getFrameSlotKind() {
return childNode.getFrameSlotKind();

View File

@@ -227,10 +227,26 @@ public final class BaseModule extends StdLibModule {
return MixinTypeAlias.instance;
}
public static VmTypeAlias getUIntTypeAlias() {
return UIntTypeAlias.instance;
}
public static VmTypeAlias getUInt8TypeAlias() {
return UInt8TypeAlias.instance;
}
public static VmTypeAlias getUInt16TypeAlias() {
return UInt16TypeAlias.instance;
}
public static VmTypeAlias getUInt32TypeAlias() {
return UInt32TypeAlias.instance;
}
public static VmTypeAlias getCharTypeAlias() {
return CharTypeAlias.instance;
}
private static final class AnyClass {
static final VmClass instance = loadClass("Any");
}
@@ -403,10 +419,26 @@ public final class BaseModule extends StdLibModule {
static final VmTypeAlias instance = loadTypeAlias("Int32");
}
private static final class UIntTypeAlias {
static final VmTypeAlias instance = loadTypeAlias("UInt");
}
private static final class UInt8TypeAlias {
static final VmTypeAlias instance = loadTypeAlias("UInt8");
}
private static final class UInt16TypeAlias {
static final VmTypeAlias instance = loadTypeAlias("UInt16");
}
private static final class UInt32TypeAlias {
static final VmTypeAlias instance = loadTypeAlias("UInt32");
}
private static final class CharTypeAlias {
static final VmTypeAlias instance = loadTypeAlias("Char");
}
private static final class MixinTypeAlias {
static final VmTypeAlias instance = loadTypeAlias("Mixin");
}

View File

@@ -0,0 +1,95 @@
/*
* 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.
* 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.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.net.URI;
public final class CommandModule extends StdLibModule {
static final VmTyped instance = VmUtils.createEmptyModule();
static {
loadModule(URI.create("pkl:Command"), instance);
}
private CommandModule() {}
public static VmTyped getModule() {
return instance;
}
public static VmClass getCommandInfoClass() {
return CommandInfoClass.instance;
}
public static VmClass getBaseFlagClass() {
return BaseFlagClass.instance;
}
public static VmClass getFlagClass() {
return FlagClass.instance;
}
public static VmClass getBooleanFlagClass() {
return BooleanFlagClass.instance;
}
public static VmClass getCountedFlagClass() {
return CountedFlagClass.instance;
}
public static VmClass getArgumentClass() {
return ArgumentClass.instance;
}
public static VmClass getImportClass() {
return ImportClass.instance;
}
private static final class CommandInfoClass {
static final VmClass instance = loadClass("CommandInfo");
}
private static final class BaseFlagClass {
static final VmClass instance = loadClass("BaseFlag");
}
private static final class FlagClass {
static final VmClass instance = loadClass("Flag");
}
private static final class BooleanFlagClass {
static final VmClass instance = loadClass("BooleanFlag");
}
private static final class CountedFlagClass {
static final VmClass instance = loadClass("CountedFlag");
}
private static final class ArgumentClass {
static final VmClass instance = loadClass("Argument");
}
private static final class ImportClass {
static final VmClass instance = loadClass("Import");
}
@TruffleBoundary
private static VmClass loadClass(String className) {
var theModule = getModule();
return (VmClass) VmUtils.readMember(theModule, Identifier.get(className));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -147,6 +147,22 @@ public final class Identifier implements Comparable<Identifier> {
// members of pkl.yaml
public static final Identifier MAX_COLLECTION_ALIASES = get("maxCollectionAliases");
// members of pkl.Command
public static final Identifier OPTIONS = get("options");
public static final Identifier PARENT = get("parent");
public static final Identifier COMMAND = get("command");
public static final Identifier DESCRIPTION = get("description");
public static final Identifier HIDE = get("hide");
public static final Identifier NOOP = get("noOp");
public static final Identifier SUBCOMMANDS = get("subcommands");
public static final Identifier SHORT_NAME = get("shortName");
public static final Identifier METAVAR = get("metavar");
public static final Identifier MULTIPLE = get("multiple");
public static final Identifier CONVERT = get("convert");
public static final Identifier TRANSFORM_ALL = get("transformAll");
public static final Identifier GLOB = get("glob");
public static final Identifier COMPLETION_CANDIDATES = get("completionCandidates");
// common in lambdas etc
public static final Identifier IT = get("it");

View File

@@ -92,6 +92,8 @@ public final class ModuleCache {
return BaseModule.getModule();
case "Benchmark":
return BenchmarkModule.getModule();
case "Command":
return CommandModule.getModule();
case "jsonnet":
return JsonnetModule.getModule();
case "math":

View File

@@ -69,7 +69,7 @@ public final class TestRunner {
var resultsBuilder = new TestResults.Builder(info.getModuleName(), getDisplayUri(info));
try {
checkAmendsPklTest(testModule);
VmUtils.checkAmends(testModule, TestModule.getModule().getVmClass());
} catch (VmException v) {
var error =
new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, useColor));
@@ -83,17 +83,6 @@ public final class TestRunner {
return resultsBuilder.build();
}
private void checkAmendsPklTest(VmTyped value) {
var testModuleClass = TestModule.getModule().getVmClass();
var moduleClass = value.getVmClass();
while (moduleClass != testModuleClass) {
moduleClass = moduleClass.getSuperclass();
if (moduleClass == null) {
throw new VmExceptionBuilder().typeMismatch(value, testModuleClass).build();
}
}
}
private TestSectionResults runFacts(VmTyped testModule) {
var facts = VmUtils.readMember(testModule, Identifier.FACTS);
if (facts instanceof VmNull) return new TestSectionResults(TestSectionName.FACTS, List.of());

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -30,12 +30,14 @@ import java.net.URI;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.organicdesign.fp.collections.ImMap;
import org.pkl.core.FileOutput;
import org.pkl.core.PClassInfo;
import org.pkl.core.PObject;
import org.pkl.core.SecurityManager;
@@ -179,6 +181,49 @@ public final class VmUtils {
return (VmBytes) VmUtils.readMember(receiver, Identifier.BYTES);
}
public static VmTyped readModuleOutput(VmTyped module) {
var value = VmUtils.readMember(module, Identifier.OUTPUT);
if (value instanceof VmTyped typedOutput
&& typedOutput.getVmClass().getPClassInfo() == PClassInfo.ModuleOutput) {
return typedOutput;
}
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
var builder =
new VmExceptionBuilder()
.evalError(
"invalidModuleOutput",
"output",
PClassInfo.ModuleOutput.getDisplayName(),
VmUtils.getClass(value).getPClassInfo().getDisplayName(),
moduleUri);
var outputMember = module.getMember(Identifier.OUTPUT);
assert outputMember != null;
var uriOfValueMember = outputMember.getSourceSection().getSource().getURI();
// If `output` was explicitly re-assigned, show that in the stack trace.
if (!uriOfValueMember.equals(PClassInfo.pklBaseUri)) {
builder.withSourceSection(outputMember.getBodySection()).withMemberName("output");
}
throw builder.build();
}
public static Map<String, FileOutput> readFilesProperty(
VmObjectLike receiver, Function<VmTyped, FileOutput> fileOutputFactory) {
var filesOrNull = VmUtils.readMember(receiver, Identifier.FILES);
if (filesOrNull instanceof VmNull) {
return Map.of();
}
var files = (VmMapping) filesOrNull;
var result = new LinkedHashMap<String, FileOutput>();
files.forceAndIterateMemberValues(
(key, member, value) -> {
assert member.isEntry();
result.put((String) key, fileOutputFactory.apply((VmTyped) value));
return true;
});
return result;
}
@TruffleBoundary
public static Object readMember(VmObjectLike receiver, Object memberKey) {
var result = readMemberOrNull(receiver, memberKey);
@@ -855,7 +900,7 @@ public final class VmUtils {
String expression,
SecurityManager securityManager,
ModuleResolver moduleResolver) {
var syntheticModule = ModuleKeys.synthetic(URI.create(REPL_TEXT), expression);
var syntheticModule = ModuleKeys.synthetic(REPL_TEXT_URI, expression);
ResolvedModuleKey resolvedModule;
try {
resolvedModule = syntheticModule.resolve(securityManager);
@@ -870,7 +915,7 @@ public final class VmUtils {
source.createSection(0, source.getLength()),
VmUtils.unavailableSourceSection(),
null,
"repl:text",
REPL_TEXT,
syntheticModule,
resolvedModule,
false);
@@ -914,4 +959,20 @@ public final class VmUtils {
public static String concat(String str1, String str2) {
return str1 + str2;
}
/** Check that a value is a VmTyped and that it inherits from the given class */
public static VmTyped checkAmends(Object value, VmClass clazz) {
if (!(value instanceof VmTyped typed)) {
throw new VmExceptionBuilder().typeMismatch(value, clazz).build();
}
return checkAmends(typed, clazz);
}
/** Check that a typed value inherits from the given class */
public static VmTyped checkAmends(VmTyped value, VmClass clazz) {
if (!value.getVmClass().isSubclassOf(clazz)) {
throw new VmExceptionBuilder().typeMismatch(value.getVmClass(), clazz).build();
}
return value;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,14 +51,14 @@ public final class MathNodes {
public abstract static class minInt8 extends ExternalPropertyNode {
@Specialization
protected long eval(VmTyped self) {
return -128;
return Byte.MIN_VALUE;
}
}
public abstract static class maxInt8 extends ExternalPropertyNode {
@Specialization
protected long eval(VmTyped self) {
return 127;
return Byte.MAX_VALUE;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -471,13 +471,15 @@ public final class GlobResolver {
* Resolves a glob expression.
*
* <p>Each pair is the expanded form of the glob pattern, paired with its resolved absolute URI.
*
* <p>globPattern must be absolute if enclosing module details are null.
*/
@TruffleBoundary
public static Map<String, ResolvedGlobElement> resolveGlob(
SecurityManager securityManager,
ReaderBase reader,
ModuleKey enclosingModuleKey,
URI enclosingUri,
@Nullable ModuleKey enclosingModuleKey,
@Nullable URI enclosingUri,
String globPattern)
throws IOException,
SecurityManagerException,
@@ -486,6 +488,10 @@ public final class GlobResolver {
var result = new LinkedHashMap<String, ResolvedGlobElement>();
var hasAbsoluteGlob = globPattern.matches("\\w+:.*");
if ((enclosingModuleKey == null || enclosingUri == null) && !hasAbsoluteGlob) {
throw new PklBugException(
"GlobResolver.resolveGlob() callers must check that the glob pattern is absolute if calling with null enclosing module info");
}
if (reader.hasHierarchicalUris()) {
var splitPattern = splitGlobPatternIntoBaseAndWildcards(reader, globPattern, hasAbsoluteGlob);
@@ -493,7 +499,10 @@ public final class GlobResolver {
var globParts = splitPattern.second;
// short-circuit for glob pattern with no wildcards (can only match 0 or 1 element)
if (globParts.length == 0) {
var resolvedUri = IoUtils.resolve(reader, enclosingUri, globPattern);
var resolvedUri =
enclosingUri == null
? URI.create(globPattern)
: IoUtils.resolve(reader, enclosingUri, globPattern);
if (reader.hasElement(securityManager, resolvedUri)) {
result.put(globPattern, new ResolvedGlobElement(globPattern, resolvedUri, true));
}
@@ -501,7 +510,10 @@ public final class GlobResolver {
}
URI baseUri;
try {
baseUri = IoUtils.resolve(securityManager, enclosingModuleKey, URI.create(basePath));
baseUri =
enclosingModuleKey == null
? URI.create(basePath)
: IoUtils.resolve(securityManager, enclosingModuleKey, URI.create(basePath));
} catch (URISyntaxException e) {
// assertion: this is only thrown if the pattern starts with a triple-dot import.
// the language will throw an error if glob imports is combined with triple-dots.

View File

@@ -1076,3 +1076,54 @@ invalidStringBase64=\
characterCodingException=\
Invalid bytes for charset "{0}".
commandSubcommandConflict=\
Command `{0}` has subcommands with conflicting name "{1}".\n\
Elements of `command.subcommands` must have unique `name` values.
commandMustNotAssignOrAmendProperty=\
Commands must not assign or amend property `{0}`.
commandOptionNoTypeAnnotation=\
No type annotation found for `{0}` property.
commandOptionsTypeNotClass=\
Type annotation `{0}` on `options` property in `pkl:Command` subclass must be a class type.
commandOptionsTypeAbstractClass=\
Command options class `{0}` may not be abstract.
commandOptionBothFlagAndArgument=\
Found both `@Flag` and `@Argument` annotations for options property `{0}`.\n\
\n\
Only one option type may be specified.
commandOptionTypeNullableWithDefaultValue=\
Unexpected option property `{0}` with nullable type and default value.\n\
\n\
Options with default values must not be nullable.
commandOptionUnsupportedType=\
Command option property `{0}` has unsupported {1}type `{2}`.
commandOptionUnexpectedDefaultValue=\
Unexpected default value for `@{1}` property `{0}`.\n\
\n\
{1}s may not specify a default value.
commandArgumentUnexpectedNonRepeatedNullableType=\
Unexpected nullable type for non-collection `@Argument` property `{0}`.\n\
\n\
Arguments may not be nullable.
commandArgumentsMultipleRepeated=\
More than one repeated option annotated with `@Argument` found: `{0}` and `{1}`.\n\
\n\
Only one repeated argument is permitted per command.
commandFlagHelpCollision=\
Flag option `{0}` may not have name "help" or short name "h".
commandFlagInvalidType=\
Option `{0}` with annotation `@{1}` has invalid type `{2}`.\n\
Expected type: `{3}`