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
@@ -1,7 +0,0 @@
res1 = "bar"
res2 = ""
res3 = 1
res4 = 2.3
res5 = true
res6 = false
res7 = null
@@ -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
@@ -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
@@ -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
@@ -1,2 +0,0 @@
res1 = IntSeq(1, 3)
res2 = IntSeq(1, 4).step(5)
-6
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 } }
-21
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" }
}
-2
View File
@@ -1,2 +0,0 @@
res1 = Pair(1, 2)
res2 = Pair("foo", "bar")
@@ -1,3 +0,0 @@
res1 = Regex("abc")
res2 = Regex("")
res3 = Regex("(?m)^abc$")
-3
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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,20 +0,0 @@
- 1
- intseq
- file:///$snippetsDir/input/intseq.pkl
-
-
- 16
- res1
-
- 10
- 1
- 3
- 1
-
- 16
- res2
-
- 10
- 1
- 4
- 5
-58
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
-68
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
-18
View File
@@ -1,18 +0,0 @@
- 1
- pair
- file:///$snippetsDir/input/pair.pkl
-
-
- 16
- res1
-
- 9
- 1
- 2
-
- 16
- res2
-
- 9
- foo
- bar
@@ -1,22 +0,0 @@
- 1
- regex
- file:///$snippetsDir/input/regex.pkl
-
-
- 16
- res1
-
- 11
- abc
-
- 16
- res2
-
- 11
- ''
-
- 16
- res3
-
- 11
- (?m)^abc$
-30
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
@@ -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(),
@@ -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
@@ -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`")
}
}
@@ -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")
}
}
@@ -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) {
@@ -1 +0,0 @@
org.pkl.server.BinaryEvaluatorSnippetTestEngine