Introduces Bytes class (#1019)

This introduces a new `Bytes` standard library class, for working with
binary data.

* Add Bytes class to the standard library
* Change CLI to eval `output.bytes`
* Change code generators to map Bytes to respective underlying type
* Add subscript and concat operator support
* Add binary encoding for Bytes
* Add PCF and Plist rendering for Bytes

Co-authored-by: Kushal Pisavadia <kushi.p@gmail.com>
This commit is contained in:
Daniel Chao
2025-06-11 16:23:55 -07:00
committed by GitHub
parent 3bd8a88506
commit e9320557b7
104 changed files with 2210 additions and 545 deletions
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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,6 +53,15 @@ public interface Evaluator extends AutoCloseable {
*/
String evaluateOutputText(ModuleSource moduleSource);
/**
* Evaluates a module's {@code output.bytes} property.
*
* @throws PklException if an error occurs during evaluation
* @throws IllegalStateException if this evaluator has already been closed
* @since 0.29.0
*/
byte[] evaluateOutputBytes(ModuleSource moduleSource);
/**
* Evaluates a module's {@code output.value} property.
*
@@ -143,6 +152,10 @@ public interface Evaluator extends AutoCloseable {
* <td>TypeAlias</td>
* <td>{@link TypeAlias}</td>
* </tr>
* <tr>
* <td>Bytes</td>
* <td>{@code byte[]}</td>
* </tr>
* </tbody>
* </table>
*
@@ -144,6 +144,16 @@ public class EvaluatorImpl implements Evaluator {
});
}
public byte[] evaluateOutputBytes(ModuleSource moduleSource) {
return doEvaluate(
moduleSource,
(module) -> {
var output = readModuleOutput(module);
var vmBytes = VmUtils.readBytesProperty(output);
return vmBytes.export();
});
}
@Override
public Object evaluateOutputValue(ModuleSource moduleSource) {
return doEvaluate(
@@ -183,32 +193,31 @@ public class EvaluatorImpl implements Evaluator {
@Override
public Object evaluateExpression(ModuleSource moduleSource, String expression) {
// optimization: if the expression is `output.text` or `output.value` (the common cases), read
// members directly instead of creating new truffle nodes.
if (expression.equals("output.text")) {
return evaluateOutputText(moduleSource);
}
if (expression.equals("output.value")) {
return evaluateOutputValue(moduleSource);
}
return doEvaluate(
moduleSource,
(module) -> {
var expressionResult =
VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver);
if (expressionResult instanceof VmValue value) {
value.force(false);
return value.export();
}
return expressionResult;
});
// 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);
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;
});
};
}
@Override
public String evaluateExpressionString(ModuleSource moduleSource, String expression) {
// optimization: if the expression is `output.text` (the common case), read members
// directly
// instead of creating new truffle nodes.
// directly instead of creating new truffle nodes.
if (expression.equals("output.text")) {
return evaluateOutputText(moduleSource);
}
@@ -280,6 +289,10 @@ public class EvaluatorImpl implements Evaluator {
return doEvaluate(() -> VmUtils.readTextProperty(fileOutput));
}
byte[] evaluateOutputBytes(VmTyped fileOutput) {
return doEvaluate(() -> VmUtils.readBytesProperty(fileOutput).export());
}
private <T> T doEvaluate(Supplier<T> supplier) {
@Nullable TimeoutTask timeoutTask = null;
logger.clear();
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,14 @@ public interface FileOutput {
/**
* Returns the text content of this file.
*
* @throws PklException if an error occurs during evaluation
* @throws PklException if an error occurs during evaluation.
*/
String getText();
/**
* Returns the byte contents of this file.
*
* @throws PklException if an error occurs during evaluation.
*/
byte[] getBytes();
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -44,4 +44,16 @@ final class FileOutputImpl implements FileOutput {
throw new PklBugException(e);
}
}
@Override
public byte[] getBytes() {
try {
return evaluator.evaluateOutputBytes(fileOutput);
} catch (PolyglotException e) {
if (e.isCancelled()) {
throw new PklException("The evaluator is no longer available", e);
}
throw new PklBugException(e);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -102,6 +102,12 @@ final class JsonRenderer implements ValueRenderer {
String.format("Values of type `DataSize` cannot be rendered as JSON. Value: %s", value));
}
@Override
public void visitBytes(byte[] value) {
throw new RendererException(
String.format("Values of type `Bytes` cannot be rendered as JSON. Value: %s", value));
}
@Override
public void visitPair(Pair<?, ?> value) {
try {
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -49,6 +49,7 @@ public final class PClassInfo<T> implements Serializable {
public static final PClassInfo<Double> Float = pklBaseClassInfo("Float", double.class);
public static final PClassInfo<Duration> Duration = pklBaseClassInfo("Duration", Duration.class);
public static final PClassInfo<DataSize> DataSize = pklBaseClassInfo("DataSize", DataSize.class);
public static final PClassInfo<byte[]> Bytes = pklBaseClassInfo("Bytes", byte[].class);
public static final PClassInfo<Pair> Pair = pklBaseClassInfo("Pair", Pair.class);
public static final PClassInfo<Void> Collection = pklBaseClassInfo("Collection", Void.class);
public static final PClassInfo<ArrayList> List = pklBaseClassInfo("List", ArrayList.class);
@@ -115,6 +116,7 @@ public final class PClassInfo<T> implements Serializable {
if (value instanceof Set) return (PClassInfo<T>) Set;
if (value instanceof Map) return (PClassInfo<T>) Map;
if (value instanceof Pattern) return (PClassInfo<T>) Regex;
if (value instanceof byte[]) return (PClassInfo<T>) Bytes;
throw new IllegalArgumentException("Not a Pkl value: " + value);
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -21,6 +21,7 @@ import java.io.Writer;
import java.util.*;
import java.util.regex.Pattern;
import org.pkl.core.util.ArrayCharEscaper;
import org.pkl.core.util.ByteArrayUtils;
// To instantiate this class, use ValueRenderers.plist().
final class PListRenderer implements ValueRenderer {
@@ -136,6 +137,13 @@ final class PListRenderer implements ValueRenderer {
value));
}
@Override
public void visitBytes(byte[] value) {
write("<data>");
write(ByteArrayUtils.base64(value));
write("</data>");
}
@Override
public void visitPair(Pair<?, ?> value) {
doVisitIterable(value, false);
@@ -92,6 +92,21 @@ final class PcfRenderer implements ValueRenderer {
write(value.toString());
}
@Override
public void visitBytes(byte[] value) {
write("Bytes(\"");
var isFirst = true;
for (var byt : value) {
if (isFirst) {
isFirst = false;
} else {
write(", ");
}
write(Integer.valueOf(Byte.toUnsignedInt(byt)).toString());
}
write(")");
}
@Override
public void visitPair(Pair<?, ?> value) {
doVisitIterable(value, "Pair(");
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -106,6 +106,13 @@ final class PropertiesRenderer implements ValueRenderer {
"Values of type `DataSize` cannot be rendered as Properties. Value: %s", value));
}
@Override
public String convertBytes(byte[] value) {
throw new RendererException(
String.format(
"Values of type `Bytes` cannot be rendered as Properties. Value: %s", value));
}
@Override
public String convertPair(Pair<?, ?> value) {
throw new RendererException(
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -37,6 +37,8 @@ public interface ValueConverter<T> {
T convertDataSize(DataSize value);
T convertBytes(byte[] value);
T convertPair(Pair<?, ?> value);
T convertList(List<?> value);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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,6 +53,10 @@ public interface ValueVisitor {
visitDefault(value);
}
default void visitBytes(byte[] value) {
visitDefault(value);
}
default void visitPair(Pair<?, ?> value) {
visitDefault(value);
}
@@ -108,6 +112,8 @@ public interface ValueVisitor {
visitMap(map);
} else if (value instanceof Pattern pattern) {
visitRegex(pattern);
} else if (value instanceof byte[] bytes) {
visitBytes(bytes);
} else {
throw new IllegalArgumentException("Cannot visit value with unexpected type: " + value);
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -151,6 +151,12 @@ final class YamlRenderer implements ValueRenderer {
String.format("Values of type `DataSize` cannot be rendered as YAML. Value: %s", value));
}
@Override
public void visitBytes(byte[] value) {
throw new RendererException(
String.format("Values of type `Bytes` cannot be rendered as YAML. Value: %s", value));
}
@Override
public void visitPair(Pair<?, ?> value) {
doVisitIterable(value, null);
@@ -0,0 +1,41 @@
/*
* Copyright © 2025 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.ast;
import com.oracle.truffle.api.frame.VirtualFrame;
public class ByteConstantValueNode extends ExpressionNode implements ConstantNode {
private final byte value;
public ByteConstantValueNode(byte value) {
this.value = value;
}
@Override
public Long getValue() {
return (long) value;
}
public byte getByteValue() {
return value;
}
@Override
public Object executeGeneric(VirtualFrame frame) {
return getValue();
}
}
@@ -29,6 +29,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.graalvm.collections.EconomicMap;
@@ -37,6 +38,7 @@ import org.pkl.core.PklBugException;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.TypeParameter;
import org.pkl.core.TypeParameter.Variance;
import org.pkl.core.ast.ByteConstantValueNode;
import org.pkl.core.ast.ConstantNode;
import org.pkl.core.ast.ConstantValueNode;
import org.pkl.core.ast.ExpressionNode;
@@ -77,6 +79,7 @@ import org.pkl.core.ast.expression.generator.GeneratorSpreadNodeGen;
import org.pkl.core.ast.expression.generator.GeneratorWhenNode;
import org.pkl.core.ast.expression.generator.RestoreForBindingsNode;
import org.pkl.core.ast.expression.literal.AmendModuleNodeGen;
import org.pkl.core.ast.expression.literal.BytesLiteralNode;
import org.pkl.core.ast.expression.literal.CheckIsAnnotationClassNode;
import org.pkl.core.ast.expression.literal.ConstantEntriesLiteralNodeGen;
import org.pkl.core.ast.expression.literal.ElementsEntriesLiteralNodeGen;
@@ -160,6 +163,7 @@ import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.BaseModule;
import org.pkl.core.runtime.ModuleInfo;
import org.pkl.core.runtime.ModuleResolver;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmClass;
import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmDataSize;
@@ -524,9 +528,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
}
@Override
public IntLiteralNode visitIntLiteralExpr(IntLiteralExpr expr) {
var section = createSourceSection(expr);
private <T> T parseNumber(IntLiteralExpr expr, BiFunction<String, Integer, T> parser) {
var text = remove_(expr.getNumber());
var radix = 10;
@@ -547,11 +549,17 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
// also moves negation from runtime to parse time
text = "-" + text;
}
return parser.apply(text, radix);
}
@Override
public IntLiteralNode visitIntLiteralExpr(IntLiteralExpr expr) {
var section = createSourceSection(expr);
try {
var num = Long.parseLong(text, radix);
var num = parseNumber(expr, Long::parseLong);
return new IntLiteralNode(section, num);
} catch (NumberFormatException e) {
var text = expr.getNumber();
throw exceptionBuilder().evalError("intTooLarge", text).withSourceSection(section).build();
}
}
@@ -637,8 +645,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
// TODO: make sure that no user-defined List/Set/Map method is in scope
// TODO: support qualified calls (e.g., `import "pkl:base"; x = base.List()/Set()/Map()`) for
// correctness
// TODO: support qualified calls (e.g., `import "pkl:base"; x =
// base.List()/Set()/Map()/Bytes()`) for correctness
if (identifier == org.pkl.core.runtime.Identifier.LIST) {
return doVisitListLiteral(expr, argList);
}
@@ -651,6 +659,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return doVisitMapLiteral(expr, argList);
}
if (identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) {
return doVisitBytesLiteral(expr, argList);
}
var scope = symbolTable.getCurrentScope();
return new ResolveMethodNode(
@@ -1083,6 +1095,18 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
: new MapLiteralNode(createSourceSection(expr), keyAndValueNodes.first);
}
private ExpressionNode doVisitBytesLiteral(Expr expr, ArgumentList argList) {
var elementNodes = createCollectionArgumentBytesNodes(argList);
if (elementNodes.first.length == 0) {
return new ConstantValueNode(VmBytes.EMPTY);
}
return elementNodes.second
? new ConstantValueNode(
createSourceSection(expr), VmBytes.createFromConstantNodes(elementNodes.first))
: new BytesLiteralNode(createSourceSection(expr), elementNodes.first);
}
private Pair<ExpressionNode[], Boolean> createCollectionArgumentNodes(ArgumentList exprs) {
var args = exprs.getArguments();
var elementNodes = new ExpressionNode[args.size()];
@@ -1097,6 +1121,32 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return Pair.of(elementNodes, isConstantNodes);
}
private Pair<ExpressionNode[], Boolean> createCollectionArgumentBytesNodes(ArgumentList exprs) {
var args = exprs.getArguments();
var expressionNodes = new ExpressionNode[args.size()];
var isAllByteLiterals = true;
for (var i = 0; i < args.size(); i++) {
var expr = args.get(i);
if (expr instanceof IntLiteralExpr intLiteralExpr && isAllByteLiterals) {
try {
var byt = parseNumber(intLiteralExpr, Byte::parseByte);
expressionNodes[i] = new ByteConstantValueNode(byt);
} catch (NumberFormatException e) {
// proceed with initializing a constant value node; we'll throw an error inside
// BytesLiteralNode.
isAllByteLiterals = false;
expressionNodes[i] = visitExpr(expr);
}
} else {
isAllByteLiterals = false;
expressionNodes[i] = visitExpr(expr);
}
}
return Pair.of(expressionNodes, isAllByteLiterals);
}
public GeneratorMemberNode visitObjectMember(org.pkl.parser.syntax.ObjectMember member) {
return (GeneratorMemberNode) member.accept(this);
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -78,4 +78,9 @@ public abstract class AdditionNode extends BinaryExpressionNode {
protected VmMap eval(VmMap left, VmMap right) {
return left.concatenate(right);
}
@Specialization
protected VmBytes eval(VmBytes left, VmBytes right) {
return left.concatenate(right);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -98,6 +98,18 @@ public abstract class SubscriptNode extends BinaryExpressionNode {
return readMember(dynamic, key, callNode);
}
@Specialization
protected long eval(VmBytes receiver, long index) {
if (index < 0 || index >= receiver.getLength()) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder()
.evalError("elementIndexOutOfRange", index, 0, receiver.getLength() - 1)
.withProgramValue("Value", receiver)
.build();
}
return receiver.get(index);
}
private Object readMember(VmObject object, Object key, IndirectCallNode callNode) {
var result = VmUtils.readMemberOrNull(object, key, callNode);
if (result != null) return result;
@@ -118,6 +118,14 @@ public abstract class GeneratorForNode extends GeneratorMemberNode {
}
}
@Specialization
protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmBytes iterable) {
long idx = 0;
for (var byt : iterable.getBytes()) {
executeIteration(frame, parent, data, idx++, (long) byt);
}
}
@Fallback
@SuppressWarnings("unused")
protected void fallback(VirtualFrame frame, Object parent, ObjectData data, Object iterable) {
@@ -29,6 +29,7 @@ import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.BaseModule;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.Iterators.TruffleIterator;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmClass;
import org.pkl.core.runtime.VmCollection;
import org.pkl.core.runtime.VmDynamic;
@@ -161,6 +162,16 @@ public abstract class GeneratorSpreadNode extends GeneratorMemberNode {
doEvalIntSeq(frame, parent, data, iterable);
}
@Specialization
protected void eval(VirtualFrame frame, VmObject parent, ObjectData data, VmBytes iterable) {
doEvalBytes(frame, parent.getVmClass(), data, iterable);
}
@Specialization
protected void eval(VirtualFrame frame, VmClass parent, ObjectData data, VmBytes iterable) {
doEvalBytes(frame, parent, data, iterable);
}
@Fallback
@SuppressWarnings("unused")
protected void fallback(VirtualFrame frame, Object parent, ObjectData data, Object iterable) {
@@ -266,6 +277,18 @@ public abstract class GeneratorSpreadNode extends GeneratorMemberNode {
spreadIterable(frame, data, iterable);
}
private void doEvalBytes(VirtualFrame frame, VmClass parent, ObjectData data, VmBytes iterable) {
if (isTypedObjectClass(parent) || parent == getMappingClass()) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder()
.evalError("cannotSpreadObject", iterable.getVmClass(), parent)
.withHint("`Bytes` can only be spread into objects of type `Dynamic` and `Listing`.")
.withProgramValue("Value", iterable)
.build();
}
spreadIterable(frame, data, iterable);
}
private void cannotHaveMember(VmClass clazz, ObjectMember member) {
CompilerDirectives.transferToInterpreter();
var builder = exceptionBuilder();
@@ -0,0 +1,71 @@
/*
* Copyright © 2025 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.ast.expression.literal;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.ast.type.TypeNode.UInt8TypeAliasTypeNode;
import org.pkl.core.ast.type.VmTypeMismatchException;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.util.LateInit;
@NodeInfo(shortName = "Bytes()")
public final class BytesLiteralNode extends ExpressionNode {
@Children private final ExpressionNode[] elements;
@Child @LateInit private TypeNode typeNode;
public BytesLiteralNode(SourceSection sourceSection, ExpressionNode[] elements) {
super(sourceSection);
this.elements = elements;
}
private TypeNode getTypeNode() {
if (typeNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
typeNode = new UInt8TypeAliasTypeNode();
}
return typeNode;
}
@Override
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) {
var bytes = new byte[elements.length];
var typeNode = getTypeNode();
for (var i = 0; i < elements.length; i++) {
var elem = elements[i];
try {
var result = (Long) typeNode.execute(frame, elem.executeGeneric(frame));
bytes[i] = result.byteValue();
} catch (VmTypeMismatchException err) {
// optimization: don't create a new stack frame to check the type, but pretend that one
// exists.
err.putInsertedStackFrame(
getRootNode().getCallTarget(),
VmUtils.createStackFrame(elem.getSourceSection(), getRootNode().getName()));
throw err.toVmException();
}
}
return new VmBytes(bytes);
}
}
@@ -2153,52 +2153,71 @@ public abstract class TypeNode extends PklNode {
}
}
public static final class UIntTypeAliasTypeNode extends IntSlotTypeNode {
private final VmTypeAlias typeAlias;
private final long mask;
public UIntTypeAliasTypeNode(VmTypeAlias typeAlias, long mask) {
protected abstract static class IntMaskSlotTypeNode extends IntSlotTypeNode {
protected final long mask;
IntMaskSlotTypeNode(long mask) {
super(VmUtils.unavailableSourceSection());
this.typeAlias = typeAlias;
this.mask = mask;
}
@Override
protected Object executeLazily(VirtualFrame frame, Object value) {
protected final Object executeLazily(VirtualFrame frame, Object value) {
var typealias = getVmTypeAlias();
assert typealias != null;
if (value instanceof Long l) {
if ((l & mask) == l) return value;
throw new VmTypeMismatchException.Constraint(typeAlias.getConstraintSection(), value);
throw new VmTypeMismatchException.Constraint(typealias.getConstraintSection(), value);
}
throw new VmTypeMismatchException.Simple(
typeAlias.getBaseTypeSection(), value, BaseModule.getIntClass());
typealias.getBaseTypeSection(), value, BaseModule.getIntClass());
}
@Override
public VmClass getVmClass() {
public final VmClass getVmClass() {
return BaseModule.getIntClass();
}
@Override
public final VmTyped getMirror() {
return MirrorFactories.typeAliasTypeFactory.create(this);
}
@Override
public final boolean doIsEquivalentTo(TypeNode other) {
return other instanceof UIntTypeAliasTypeNode aliasTypeNode && mask == aliasTypeNode.mask;
}
@Override
protected final boolean acceptTypeNode(TypeNodeConsumer consumer) {
return consumer.accept(this);
}
}
public static final class UIntTypeAliasTypeNode extends IntMaskSlotTypeNode {
private final VmTypeAlias typeAlias;
public UIntTypeAliasTypeNode(VmTypeAlias typeAlias, long mask) {
super(mask);
this.typeAlias = typeAlias;
}
@Override
public VmTypeAlias getVmTypeAlias() {
return typeAlias;
}
}
@Override
public VmTyped getMirror() {
return MirrorFactories.typeAliasTypeFactory.create(this);
public static final class UInt8TypeAliasTypeNode extends IntMaskSlotTypeNode {
public UInt8TypeAliasTypeNode() {
super(0x00000000000000FFL);
}
@Override
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof UIntTypeAliasTypeNode aliasTypeNode && mask == aliasTypeNode.mask;
}
@Override
protected boolean acceptTypeNode(TypeNodeConsumer consumer) {
return consumer.accept(this);
public VmTypeAlias getVmTypeAlias() {
return BaseModule.getUInt8TypeAlias();
}
}
@@ -174,7 +174,7 @@ public abstract class UnresolvedTypeNode extends PklNode {
case "Int8":
return new Int8TypeAliasTypeNode();
case "UInt8":
return new UIntTypeAliasTypeNode(alias, 0x00000000000000FFL);
return new UInt8TypeAliasTypeNode();
case "Int16":
return new Int16TypeAliasTypeNode();
case "UInt16":
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -17,7 +17,7 @@ package org.pkl.core.resource;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.pkl.core.util.ByteArrayUtils;
/** An external (file, HTTP, etc.) resource. */
public record Resource(URI uri, byte[] bytes) {
@@ -44,6 +44,6 @@ public record Resource(URI uri, byte[] bytes) {
/** Returns the content of this resource in Base64. */
public String getBase64() {
return Base64.getEncoder().encodeToString(bytes);
return ByteArrayUtils.base64(bytes);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -71,6 +71,10 @@ public final class BaseModule extends StdLibModule {
return DataSizeClass.instance;
}
public static VmClass getBytesClass() {
return BytesClass.instance;
}
public static VmClass getIntSeqClass() {
return IntSeqClass.instance;
}
@@ -219,6 +223,10 @@ public final class BaseModule extends StdLibModule {
return MixinTypeAlias.instance;
}
public static VmTypeAlias getUInt8TypeAlias() {
return UInt8TypeAlias.instance;
}
private static final class AnyClass {
static final VmClass instance = loadClass("Any");
}
@@ -259,6 +267,10 @@ public final class BaseModule extends StdLibModule {
static final VmClass instance = loadClass("DataSize");
}
private static final class BytesClass {
static final VmClass instance = loadClass("Bytes");
}
private static final class IntSeqClass {
static final VmClass instance = loadClass("IntSeq");
}
@@ -383,6 +395,10 @@ public final class BaseModule extends StdLibModule {
static final VmTypeAlias instance = loadTypeAlias("Int32");
}
private static final class UInt8TypeAlias {
static final VmTypeAlias instance = loadTypeAlias("UInt8");
}
private static final class MixinTypeAlias {
static final VmTypeAlias instance = loadTypeAlias("Mixin");
}
@@ -75,6 +75,11 @@ public final class Identifier implements Comparable<Identifier> {
// XmlCData}
public static final Identifier TEXT = get("text");
public static final Identifier BYTES_CONSTRUCTOR = get("Bytes");
// members of pkl.base#{FileOutput}
public static final Identifier BYTES = get("bytes");
// members of pkl.base#ModuleOutput, pkl.base#Resource, pkl.base#String
public static final Identifier BASE64 = get("base64");
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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,7 @@ public final class ResourceManager {
new VmObjectFactory<Resource>(BaseModule::getResourceClass)
.addProperty("uri", resource -> resource.uri().toString())
.addProperty("text", Resource::getText)
.addProperty("base64", Resource::getBase64);
.addProperty("bytes", resource -> new VmBytes(resource.bytes()));
}
@TruffleBoundary
@@ -0,0 +1,208 @@
/*
* Copyright © 2025 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;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.CompilerDirectives.ValueType;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.PrimitiveIterator;
import org.pkl.core.DataSizeUnit;
import org.pkl.core.ast.ByteConstantValueNode;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.util.ByteArrayUtils;
import org.pkl.core.util.Nullable;
@ValueType
public final class VmBytes extends VmValue implements Iterable<Long> {
private @Nullable VmList vmList;
private @Nullable String base64;
private @Nullable String hex;
private final byte[] bytes;
private @Nullable VmDataSize size;
public static VmBytes EMPTY = new VmBytes(new byte[0]);
@TruffleBoundary
public static VmBytes createFromConstantNodes(ExpressionNode[] elements) {
if (elements.length == 0) {
return EMPTY;
}
var bytes = new byte[elements.length];
for (var i = 0; i < elements.length; i++) {
var exprNode = elements[i];
// guaranteed by AstBuilder
assert exprNode instanceof ByteConstantValueNode;
bytes[i] = ((ByteConstantValueNode) exprNode).getByteValue();
}
return new VmBytes(bytes);
}
public VmBytes(byte[] bytes) {
this.bytes = bytes;
}
public VmBytes(VmList vmList, byte[] bytes) {
this.vmList = vmList;
this.bytes = bytes;
}
@Override
public VmClass getVmClass() {
return BaseModule.getBytesClass();
}
@Override
public void force(boolean allowUndefinedValues) {
// do nothing
}
@Override
public byte[] export() {
return bytes;
}
@Override
public void accept(VmValueVisitor visitor) {
visitor.visitBytes(this);
}
@Override
public <T> T accept(VmValueConverter<T> converter, Iterable<Object> path) {
return converter.convertBytes(this, path);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof VmBytes vmBytes)) {
return false;
}
return Arrays.equals(bytes, vmBytes.bytes);
}
@Override
public int hashCode() {
return Arrays.hashCode(bytes);
}
public byte[] getBytes() {
return bytes;
}
public long get(long index) {
return getBytes()[(int) index];
}
public VmBytes concatenate(VmBytes right) {
if (bytes.length == 0) return right;
if (right.bytes.length == 0) return this;
var newBytes = new byte[bytes.length + right.bytes.length];
System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
System.arraycopy(right.bytes, 0, newBytes, bytes.length, right.bytes.length);
return new VmBytes(newBytes);
}
public VmList toList() {
if (vmList == null) {
vmList = VmList.create(bytes);
}
return vmList;
}
public String base64() {
if (base64 == null) {
base64 = ByteArrayUtils.base64(bytes);
}
return base64;
}
public String hex() {
if (hex == null) {
hex = ByteArrayUtils.toHex(bytes);
}
return hex;
}
public int getLength() {
return bytes.length;
}
public VmDataSize getSize() {
if (size == null) {
if (getLength() == 0) {
// avoid log10(0), which gives us -Infinity
size = new VmDataSize(0, DataSizeUnit.BYTES);
} else {
var magnitude = (int) Math.floor(Math.log10(getLength()));
var unit =
switch (magnitude) {
case 0, 1, 2 -> DataSizeUnit.BYTES;
case 3, 4, 5 -> DataSizeUnit.KILOBYTES;
case 6, 7, 8 -> DataSizeUnit.MEGABYTES;
case 9, 10, 11 -> DataSizeUnit.GIGABYTES;
// in practice, can never happen (Java can only hold at most math.maxInt bytes).
case 12, 13, 14 -> DataSizeUnit.TERABYTES;
default -> DataSizeUnit.PETABYTES;
};
size = new VmDataSize(getLength(), DataSizeUnit.BYTES).convertTo(unit);
}
}
return size;
}
@Override
public String toString() {
var sb = new StringBuilder("Bytes(");
var isFirst = true;
for (var byt : bytes) {
if (isFirst) {
isFirst = false;
} else {
sb.append(", ");
}
sb.append(Byte.toUnsignedInt(byt));
}
sb.append(")");
return sb.toString();
}
@Override
public Iterator<Long> iterator() {
return new PrimitiveIterator.OfLong() {
int index = 0;
@Override
public boolean hasNext() {
return index < bytes.length;
}
@Override
public long nextLong() {
if (!hasNext()) {
CompilerDirectives.transferToInterpreter();
throw new NoSuchElementException();
}
var result = Byte.toUnsignedLong(bytes[index]);
index += 1;
return result;
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -98,6 +98,16 @@ public final class VmList extends VmCollection {
return new VmList(vector.immutable());
}
@TruffleBoundary
public static VmList create(byte[] elements) {
if (elements.length == 0) return EMPTY;
var vector = RrbTree.emptyMutable();
for (var elem : elements) {
vector.append(Byte.toUnsignedLong(elem));
}
return new VmList(vector.immutable());
}
@TruffleBoundary
public static VmList create(Object[] elements, int length) {
if (elements.length == 0) return EMPTY;
@@ -175,6 +175,10 @@ public final class VmUtils {
return (String) VmUtils.readMember((VmObjectLike) receiver, Identifier.TEXT);
}
public static VmBytes readBytesProperty(VmObjectLike receiver) {
return (VmBytes) VmUtils.readMember(receiver, Identifier.BYTES);
}
@TruffleBoundary
public static Object readMember(VmObjectLike receiver, Object memberKey) {
var result = readMemberOrNull(receiver, memberKey);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -52,6 +52,8 @@ public interface VmValueConverter<T> {
T convertDataSize(VmDataSize value, Iterable<Object> path);
T convertBytes(VmBytes vmBytes, Iterable<Object> path);
T convertIntSeq(VmIntSeq value, Iterable<Object> path);
T convertList(VmList value, Iterable<Object> path);
@@ -114,6 +114,45 @@ public final class VmValueRenderer {
append(value);
}
private void renderByteSize(VmDataSize size) {
var value = size.getValue();
if (value % 1 == 0) {
append((int) value);
} else if ((value * 10) % 1 == 0) {
append(String.format("%.1f", value));
} else {
append(String.format("%.2f", value));
}
append(".");
append(size.getUnit());
}
@Override
public void visitBytes(VmBytes value) {
append("Bytes(");
// truncate bytes if over 8 bytes
renderByteElems(value, Math.min(value.getLength(), 8));
if (value.getLength() > 8) {
append(", ... <total size: ");
renderByteSize(value.getSize());
append(">");
}
append(")");
}
private void renderByteElems(VmBytes value, int limit) {
var isFirst = true;
var bytes = value.getBytes();
for (var i = 0; i < limit; i++) {
if (isFirst) {
isFirst = false;
} else {
append(", ");
}
append(Byte.toUnsignedInt(bytes[i]));
}
}
@Override
public void visitPair(VmPair value) {
append("Pair(");
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -30,6 +30,8 @@ public interface VmValueVisitor {
void visitDataSize(VmDataSize value);
void visitBytes(VmBytes value);
void visitIntSeq(VmIntSeq value);
void visitList(VmList value);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -30,6 +30,7 @@ public final class PklConverter implements VmValueConverter<Object> {
private final @Nullable VmFunction floatConverter;
private final @Nullable VmFunction durationConverter;
private final @Nullable VmFunction dataSizeConverter;
private final @Nullable VmFunction bytesConverter;
private final @Nullable VmFunction intSeqConverter;
private final @Nullable VmFunction listConverter;
private final @Nullable VmFunction setConverter;
@@ -56,6 +57,7 @@ public final class PklConverter implements VmValueConverter<Object> {
floatConverter = typeConverters.get(BaseModule.getFloatClass());
durationConverter = typeConverters.get(BaseModule.getDurationClass());
dataSizeConverter = typeConverters.get(BaseModule.getDataSizeClass());
bytesConverter = typeConverters.get(BaseModule.getBytesClass());
intSeqConverter = typeConverters.get(BaseModule.getIntSeqClass());
listConverter = typeConverters.get(BaseModule.getListClass());
setConverter = typeConverters.get(BaseModule.getSetClass());
@@ -100,6 +102,11 @@ public final class PklConverter implements VmValueConverter<Object> {
return doConvert(value, path, dataSizeConverter);
}
@Override
public Object convertBytes(VmBytes value, Iterable<Object> path) {
return doConvert(value, path, bytesConverter);
}
@Override
public Object convertIntSeq(VmIntSeq value, Iterable<Object> path) {
return doConvert(value, path, intSeqConverter);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -142,4 +142,13 @@ public final class BaseNodes {
return new VmIntSeq(first, second, 1L);
}
}
public abstract static class Bytes extends ExternalMethod1Node {
@Specialization
protected VmList eval(VirtualFrame frame, VmTyped self, Object args) {
// invocations of this method are handled specially in AstBuilder
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().bug("Node `BaseNodes.Bytes` should never be executed.").build();
}
}
}
@@ -0,0 +1,121 @@
/*
* Copyright © 2025 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.stdlib.base;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Specialization;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmList;
import org.pkl.core.runtime.VmNull;
import org.pkl.core.stdlib.ExternalMethod0Node;
import org.pkl.core.stdlib.ExternalMethod1Node;
import org.pkl.core.stdlib.ExternalPropertyNode;
import org.pkl.core.util.ByteArrayUtils;
public final class BytesNodes {
private BytesNodes() {}
public abstract static class toList extends ExternalMethod0Node {
@Specialization
protected VmList eval(VmBytes self) {
return self.toList();
}
}
public abstract static class length extends ExternalPropertyNode {
@Specialization
protected long eval(VmBytes self) {
return self.getLength();
}
}
public abstract static class size extends ExternalPropertyNode {
@Specialization
protected VmDataSize eval(VmBytes self) {
return self.getSize();
}
}
public abstract static class base64 extends ExternalPropertyNode {
@Specialization
protected String eval(VmBytes self) {
return self.base64();
}
}
public abstract static class hex extends ExternalPropertyNode {
@Specialization
protected String eval(VmBytes self) {
return self.hex();
}
}
public abstract static class md5 extends ExternalPropertyNode {
@Specialization
protected String eval(VmBytes self) {
return ByteArrayUtils.md5(self.getBytes());
}
}
public abstract static class sha1 extends ExternalPropertyNode {
@Specialization
protected String eval(VmBytes self) {
return ByteArrayUtils.sha1(self.getBytes());
}
}
public abstract static class sha256 extends ExternalPropertyNode {
@Specialization
protected String eval(VmBytes self) {
return ByteArrayUtils.sha256(self.getBytes());
}
}
public abstract static class sha256Int extends ExternalPropertyNode {
@Specialization
protected long eval(VmBytes self) {
return ByteArrayUtils.sha256Int(self.getBytes());
}
}
public abstract static class getOrNull extends ExternalMethod1Node {
@Specialization
protected Object eval(VmBytes self, long index) {
if (index < 0 || index >= self.getLength()) {
return VmNull.withoutDefault();
}
return self.get(index);
}
}
public abstract static class decodeToString extends ExternalMethod1Node {
@Specialization
protected String eval(VmBytes self, String charset) {
try {
var byteBuffer = ByteBuffer.wrap(self.getBytes());
var decoder = Charset.forName(charset).newDecoder();
return decoder.decode(byteBuffer).toString();
} catch (CharacterCodingException e) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("characterCodingException", charset).build();
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -126,6 +126,11 @@ public final class JsonRendererNodes {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitBytes(VmBytes value) {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitRegex(VmRegex value) {
cannotRenderTypeAddConverter(value);
@@ -18,17 +18,22 @@ package org.pkl.core.stdlib.base;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.LoopNode;
import org.pkl.core.ast.expression.binary.*;
import org.pkl.core.ast.internal.IsInstanceOfNode;
import org.pkl.core.ast.internal.IsInstanceOfNodeGen;
import org.pkl.core.ast.lambda.*;
import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.ast.type.TypeNode.UInt8TypeAliasTypeNode;
import org.pkl.core.ast.type.VmTypeMismatchException;
import org.pkl.core.runtime.*;
import org.pkl.core.stdlib.*;
import org.pkl.core.stdlib.base.CollectionNodes.CompareByNode;
import org.pkl.core.stdlib.base.CollectionNodes.CompareNode;
import org.pkl.core.stdlib.base.CollectionNodes.CompareWithNode;
import org.pkl.core.util.EconomicSets;
import org.pkl.core.util.LateInit;
// duplication between ListNodes and SetNodes is "intentional"
// (sharing nodes between VmCollection subtypes results in
@@ -1321,4 +1326,34 @@ public final class ListNodes {
return self.toDynamic();
}
}
public abstract static class toBytes extends ExternalMethod0Node {
@Child @LateInit private TypeNode typeNode;
private TypeNode getTypeNode() {
if (typeNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
typeNode = new UInt8TypeAliasTypeNode();
}
return typeNode;
}
@Specialization
protected VmBytes eval(VirtualFrame frame, VmList self) {
var typeNode = getTypeNode();
var bytes = new byte[self.getLength()];
try {
for (var i = 0; i < self.getLength(); i++) {
var elem = self.get(i);
var num = (Long) typeNode.executeEagerly(frame, elem);
bytes[i] = num.byteValue();
}
} catch (VmTypeMismatchException e) {
CompilerDirectives.transferToInterpreter();
throw e.toVmException();
}
return new VmBytes(self, bytes);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -54,7 +54,6 @@ public final class PListRendererNodes {
}
// keep in sync with org.pkl.core.PListRenderer
@SuppressWarnings("HttpUrlsUsage")
private static final class PListRenderer extends AbstractRenderer {
// it's safe (though not required) to escape all the following characters in XML text nodes
@@ -124,6 +123,13 @@ public final class PListRendererNodes {
.build();
}
@Override
public void visitBytes(VmBytes value) {
builder.append("<data>");
builder.append(value.base64());
builder.append("</data>");
}
@Override
public void visitRegex(VmRegex value) {
throw new VmExceptionBuilder()
@@ -17,6 +17,7 @@ package org.pkl.core.stdlib.base;
import org.pkl.core.ValueFormatter;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmDuration;
import org.pkl.core.runtime.VmDynamic;
@@ -99,6 +100,11 @@ public final class PcfRenderer extends AbstractRenderer {
builder.append(value);
}
@Override
public void visitBytes(VmBytes value) {
builder.append(value);
}
@Override
public void visitPair(VmPair value) {
builder.append("Pair(");
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -18,6 +18,7 @@ package org.pkl.core.stdlib.base;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmDuration;
import org.pkl.core.runtime.VmDynamic;
@@ -117,6 +118,11 @@ public final class PropertiesRendererNodes {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitBytes(VmBytes value) {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitIntSeq(VmIntSeq value) {
cannotRenderTypeAddConverter(value);
@@ -1,102 +0,0 @@
/*
* Copyright © 2024 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.stdlib.base;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import java.util.*;
import org.pkl.core.resource.Resource;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmTyped;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.stdlib.ExternalPropertyNode;
import org.pkl.core.util.ByteArrayUtils;
public final class ResourceNodes {
private ResourceNodes() {}
public abstract static class md5 extends ExternalPropertyNode {
@TruffleBoundary
@Specialization(guards = "self.hasExtraStorage()")
protected String evalWithExtraStorage(VmTyped self) {
var resource = (Resource) self.getExtraStorage();
return ByteArrayUtils.md5(resource.bytes());
}
@TruffleBoundary
@Specialization(guards = "!self.hasExtraStorage()")
protected String evalWithoutExtraStorage(VmTyped self) {
// `pkl.base#Resource` is designed to allow direct instantiation,
// in which case it isn't backed by a `org.pkl.core.resource.Resource`.
// It seems the best we can do here
// is to expect `pkl.base#Resource.base64` to be set and decode it.
var base64 = (String) VmUtils.readMember(self, Identifier.BASE64);
var bytes = Base64.getDecoder().decode(base64);
return ByteArrayUtils.md5(bytes);
}
}
public abstract static class sha1 extends ExternalPropertyNode {
@TruffleBoundary
@Specialization(guards = "self.hasExtraStorage()")
protected String evalWithExtraStorage(VmTyped self) {
var resource = (Resource) self.getExtraStorage();
return ByteArrayUtils.sha1(resource.bytes());
}
@TruffleBoundary
@Specialization(guards = "!self.hasExtraStorage()")
protected String evalWithoutExtraStorage(VmTyped self) {
var base64 = (String) VmUtils.readMember(self, Identifier.BASE64);
var bytes = Base64.getDecoder().decode(base64);
return ByteArrayUtils.sha1(bytes);
}
}
public abstract static class sha256 extends ExternalPropertyNode {
@TruffleBoundary
@Specialization(guards = "self.hasExtraStorage()")
protected String evalWithExtraStorage(VmTyped self) {
var resource = (Resource) self.getExtraStorage();
return ByteArrayUtils.sha256(resource.bytes());
}
@TruffleBoundary
@Specialization(guards = "!self.hasExtraStorage()")
protected String evalWithoutExtraStorage(VmTyped self) {
var base64 = (String) VmUtils.readMember(self, Identifier.BASE64);
var bytes = Base64.getDecoder().decode(base64);
return ByteArrayUtils.sha256(bytes);
}
}
public abstract static class sha256Int extends ExternalPropertyNode {
@TruffleBoundary
@Specialization(guards = "self.hasExtraStorage()")
protected long evalWithExtraStorage(VmTyped self) {
var resource = (Resource) self.getExtraStorage();
return ByteArrayUtils.sha256Int(resource.bytes());
}
@TruffleBoundary
@Specialization(guards = "!self.hasExtraStorage()")
protected long evalWithoutExtraStorage(VmTyped self) {
var base64 = (String) VmUtils.readMember(self, Identifier.BASE64);
var bytes = Base64.getDecoder().decode(base64);
return ByteArrayUtils.sha256Int(bytes);
}
}
}
@@ -19,9 +19,11 @@ import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.LoopNode;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.*;
import org.pkl.core.PklBugException;
import org.pkl.core.ast.lambda.ApplyVmFunction1Node;
import org.pkl.core.ast.lambda.ApplyVmFunction1NodeGen;
import org.pkl.core.runtime.*;
@@ -149,6 +151,19 @@ public final class StringNodes {
}
}
public abstract static class isBase64 extends ExternalPropertyNode {
@Specialization
@TruffleBoundary
protected boolean eval(String self) {
try {
Base64.getDecoder().decode(self);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
public abstract static class chars extends ExternalPropertyNode {
@Specialization
@TruffleBoundary
@@ -900,7 +915,7 @@ public final class StringNodes {
@TruffleBoundary
@Specialization
protected String eval(String self) {
return Base64.getEncoder().encodeToString(self.getBytes(StandardCharsets.UTF_8));
return ByteArrayUtils.base64(self.getBytes(StandardCharsets.UTF_8));
}
}
@@ -920,6 +935,35 @@ public final class StringNodes {
}
}
public abstract static class base64DecodedBytes extends ExternalPropertyNode {
@TruffleBoundary
@Specialization
protected VmBytes eval(String self) {
try {
return new VmBytes(Base64.getDecoder().decode(self));
} catch (IllegalArgumentException e) {
throw exceptionBuilder()
.adhocEvalError(e.getMessage())
.withProgramValue("String", self)
.withCause(e)
.build();
}
}
}
public abstract static class encodeToBytes extends ExternalMethod1Node {
@TruffleBoundary
@Specialization
protected VmBytes eval(String self, String charsetName) {
try {
var bytes = self.getBytes(charsetName);
return new VmBytes(bytes);
} catch (UnsupportedEncodingException e) {
throw PklBugException.unreachableCode();
}
}
}
@TruffleBoundary
private static String substringFrom(String string, int start) {
return string.substring(start);
@@ -18,6 +18,7 @@ package org.pkl.core.stdlib.base;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmCollection;
import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmDuration;
@@ -177,6 +178,11 @@ public final class YamlRendererNodes {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitBytes(VmBytes value) {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitRegex(VmRegex value) {
cannotRenderTypeAddConverter(value);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -21,6 +21,7 @@ import java.util.Set;
import java.util.regex.Pattern;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.JsonnetModule;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmDuration;
import org.pkl.core.runtime.VmDynamic;
@@ -199,6 +200,11 @@ public final class RendererNodes {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitBytes(VmBytes value) {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitIntSeq(VmIntSeq value) {
cannotRenderTypeAddConverter(value);
@@ -44,6 +44,7 @@ import org.pkl.core.ast.type.TypeNode.UnionOfStringLiteralsTypeNode;
import org.pkl.core.ast.type.TypeNode.UnionTypeNode;
import org.pkl.core.ast.type.VmTypeMismatchException;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmBytes;
import org.pkl.core.runtime.VmClass;
import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmDuration;
@@ -522,6 +523,14 @@ public final class RendererNodes {
.build();
}
@Override
public void visitBytes(VmBytes value) {
throw new VmExceptionBuilder()
.evalError("cannotRenderTypeAddConverter", "Bytes", "Protobuf")
.withProgramValue("Value", value)
.build();
}
@Override
public void visitIntSeq(VmIntSeq value) {
writePropertyName();
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -154,6 +154,11 @@ public final class RendererNodes {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitBytes(VmBytes value) {
cannotRenderTypeAddConverter(value);
}
@Override
public void visitPair(VmPair value) {
cannotRenderTypeAddConverter(value);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
@@ -17,11 +17,16 @@ package org.pkl.core.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import org.pkl.core.runtime.VmExceptionBuilder;
public final class ByteArrayUtils {
private ByteArrayUtils() {}
public static String base64(byte[] input) {
return Base64.getEncoder().encodeToString(input);
}
public static String md5(byte[] input) {
return hash(input, "MD5");
}
@@ -1064,3 +1064,9 @@ External reader process has already terminated.
invalidOpaqueFileUri=\
File URIs must have a path that starts with `/` (e.g. file:/path/to/my_module.pkl).\n\
To resolve relative paths, remove the scheme prefix (remove "file:").
invalidStringBase64=\
`{0}` is not in base64 encoding.
characterCodingException=\
Invalid bytes for charset "{0}".