Implement Pkl binary renderer and parser (#1203)

Implements a binary renderer for Pkl values, which is a lossless capturing of Pkl data.

This follows the pkl binary format that is already used with `pkl server` calls, and is
made available as a Java API and also an in-language API.

Also, introduces a binary parser into the corresponding `PObject` types in Java.
This commit is contained in:
Jen Basch
2025-10-20 09:10:22 -07:00
committed by GitHub
parent c602dbb84c
commit 6c036bf82a
298 changed files with 4236 additions and 2581 deletions

View File

@@ -28,17 +28,7 @@ dependencies {
testImplementation(projects.pklCommonsTest)
}
tasks.test {
inputs
.dir("src/test/files/SnippetTests/input")
.withPropertyName("snippetTestsInput")
.withPathSensitivity(PathSensitivity.RELATIVE)
inputs
.dir("src/test/files/SnippetTests/output")
.withPropertyName("snippetTestsOutput")
.withPathSensitivity(PathSensitivity.RELATIVE)
exclude("**/NativeServerTest.*")
}
tasks.test { exclude("**/NativeServerTest.*") }
private fun Test.configureNativeTest() {
testClassesDirs = files(tasks.test.get().testClassesDirs)

View File

@@ -1,267 +0,0 @@
/*
* 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.
* 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.server
import java.nio.file.Path
import java.time.Duration
import org.msgpack.core.MessagePacker
import org.pkl.core.*
import org.pkl.core.ast.member.ObjectMember
import org.pkl.core.evaluatorSettings.TraceMode
import org.pkl.core.http.HttpClient
import org.pkl.core.module.ModuleKeyFactory
import org.pkl.core.project.DeclaredDependencies
import org.pkl.core.resource.ResourceReader
import org.pkl.core.runtime.*
internal class BinaryEvaluator(
transformer: StackFrameTransformer,
manager: SecurityManager,
httpClient: HttpClient,
logger: Logger,
factories: Collection<ModuleKeyFactory?>,
readers: Collection<ResourceReader?>,
environmentVariables: Map<String, String>,
externalProperties: Map<String, String>,
timeout: Duration?,
moduleCacheDir: Path?,
declaredDependencies: DeclaredDependencies?,
outputFormat: String?,
traceMode: TraceMode,
) :
EvaluatorImpl(
transformer,
false,
manager,
httpClient,
logger,
factories,
readers,
environmentVariables,
externalProperties,
timeout,
moduleCacheDir,
declaredDependencies,
outputFormat,
traceMode,
) {
fun evaluate(moduleSource: ModuleSource, expression: String?): ByteArray {
return doEvaluate(moduleSource) { module ->
val evalResult =
expression?.let { VmUtils.evaluateExpression(module, it, securityManager, moduleResolver) }
?: module
VmValue.force(evalResult, false)
threadLocalBufferPacker
.get()
.apply {
clear()
ValueEncoder(this).visit(evalResult)
}
.toByteArray()
}
}
private class ValueEncoder(private val packer: MessagePacker) : VmValueVisitor {
companion object {
private const val CODE_OBJECT: Byte = 0x1
private const val CODE_MAP: Byte = 0x2
private const val CODE_MAPPING: Byte = 0x3
private const val CODE_LIST: Byte = 0x4
private const val CODE_LISTING: Byte = 0x5
private const val CODE_SET: Byte = 0x6
private const val CODE_DURATION: Byte = 0x7
private const val CODE_DATASIZE: Byte = 0x8
private const val CODE_PAIR: Byte = 0x9
private const val CODE_INTSEQ: Byte = 0xA
private const val CODE_REGEX: Byte = 0xB
private const val CODE_CLASS: Byte = 0xC
private const val CODE_TYPEALIAS: Byte = 0xD
private const val CODE_FUNCTION: Byte = 0xE
private const val CODE_BYTES: Byte = 0xF
private const val CODE_PROPERTY: Byte = 0x10
private const val CODE_ENTRY: Byte = 0x11
private const val CODE_ELEMENT: Byte = 0x12
}
override fun visitString(value: String) {
packer.packString(value)
}
override fun visitBoolean(value: Boolean) {
packer.packBoolean(value)
}
override fun visitInt(value: Long) {
packer.packLong(value)
}
override fun visitFloat(value: Double) {
packer.packDouble(value)
}
override fun visitDuration(value: VmDuration) {
packer.packArrayHeader(3)
packer.packInt(CODE_DURATION.toInt())
packer.packDouble(value.value)
packer.packString(value.unit.toString())
}
override fun visitDataSize(value: VmDataSize) {
packer.packArrayHeader(3)
packer.packInt(CODE_DATASIZE.toInt())
packer.packDouble(value.value)
packer.packString(value.unit.toString())
}
override fun visitBytes(value: VmBytes) {
packer.packArrayHeader(2)
packer.packInt(CODE_BYTES.toInt())
packer.packBinaryHeader(value.bytes.size)
packer.addPayload(value.bytes)
}
override fun visitIntSeq(value: VmIntSeq) {
packer.packArrayHeader(4)
packer.packInt(CODE_INTSEQ.toInt())
packer.packLong(value.start)
packer.packLong(value.end)
packer.packLong(value.step)
}
private fun doVisitCollection(length: Int, value: Iterable<Any>) {
packer.packArrayHeader(length)
for (elem in value) {
visit(elem)
}
}
override fun visitList(value: VmList) {
packer.packArrayHeader(2)
packer.packInt(CODE_LIST.toInt())
doVisitCollection(value.length, value)
}
override fun visitSet(value: VmSet) {
packer.packArrayHeader(2)
packer.packInt(CODE_SET.toInt())
doVisitCollection(value.length, value)
}
override fun visitMap(value: VmMap) {
packer.packArrayHeader(2)
packer.packInt(CODE_MAP.toInt())
packer.packMapHeader(value.length)
for ((k, v) in value) {
visit(k)
visit(v)
}
}
override fun visitTyped(value: VmTyped) {
packObjectPreamble(value)
packer.packArrayHeader(value.vmClass.allRegularPropertyNames.size())
value.iterateAlreadyForcedMemberValues(this::doVisitObjectMember)
}
private fun doVisitObjectMember(key: Any, member: ObjectMember, value: Any): Boolean {
if (member.isClass || member.isTypeAlias) return true
packer.packArrayHeader(3)
when {
member.isProp -> {
packer.packInt(CODE_PROPERTY.toInt())
packer.packString(key.toString())
}
member.isEntry -> {
packer.packInt(CODE_ENTRY.toInt())
visit(key)
}
else -> {
packer.packInt(CODE_ELEMENT.toInt())
packer.packLong(key as Long)
}
}
visit(value)
return true
}
override fun visitDynamic(value: VmDynamic) {
packObjectPreamble(value)
packer.packArrayHeader(value.regularMemberCount)
value.iterateAlreadyForcedMemberValues(this::doVisitObjectMember)
}
private fun packObjectPreamble(value: VmObjectLike) {
packer.packArrayHeader(4)
packer.packInt(CODE_OBJECT.toInt())
packer.packString(value.vmClass.displayName)
packer.packString(value.vmClass.module.moduleInfo.moduleKey.uri.toString())
}
override fun visitListing(value: VmListing) {
packer.packArrayHeader(2)
packer.packInt(CODE_LISTING.toInt())
packer.packArrayHeader(value.length)
value.iterateAlreadyForcedMemberValues { _, _, memberValue ->
visit(memberValue)
true
}
}
override fun visitMapping(value: VmMapping) {
packer.packArrayHeader(2)
packer.packInt(CODE_MAPPING.toInt())
packer.packMapHeader(value.length.toInt())
value.iterateAlreadyForcedMemberValues { key, _, memberValue ->
visit(key)
visit(memberValue)
true
}
}
override fun visitClass(value: VmClass) {
packer.packArrayHeader(1)
packer.packInt(CODE_CLASS.toInt())
}
override fun visitTypeAlias(value: VmTypeAlias) {
packer.packArrayHeader(1)
packer.packInt(CODE_TYPEALIAS.toInt())
}
override fun visitPair(value: VmPair) {
packer.packArrayHeader(3)
packer.packInt(CODE_PAIR.toInt())
visit(value.first)
visit(value.second)
}
override fun visitRegex(value: VmRegex) {
packer.packArrayHeader(2)
packer.packInt(CODE_REGEX.toInt())
packer.packString(value.pattern.pattern())
}
override fun visitNull(value: VmNull) {
packer.packNil()
}
override fun visitFunction(value: VmFunction) {
packer.packArrayHeader(1)
packer.packInt(CODE_FUNCTION.toInt())
}
}
}

View File

@@ -25,7 +25,6 @@ import java.util.regex.Pattern
import kotlin.random.Random
import org.pkl.core.*
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
import org.pkl.core.evaluatorSettings.TraceMode
import org.pkl.core.externalreader.ExternalReaderProcess
import org.pkl.core.externalreader.ExternalResourceResolver
import org.pkl.core.externalreader.ModuleReaderSpec
@@ -44,7 +43,7 @@ import org.pkl.core.resource.ResourceReaders
import org.pkl.core.util.IoUtils
class Server(private val transport: MessageTransport) : AutoCloseable {
private val evaluators: MutableMap<Long, BinaryEvaluator> = ConcurrentHashMap()
private val evaluators: MutableMap<Long, Evaluator> = ConcurrentHashMap()
// https://github.com/jano7/executor would be the perfect executor here
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
@@ -98,7 +97,7 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
}
private fun handleCreateEvaluator(message: CreateEvaluatorRequest) {
val evaluatorId = Random.Default.nextLong()
val evaluatorId = Random.nextLong()
val baseResponse = CreateEvaluatorResponse(message.requestId(), null, null)
val evaluator =
@@ -126,7 +125,8 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
executor.execute {
try {
val resp = evaluator.evaluate(ModuleSource.create(msg.moduleUri, msg.moduleText), msg.expr)
val src = ModuleSource.create(msg.moduleUri, msg.moduleText)
val resp = evaluator.evaluateExpressionPklBinary(src, msg.expr ?: "module")
transport.send(baseResponse.copy(result = resp))
} catch (e: PklBugException) {
transport.send(baseResponse.copy(error = e.toString()))
@@ -183,53 +183,52 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
)
}
private fun createEvaluator(message: CreateEvaluatorRequest, evaluatorId: Long): BinaryEvaluator {
private fun createEvaluator(message: CreateEvaluatorRequest, evaluatorId: Long): Evaluator {
val modulePaths = message.modulePaths ?: emptyList()
val resolver = ModulePathResolver(modulePaths)
try {
val modulePaths = message.modulePaths ?: emptyList()
val resolver = ModulePathResolver(modulePaths)
val allowedModules = message.allowedModules?.map { Pattern.compile(it) } ?: emptyList()
val allowedResources = message.allowedResources?.map { Pattern.compile(it) } ?: emptyList()
val rootDir = message.rootDir
val env = message.env ?: emptyMap()
val properties = message.properties ?: emptyMap()
val timeout = message.timeout
val cacheDir = message.cacheDir
val httpClient =
with(HttpClient.builder()) {
message.http?.proxy?.let { proxy ->
setProxy(proxy.address, proxy.noProxy ?: listOf())
proxy.address?.let(IoUtils::setSystemProxy)
proxy.noProxy?.let { System.setProperty("http.nonProxyHosts", it.joinToString("|")) }
return with(EvaluatorBuilder.unconfigured()) {
setStackFrameTransformer(StackFrameTransformers.defaultTransformer)
color = false
httpClient =
with(HttpClient.builder()) {
message.http?.proxy?.let { proxy ->
setProxy(proxy.address, proxy.noProxy ?: listOf())
proxy.address?.let(IoUtils::setSystemProxy)
proxy.noProxy?.let { System.setProperty("http.nonProxyHosts", it.joinToString("|")) }
}
message.http?.caCertificates?.let(::addCertificates)
message.http?.rewrites?.let(::setRewrites)
buildLazily()
}
message.http?.caCertificates?.let(::addCertificates)
message.http?.rewrites?.let(::setRewrites)
buildLazily()
}
val dependencies =
securityManager =
with(SecurityManagers.standardBuilder()) {
message.allowedModules?.let { patterns ->
setAllowedModules(patterns.map { Pattern.compile(it) })
}
message.allowedResources?.let { patterns ->
setAllowedResources(patterns.map { Pattern.compile(it) })
}
setRootDir(message.rootDir)
build()
}
logger = ClientLogger(evaluatorId, transport)
addModuleKeyFactories(createModuleKeyFactories(message, evaluatorId, resolver))
addResourceReaders(createResourceReaders(message, evaluatorId, resolver))
message.env?.let { environmentVariables = it }
message.properties?.let { externalProperties = it }
timeout = message.timeout
moduleCacheDir = message.cacheDir
message.project?.let { proj ->
buildDeclaredDependencies(proj.projectFileUri, proj.dependencies, null)
val dependencies = buildDeclaredDependencies(proj.projectFileUri, proj.dependencies, null)
log("Got dependencies: $dependencies")
setProjectDependencies(dependencies)
}
log("Got dependencies: $dependencies")
return BinaryEvaluator(
StackFrameTransformers.defaultTransformer,
SecurityManagers.standard(
allowedModules,
allowedResources,
SecurityManagers.defaultTrustLevels,
rootDir,
),
httpClient,
ClientLogger(evaluatorId, transport),
createModuleKeyFactories(message, evaluatorId, resolver),
createResourceReaders(message, evaluatorId, resolver),
env,
properties,
timeout,
cacheDir,
dependencies,
message.outputFormat,
message.traceMode ?: TraceMode.COMPACT,
)
outputFormat = message.outputFormat
message.traceMode?.let { traceMode = it }
build()
}
} catch (e: IllegalArgumentException) {
throw ProtocolException(e.message ?: "Failed to create an evalutor. $e", e)
}

View File

@@ -1,7 +0,0 @@
res1 = "bar"
res2 = ""
res3 = 1
res4 = 2.3
res5 = true
res6 = false
res7 = null

View File

@@ -1,25 +0,0 @@
module com.foo.bar.MyModule
class Person {
firstName: String
lastName: String
age: Int
}
barnOwl: Person = new {
firstName = "Barn Owl"
lastName = "Bird"
age = 38
}
pigeon: Person = new {
firstName = "Pigeon"
lastName = "Bird"
age = 41
}
typealias MyPerson = Person
personClass = Person
personTypeAlias = MyPerson

View File

@@ -1,11 +0,0 @@
res1: DataSize = 1.b
res2: DataSize = 2.kb
res3: DataSize = 3.kib
res4: DataSize = 4.mb
res5: DataSize = 5.mib
res6: DataSize = 6.gb
res7: DataSize = 7.gib
res8: DataSize = 8.tb
res9: DataSize = 9.tib
res10: DataSize = 10.pb
res11: DataSize = 11.pib

View File

@@ -1,7 +0,0 @@
res1 = 1.ns
res2 = 2.us
res3 = 3.ms
res4 = 4.s
res5 = 5.min
res6 = 6.h
res7 = 7.d

View File

@@ -1,2 +0,0 @@
res1 = IntSeq(1, 3)
res2 = IntSeq(1, 4).step(5)

View File

@@ -1,6 +0,0 @@
res1: List<Int> = List(1, 3, 5, 7)
res2: Listing<Int> = new { 2; 4; 6; 8 }
res3: List<Int> = List()
res4: Listing<Int> = new {}
res5: List<List<Int>> = List(List(1, 2))
res6: Listing<Listing<Int>> = new { new { 1; 2 } }

View File

@@ -1,21 +0,0 @@
res1: Map = Map("foo", 1, "bar", 2)
res2: Mapping = new {
["foo"] = 1
["bar"] = 2
}
res3: Mapping = new {
["childMap"] = new Mapping {
["childFoo"] = 3
}
}
res4: Mapping = new {
[Map("foo", 1)] = new Mapping {
["bar"] = 2
}
}
// https://github.com/apple/pkl/issues/1151
res5: Mapping = new {
local self = this
["foo"] = new Dynamic { name = "foo" }
["bar"] = new Dynamic { name = self["foo"].name + "bar" }
}

View File

@@ -1,2 +0,0 @@
res1 = Pair(1, 2)
res2 = Pair("foo", "bar")

View File

@@ -1,3 +0,0 @@
res1 = Regex("abc")
res2 = Regex("")
res3 = Regex("(?m)^abc$")

View File

@@ -1,3 +0,0 @@
res1: Set<Int> = Set(1, 3, 5, 7)
res2: Set<Int> = Set()
res3: Set<Any> = Set(1, true, "", null)

View File

@@ -1,32 +0,0 @@
- 1
- basic
- file:///$snippetsDir/input/basic.pkl
-
-
- 16
- res1
- bar
-
- 16
- res2
- ''
-
- 16
- res3
- 1
-
- 16
- res4
- 2.3
-
- 16
- res5
- true
-
- 16
- res6
- false
-
- 16
- res7
- null

View File

@@ -1,54 +0,0 @@
- 1
- com.foo.bar.MyModule
- file:///$snippetsDir/input/classes.pkl
-
-
- 16
- barnOwl
-
- 1
- com.foo.bar.MyModule#Person
- file:///$snippetsDir/input/classes.pkl
-
-
- 16
- firstName
- Barn Owl
-
- 16
- lastName
- Bird
-
- 16
- age
- 38
-
- 16
- pigeon
-
- 1
- com.foo.bar.MyModule#Person
- file:///$snippetsDir/input/classes.pkl
-
-
- 16
- firstName
- Pigeon
-
- 16
- lastName
- Bird
-
- 16
- age
- 41
-
- 16
- personClass
-
- 12
-
- 16
- personTypeAlias
-
- 13

View File

@@ -1,81 +0,0 @@
- 1
- datasize
- file:///$snippetsDir/input/datasize.pkl
-
-
- 16
- res1
-
- 8
- 1.0
- b
-
- 16
- res2
-
- 8
- 2.0
- kb
-
- 16
- res3
-
- 8
- 3.0
- kib
-
- 16
- res4
-
- 8
- 4.0
- mb
-
- 16
- res5
-
- 8
- 5.0
- mib
-
- 16
- res6
-
- 8
- 6.0
- gb
-
- 16
- res7
-
- 8
- 7.0
- gib
-
- 16
- res8
-
- 8
- 8.0
- tb
-
- 16
- res9
-
- 8
- 9.0
- tib
-
- 16
- res10
-
- 8
- 10.0
- pb
-
- 16
- res11
-
- 8
- 11.0
- pib

View File

@@ -1,53 +0,0 @@
- 1
- duration
- file:///$snippetsDir/input/duration.pkl
-
-
- 16
- res1
-
- 7
- 1.0
- ns
-
- 16
- res2
-
- 7
- 2.0
- us
-
- 16
- res3
-
- 7
- 3.0
- ms
-
- 16
- res4
-
- 7
- 4.0
- s
-
- 16
- res5
-
- 7
- 5.0
- min
-
- 16
- res6
-
- 7
- 6.0
- h
-
- 16
- res7
-
- 7
- 7.0
- d

View File

@@ -1,20 +0,0 @@
- 1
- intseq
- file:///$snippetsDir/input/intseq.pkl
-
-
- 16
- res1
-
- 10
- 1
- 3
- 1
-
- 16
- res2
-
- 10
- 1
- 4
- 5

View File

@@ -1,58 +0,0 @@
- 1
- list
- file:///$snippetsDir/input/list.pkl
-
-
- 16
- res1
-
- 4
-
- 1
- 3
- 5
- 7
-
- 16
- res2
-
- 5
-
- 2
- 4
- 6
- 8
-
- 16
- res3
-
- 4
- []
-
- 16
- res4
-
- 5
- []
-
- 16
- res5
-
- 4
-
-
- 4
-
- 1
- 2
-
- 16
- res6
-
- 5
-
-
- 5
-
- 1
- 2

View File

@@ -1,68 +0,0 @@
- 1
- map
- file:///$snippetsDir/input/map.pkl
-
-
- 16
- res1
-
- 2
-
foo: 1
bar: 2
-
- 16
- res2
-
- 3
-
foo: 1
bar: 2
-
- 16
- res3
-
- 3
-
childMap:
- 3
-
childFoo: 3
-
- 16
- res4
-
- 3
-
?
- 2
-
foo: 1
:
- 3
-
bar: 2
-
- 16
- res5
-
- 3
-
foo:
- 1
- Dynamic
- pkl:base
-
-
- 16
- name
- foo
bar:
- 1
- Dynamic
- pkl:base
-
-
- 16
- name
- foobar

View File

@@ -1,18 +0,0 @@
- 1
- pair
- file:///$snippetsDir/input/pair.pkl
-
-
- 16
- res1
-
- 9
- 1
- 2
-
- 16
- res2
-
- 9
- foo
- bar

View File

@@ -1,22 +0,0 @@
- 1
- regex
- file:///$snippetsDir/input/regex.pkl
-
-
- 16
- res1
-
- 11
- abc
-
- 16
- res2
-
- 11
- ''
-
- 16
- res3
-
- 11
- (?m)^abc$

View File

@@ -1,30 +0,0 @@
- 1
- set
- file:///$snippetsDir/input/set.pkl
-
-
- 16
- res1
-
- 6
-
- 1
- 3
- 5
- 7
-
- 16
- res2
-
- 6
- []
-
- 16
- res3
-
- 6
-
- 1
- true
- ''
- null

View File

@@ -308,13 +308,13 @@ abstract class AbstractServerTest {
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.result?.debugYaml)
assertThat(evaluateResponse.result?.debugRendering)
.isEqualTo(
"""
- 6
-
- bird:/foo.txt
- bird:/subdir/bar.txt
- 'bird:/foo.txt'
- 'bird:/subdir/bar.txt'
"""
.trimIndent()
)
@@ -346,7 +346,7 @@ abstract class AbstractServerTest {
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.result?.debugYaml)
assertThat(evaluateResponse.result?.debugRendering)
.isEqualTo(
"""
- 6
@@ -547,11 +547,11 @@ abstract class AbstractServerTest {
"""
- 6
-
- bird:/Person.pkl
- bird:/birds/parrot.pkl
- bird:/birds/pigeon.pkl
- bird:/majesticBirds/barnOwl.pkl
- bird:/majesticBirds/elfOwl.pkl
- 'bird:/Person.pkl'
- 'bird:/birds/parrot.pkl'
- 'bird:/birds/pigeon.pkl'
- 'bird:/majesticBirds/barnOwl.pkl'
- 'bird:/majesticBirds/elfOwl.pkl'
"""
.trimIndent()
)
@@ -643,7 +643,7 @@ abstract class AbstractServerTest {
val response = client.receive<EvaluateResponse>()
assertThat(response.error).isNull()
val tripleQuote = "\"\"\""
assertThat(response.result?.debugYaml)
assertThat(response.result?.debugRendering)
.isEqualTo(
"""
|
@@ -666,6 +666,7 @@ abstract class AbstractServerTest {
res3 {
ressy = "the module2 output"
}
"""
.trimIndent()
)
@@ -713,7 +714,7 @@ abstract class AbstractServerTest {
)
val evaluatorResponse = client.receive<EvaluateResponse>()
assertThat(evaluatorResponse.result?.debugYaml).isEqualTo("1")
assertThat(evaluatorResponse.result?.debugRendering).isEqualTo("1")
}
@Test
@@ -753,13 +754,14 @@ abstract class AbstractServerTest {
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.result).isNotNull
assertThat(evaluateResponse.result?.debugYaml)
assertThat(evaluateResponse.result?.debugRendering)
.isEqualTo(
"""
|
firstName = "Pigeon"
lastName = "Bird"
fullName = "Pigeon Bird"
"""
.trimIndent()
)
@@ -793,13 +795,14 @@ abstract class AbstractServerTest {
val response12 = client.receive<EvaluateResponse>()
assertThat(response12.result).isNotNull
assertThat(response12.result?.debugYaml)
assertThat(response12.result?.debugRendering)
.isEqualTo(
"""
|
firstName = "Pigeon"
lastName = "Bird"
fullName = "Pigeon Bird"
"""
.trimIndent()
)
@@ -823,13 +826,14 @@ abstract class AbstractServerTest {
val response22 = client.receive<EvaluateResponse>()
assertThat(response22.result).isNotNull
assertThat(response22.result?.debugYaml)
assertThat(response22.result?.debugRendering)
.isEqualTo(
"""
|
firstName = "Parrot"
lastName = "Bird"
fullName = "Parrot Bird"
"""
.trimIndent()
)
@@ -996,9 +1000,6 @@ abstract class AbstractServerTest {
.contains("Rewrite rule must end with '/', but was 'https://example.com'")
}
private val ByteArray.debugYaml
get() = MessagePackDebugRenderer(this).output.trimIndent()
private fun TestTransport.sendCreateEvaluatorRequest(
requestId: Long = 123,
resourceReaders: List<ResourceReaderSpec> = listOf(),

View File

@@ -1,73 +0,0 @@
/*
* 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.
* 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.server
import java.nio.file.Path
import kotlin.Pair
import kotlin.reflect.KClass
import org.junit.platform.commons.annotation.Testable
import org.pkl.commons.test.InputOutputTestEngine
import org.pkl.core.*
import org.pkl.core.evaluatorSettings.TraceMode
import org.pkl.core.http.HttpClient
import org.pkl.core.module.ModuleKeyFactories
@Testable class BinaryEvaluatorSnippetTests
class BinaryEvaluatorSnippetTestEngine : InputOutputTestEngine() {
override val testClass: KClass<*> = BinaryEvaluatorSnippetTests::class
private val snippetsDir = rootProjectDir.resolve("pkl-server/src/test/files/SnippetTests")
private val outputDir = snippetsDir.resolve("output")
override val inputDir: Path = snippetsDir.resolve("input")
override val isInputFile: (Path) -> Boolean = { true }
override fun expectedOutputFileFor(inputFile: Path): Path {
val relativePath = inputDir.relativize(inputFile).toString()
return outputDir.resolve(relativePath.dropLast(3) + "yaml")
}
private val evaluator =
BinaryEvaluator(
StackFrameTransformers.empty,
SecurityManagers.defaultManager,
HttpClient.dummyClient(),
Loggers.stdErr(),
listOf(ModuleKeyFactories.file),
listOf(),
mapOf(),
mapOf(),
null,
null,
null,
null,
TraceMode.COMPACT,
)
private fun String.stripFilePaths() =
replace(snippetsDir.toUri().toString(), "file:///\$snippetsDir/")
override fun generateOutputFor(inputFile: Path): Pair<Boolean, String> {
val bytes = evaluator.evaluate(ModuleSource.path(inputFile), null)
return true to bytes.debugRendering.stripFilePaths()
}
}
val ByteArray.debugRendering: String
get() = MessagePackDebugRenderer(this).output

View File

@@ -1,154 +0,0 @@
/*
* 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.
* 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.server
import java.nio.file.Path
import java.util.regex.Pattern
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.pkl.core.*
import org.pkl.core.evaluatorSettings.TraceMode
import org.pkl.core.http.HttpClient
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.resource.ResourceReaders
class BinaryEvaluatorTest {
private val evaluator =
BinaryEvaluator(
StackFrameTransformers.defaultTransformer,
SecurityManagers.standard(
listOf(Pattern.compile(".*")),
listOf(Pattern.compile(".*")),
SecurityManagers.defaultTrustLevels,
Path.of(""),
),
HttpClient.dummyClient(),
Loggers.noop(),
listOf(ModuleKeyFactories.standardLibrary),
listOf(ResourceReaders.environmentVariable(), ResourceReaders.externalProperty()),
mapOf(),
mapOf(),
null,
null,
null,
null,
TraceMode.COMPACT,
)
private fun evaluate(text: String, expression: String?) =
evaluator.evaluate(ModuleSource.text(text), expression)
@Test
fun `evaluate whole module`() {
val bytes = evaluate("foo = 1", null)
assertThat(bytes.debugRendering)
.isEqualTo(
"""
- 1
- text
- repl:text
-
-
- 16
- foo
- 1
"""
.trimIndent()
)
}
@Test
fun `evaluate subpath`() {
val bytes =
evaluate(
"""
foo {
bar = 2
}
"""
.trimIndent(),
"foo.bar",
)
assertThat(bytes.asInt()).isEqualTo(2)
}
@Test
fun `evaluate output text`() {
val bytes =
evaluate(
"""
foo {
bar = 2
}
output {
renderer = new YamlRenderer {}
}
"""
.trimIndent(),
"output.text",
)
assertThat(bytes.asString())
.isEqualTo(
"""
foo:
bar: 2
"""
.trimIndent()
)
}
@Test
fun `evaluate let expression`() {
val bytes = evaluate("foo = 1", "let (bar = 2) foo + bar")
assertThat(bytes.asInt()).isEqualTo(3)
}
@Test
fun `evaluate import expression`() {
val bytes = evaluate("", """import("pkl:release").current.documentation.homepage""")
assertThat(bytes.asString()).startsWith("https://pkl-lang.org/")
}
@Test
fun `evaluate expression with invalid syntax`() {
val error = assertThrows<PklException> { evaluate("foo = 1", "<>!!!") }
assertThat(error).hasMessageContaining("Unexpected token")
assertThat(error).hasMessageContaining("<>!!!")
}
@Test
fun `evaluate non-expression`() {
val error = assertThrows<PklException> { evaluate("bar = 2", "bar = 15") }
assertThat(error).hasMessageContaining("Unexpected token")
assertThat(error).hasMessageContaining("bar = 15")
}
@Test
fun `evaluate semantically invalid expression`() {
val error = assertThrows<PklException> { evaluate("foo = 1", "foo as String") }
assertThat(error).hasMessageContaining("Expected value of type `String`, but got type `Int`")
}
}

View File

@@ -1,107 +0,0 @@
/*
* 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.
* 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.server
import java.lang.IllegalStateException
import org.msgpack.core.MessagePack
import org.msgpack.core.MessageUnpacker
import org.msgpack.value.ValueType
import org.pkl.core.util.yaml.YamlEmitter
/** Renders MessagePack structures in YAML. */
class MessagePackDebugRenderer(bytes: ByteArray) {
private val unpacker: MessageUnpacker = MessagePack.newDefaultUnpacker(bytes)
private val currIndent = StringBuilder("")
private val sb = StringBuilder()
private val indent = " "
private val yamlEmitter = YamlEmitter.create(sb, "1.2", indent)
private fun incIndent() {
currIndent.append(indent)
}
private fun decIndent() {
currIndent.setLength(currIndent.length - indent.length)
}
private fun newline() {
sb.append("\n")
sb.append(currIndent)
}
private fun renderKey() {
val mf = unpacker.nextFormat
when (mf.valueType!!) {
ValueType.STRING -> yamlEmitter.emit(unpacker.unpackString(), currIndent, true)
ValueType.MAP,
ValueType.ARRAY -> {
sb.append("? ")
incIndent()
renderValue()
decIndent()
newline()
}
else -> renderValue()
}
sb.append(": ")
}
private fun renderValue() {
val mf = unpacker.nextFormat
when (mf.valueType!!) {
ValueType.INTEGER,
ValueType.FLOAT,
ValueType.BOOLEAN,
ValueType.NIL -> sb.append(unpacker.unpackValue().toJson())
ValueType.STRING -> yamlEmitter.emit(unpacker.unpackString(), currIndent, false)
ValueType.ARRAY -> {
val size = unpacker.unpackArrayHeader()
if (size == 0) {
sb.append("[]")
return
}
for (i in 0 until size) {
newline()
sb.append("- ")
incIndent()
renderValue()
decIndent()
}
}
ValueType.MAP -> {
val size = unpacker.unpackMapHeader()
if (size == 0) {
sb.append("{}")
return
}
for (i in 0 until size) {
newline()
renderKey()
incIndent()
renderValue()
decIndent()
}
}
ValueType.BINARY,
ValueType.EXTENSION -> throw IllegalStateException("Unexpected value type ${mf.valueType}")
}
}
val output: String by lazy {
renderValue()
sb.toString().removePrefix("\n")
}
}

View File

@@ -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,6 +20,7 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
import org.msgpack.core.MessagePack
import org.msgpack.value.ImmutableValue
import org.pkl.commons.test.MessagePackDebugRenderer
fun ByteArray.unpack(): ImmutableValue = MessagePack.newDefaultUnpacker(this).unpackValue()
@@ -27,6 +28,9 @@ fun ByteArray.asInt(): Int = unpack().asIntegerValue().asInt()
fun ByteArray.asString(): String = unpack().asStringValue().asString()
val ByteArray.debugRendering: String
get() = MessagePackDebugRenderer(this).output
fun createDirectExecutor(): ExecutorService =
object : AbstractExecutorService() {
override fun execute(command: Runnable) {

View File

@@ -1 +0,0 @@
org.pkl.server.BinaryEvaluatorSnippetTestEngine