Pkl 0.28 Release Notes

Pkl 0.28 was released on February 26th, 2025.
The latest bugfix release is 0.28.0. (All Versions)

The next release (0.29) is scheduled for June 2025. To see what’s coming in the future, follow the Pkl Roadmap.

Please send feedback and questions to GitHub Discussions, or submit an issue on GitHub.

Pkl is hosted on GitHub. To get started, follow Installation.

Highlights 💖

News you don’t want to miss.

New parser

Pkl has new parser (#917, #957, #962)!

The first step to evaluating a program is to parse its source code into a parse tree. Currently, Pkl uses ANTLR to generate parser code from grammar files.

Using this approach has allowed for rapidly iterating and changing the language’s grammar. However, Pkl’s grammar has matured; it’s not as likely that the grammar will change from one version to the next. Additionally, the current parser’s performance has become more painful as projects written in Pkl grow in size. In many cases, parsing can take up most of the time spent during evaluation.

In 0.28, the ANTLR-generated parser is being replaced by a handwritten parser. In benchmarking tests, this has shown to improve parsing speed by around two orders of magnitude. Here are some quick comparisons between the two parsers, run on a MacBook Pro M4 Max machine, with 5 warmup iterations, and run for 10 iterations:

Old parser New parser

Language snippet tests

440.8ms

5.9ms

Standard library

136.7ms[1]

0.8ms

All pkl-pantry modules

1424.2ms

7.0ms

Noteworthy 🎶

Ready when you need them.

CLI improvements

Colored help messages

The pkl, pkldoc, pkl-codegen-java, and pkl-codegen-kotlin CLIs now emits help text with colors (#947).

Here is a sneak peek:

new pkl cli help output

Thanks to @gordonbondon for their contribution!

jpkl support on Windows

The jpkl executable is now supported on Windows (#872).

jpkl is a fat jar that can be executed directly when on macOS and Linux, but current users of Windows are required to execute it with java -jar <path/to/jpkl>.

In 0.28, Windows users can also call execute this command directly. To do so, the filename should be called jpkl.bat, and placed in PATH.

Type constraint changes

Type annotations that are executed as a result of calling a constraint expressions now perform an eager typecheck (#964).

In Pkl 0.27, we changed how typechecks are executed for Listing and Mapping types.

However, this had two unintended side effects:

For one, error messages from certain failing constraints show ? in place of values that are failing.

The following snippet:

class Bird { name: String }

bird: Listing<Bird>(firstOneIsQuail) = new {
  new { name = "Pigeon" }
  new { name = "Quail" }
}

local firstOneIsQuail = (it: Listing<Bird>) -> it[0].name == "Quail"

Produces the following error message:

–– Pkl Error ––
Type constraint `firstOneIsQuail` violated.
Value: new Listing { ?; ? }

3 | bird: Listing<Bird>(firstOneIsQuail) = new {
                        ^^^^^^^^^^^^^^^

This is happening because lazy typechecks of mappings and listings will return a new listing. Because the lambda’s argument is defined as it: Listing<Bird>, the value being passed into the function body is this new listing[2].

Secondly, some constraints became less strict, allowing more values to pass.

The following snippet fails in Pkl 0.26, but passes in Pkl 0.27:

class Bird { name: String }

local nonEmpty = (it: Listing<Bird>) -> !it.isEmpty

birds: Listing(nonEmpty) = new { 1; 2; 3 }

This is because the function parameter it: Listing<Bird> does not actually check members of the argument until they are accessed, and the function body !it.isEmpty does not access any members.

To address both of these issues, the rule for executing type constraints has changed. When executing a type annotation, mapping and listing typechecks are always eager. This means that Pkl will check that each member of the mapping/listing conforms to the type parameter.

Java 22+ support

Pkl’s Java libraries now support Java 22 and higher (#876).

Thanks to @sgammon for their contribution!

New standard library method

The pkl:math standard library module has a new method, called atan2 (#819). It computes the 2-argument arctangent.

Thanks to @gordonbondon for their contribution!

Overhauled for-generator implementation

The implementation of for-generators has been overhauled (#844).

This fixes some known bugs with the current for-generator implementation, and also improves code health by reducing complexity and removing workarounds and band-aids.

Thanks to @odenix for their contribution!

Java code generator improvements

Improvements have been made to the Java code generator (#792).

In pkl-codegen-java, the --params-annotation flag now accepts none as a value, which causes the code generator to skip adding annotations to a constructor’s parameters. Additionally, the Kotlin API org.pkl.codegen.java.JavaCodeGeneratorOptions.paramsAnnotation is now nullable. If null, this also skips adding annotations.

By default, this annotation is set to org.pkl.config.java.mapper.Named, or none if the --generate-spring-boot-config flag is set.

This is useful for those compiling Java code with the -parameters flag set, because the pkl-config-java library is able to map into these classes without any extra annotations.

Thanks to @odenix for their contribution!

Kotlin code generator improvements

Improvements have been made to the Kotlin code generator (#793).

Currently, the output of toString() on a generated Kotlin class differs depending on if it was generated as a data class or as a regular class.

In Pkl 0.28, this method produces the same format, and matches the format used by data classes.

Thanks to @odenix for their contribution!

pkldoc improvements

pkldoc has a new flag to write real files instead of symlinks (#824).

Currently, pkldoc creates a symlink called "current", which points to the latest non-prerelease version of a package. However, these symlinks can be problematic for some systems.

A new flag, --no-symlinks, and a similarly named Gradle property, noSymlinks, instructs pkldoc to write real files instead of symlinks.

Thanks to @netvl for their contributions!

jar:nested: URIs are accepted by default

To improve integration with Spring Boot, the Pkl evaluator by default now accepts module URIs starting with jar:nested: (#895).

The --allowed-modules flag (and the equally named option in other APIs) can be used to configure the set of allowed modules.

Breaking Changes 💔

Things to watch out for when upgrading.

Stricter grammar

As a result of implementing a new parser, the following previously accepted grammar is no longer valid (#917).

Inline object entries

When declaring multiple entries on the same line, a semicolon is now required to separate them.

The following code snippet is currently valid, and becomes a syntax error in 0.28.

obj { ["one"] = 1 ["two"] = 2 }

To fix, insert a semicolon between the two entries:

-obj { ["one"] = 1 ["two"] = 2 }
+obj { ["one"] = 1; ["two"] = 2 }

String escapes

When using custom string delimiters, the escape sequence becomes backslash (\) plus the number of pounds used to delimit the string.

Due to a parser bug, any extra pounds after this escape sequence are also permitted.

In Pkl 0.28, this becomes a syntax error.

foo = "foo \#(bar)" // Invalid character escape sequence `\#`.

Whitespace between generator members

Inline object spreads, for generators, and when generators all now require either whitespace or a semicolon separator.

foo { ...bar...baz } // Syntax error

Minimum Gradle version bump

The minimum Gradle version for the Gradle Plugin has been bumped to 8.2 (#901).

Minimum Kotlin version bump

For users of Pkl’s Kotlin libraries, the minimum Kotlin version has been bumped to 2.0 (#900).

Java/Kotlin API breaking changes

Breaking changes have been made to the Java/Kotlin APIs (#748, #749, #776, #793, #808, #810).

API Breakage

org.pkl.core.resource.Resource

Converted into a record; previous getter methods are deprecated.

org.pkl.core.project.Package

Converted into a record; previous getter methods are deprecated.

org.pkl.core.Member.SourceLocation

Converted into a record; previous getter methods are deprecated.

org.pkl.core.Release

Converted into a record; previous getter methods are deprecated.

org.pkl.core.Release.SourceCode

Converted into a record; previous getter methods are deprecated.

org.pkl.core.Release.Documentation

Converted into a record; previous getter methods are deprecated.

org.pkl.core.Release.StandardLibrary

Converted into a record; previous getter methods are deprecated.

org.pkl.core.project.DeclaredDependencies

Converted into a record; previous getter methods are deprecated.

org.pkl.core.evaluatorSettings.PklEvaluatorSettings.Proxy.create

Deprecated; use the constructor instead.

org.pkl.core.module.ExternalModuleResolver

Moved into package org.pkl.core.externalreader

org.pkl.core.resource.ExternalResourceResolver

Moved into package org.pkl.core.externalreader

org.pkl.core.messaging.MessageTransportModuleResolver

Renamed to ExternalModuleResolverImpl, made package-private

org.pkl.core.messaging.MessageTransportResourceResolver

Renamed to ExternalResourceResolverImpl, made package-private

org.pkl.core.module.ExternalModuleResolver.Spec

Replaced with record org.pkl.core.externalreader.ModuleReaderSpec

org.pkl.core.resource.ExternalResourceResolver.Spec

Replaced with record org.pkl.core.externalreader.ResourceReaderSpec

org.pkl.core.messaging.CreateEvaluatorRequest

Changed properties allowedModules and allowedResources to be of type List<String> instead of List<Pattern>

org.pkl.codegen.kotlin.KotlinCodegenOptions

Renamed to org.pkl.codegen.kotlin.KotlinCodeGeneratorOptions

org.pkl.codegen.kotlin.CliKotlinCodeGeneratorOptions.toKotlinCodegenOptions

Deprecated without replacement

org.pkl.codegen.java.JavaCodegenOptions

Renamed to org.pkl.codegen.java.JavaCodeGeneratorOptions

org.pkl.codegen.java.CliJavaCodeGeneratorOptions.toJavaCodegenOptions()

Deprecated without replacement

Fat jars no longer shade Truffle

Pkl publishes two fat jars to Maven Central: pkl-tools, and pkl-config-java-all.

Due a version bump in the Truffle library, the package com.oracle.truffle can no longer be shaded.

For users that use both Pkl and other Truffle languages, this means that their version of Truffle should match Pkl’s version.

Miscellaneous 🐸

  • Documentation improvements (#792, #846, #860, #892, #921, #937, #943, #944, #955, #956, #973).

  • The repository now requires Java 21 or higher to build (#876).

  • Enable multi-jdk testing via -DmultiJdkTesting=true flag when building Pkl (#876).

  • Allow setting commit id via -DcommitId flag when building Pkl (#954).

Bugs fixed 🐜

The following bugs have been fixed.

  • Optimization: const members should be cached for all children in the prototype chain (#508)

  • codegen-kotlin: Use same toString() representation for data classes and regular classes (#717)

  • Late-bound values of iteratees within nested for/spread fail to resolve for-generator variables (#741)

  • codegen-java/kotlin: Fix generation of equals/hashCode methods (#802)

  • Not possible to render mapping with Int keys in YAML (#878)

  • Parser accepts wrong string escapes (#888)

  • Downstream native-image embedders are broken (#907)

  • Failed type constraint error messages do not show forced members (#918)

  • ANTLR incompatibilities (#927)

  • Doc comments with interleaving comments result in an error (#931)

  • Spread elements inside an object body don’t need separators (#932)

  • Correctly set allowed modules/resoures when external reader scheme contain regex control characters (#941)

  • Bad import analysis fails with "None (cause has no message)" (#949)

Contributors 🙏

We would like to thank the contributors to this release (in alphabetical order):


1. In the pkl CLI, the standard library is parsed during compilation rather than during evaluation. As a result, there is no parsing overhead.
2. See delegating objects in SPICE-0010 for more details on this behavior.