commit ecad035dcaf8f49eba08bf816eba76bd581c87d3 Author: Peter Niederwieser Date: Tue Jan 19 14:51:19 2016 +0100 Initial commit diff --git a/.circleci/config.pkl b/.circleci/config.pkl new file mode 100644 index 00000000..e985145b --- /dev/null +++ b/.circleci/config.pkl @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// +// File gets rendered to .circleci/config.yml via git hook. +amends ".../pkl-project-commons/packages/pkl.impl.circleci/PklCI.pkl" + +import "jobs/BuildNativeJob.pkl" +import "jobs/GradleCheckJob.pkl" +import "jobs/DeployJob.pkl" +import "jobs/SimpleGradleJob.pkl" + +local prbJobs: Listing = gradleCheckJobs.keys.toListing() + +local buildAndTestJobs = (prbJobs) { + "bench" +// "gradle-compatibility" + ...buildNativeJobs.keys.filter((it) -> it.endsWith("snapshot")) +} + +local releaseJobs = (prbJobs) { + "bench" +// "gradle-compatibility" + ...buildNativeJobs.keys.filter((it) -> it.endsWith("release")) +} + +prb { + jobs = prbJobs +} + +main { + jobs { + ...buildAndTestJobs + new { + ["deploy-snapshot"] { + requires = buildAndTestJobs + context = "pkl-maven-release" + } + } + } +} + +release { + jobs { + ...releaseJobs + // do GitHub release first because we can overwrite the tag. + // publishing to Maven Central is final. + new { + ["github-release"] { + requires = releaseJobs + context = "pkl-github-release" + } + } + new { + ["deploy-release"] { + requires { "github-release" } + context = "pkl-maven-release" + } + } + } +} + +triggerDocsBuild = "both" + +triggerPackageDocsBuild = "release" + +local buildNativeJobs: Mapping = new { + for (_dist in List("release", "snapshot")) { + for (_arch in List("amd64", "aarch64")) { + for (_os in List("macOS", "linux")) { + ["pkl-cli-\(_os)-\(_arch)-\(_dist)"] { + arch = _arch + os = _os + isRelease = _dist == "release" + } + } + } + ["pkl-cli-linux-alpine-amd64-\(_dist)"] { + arch = "amd64" + os = "linux" + musl = true + isRelease = _dist == "release" + } + } +} + +local gradleCheckJobs: Mapping = new { + ["gradle-check-jdk11"] { + javaVersion = "11.0" + isRelease = false + } + ["gradle-check-jdk17"] { + javaVersion = "17.0" + isRelease = false + } +} + +jobs { + for (jobName, job in buildNativeJobs) { + [jobName] = job.job + } + for (jobName, job in gradleCheckJobs) { + [jobName] = job.job + } + ["bench"] = new SimpleGradleJob { command = "bench:jmh" }.job + ["gradle-compatibility"] = new SimpleGradleJob { + name = "gradle compatibility" + command = #""" + :pkl-gradle:build \ + :pkl-gradle:compatibilityTestReleases \ + :pkl-gradle:compatibilityTestCandidate + """# + }.job + ["deploy-snapshot"] = new DeployJob { + command = "publishToSonatype" + }.job + ["deploy-release"] = new DeployJob { + isRelease = true + command = "publishToSonatype closeAndReleaseSonatypeStagingRepository" + }.job + ["github-release"] { + docker { + new { + image = "maniator/gh:v2.40.1" + } + } + steps { + new AttachWorkspaceStep { at = "." } + new RunStep { + name = "Publish release on GitHub" + command = #""" + gh release create "${CIRCLE_TAG}" \ + --title "${CIRCLE_TAG}" \ + --target "${CIRCLE_SHA1}" \ + --verify-tag \ + --notes "Release notes: https://pkl-lang.org/main/current/release-notes/changelog.html#release-${CIRCLE_TAG}" \ + --repo "${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" \ + pkl-cli/build/executable/* + """# + } + } + } +} diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..58e76a70 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,870 @@ +# Generated from CircleCI.pkl. DO NOT EDIT. +version: '2.1' +orbs: + pr-approval: apple/pr-approval@0.1.0 +jobs: + pkl-cli-macOS-amd64-release: + steps: + - checkout + - run: + command: /usr/sbin/softwareupdate --install-rosetta --agree-to-license + name: Installing Rosetta 2 + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:macExecutableAmd64 pkl-core:testMacExecutableAmd64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + resource_class: macos.m1.large.gen1 + macos: + xcode: 15.2.0 + pkl-cli-linux-amd64-release: + steps: + - checkout + - restore_cache: + key: staticdeps-amd64 + name: Restore static deps from cache + - run: + command: |- + sed -ie '/\[ol8_codeready_builder\]/,/^$/s/enabled=0/enabled=1/g' /etc/yum.repos.d/oracle-linux-ol8.repo \ + && microdnf -y install util-linux tree coreutils-single findutils curl tar gzip git zlib-devel gcc-c++ make openssl glibc-langpack-en libstdc++-static \ + && microdnf clean all \ + && rm -rf /var/cache/dnf + + # install jdk + curl -L \ + https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.20.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.20.1_1.tar.gz -o /tmp/jdk.tar.gz + + mkdir /jdk \ + && cd /jdk \ + && cat /tmp/jdk.tar.gz | tar --strip-components=1 -xzC . + + mkdir -p ~/staticdeps/bin + + cp /usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.a ~/staticdeps + + # install zlib + if [[ ! -f ~/staticdeps/include/zlib.h ]]; then + curl -L https://github.com/madler/zlib/releases/download/v1.2.13/zlib-1.2.13.tar.gz -o /tmp/zlib.tar.gz + + mkdir -p /tmp/dep_zlib-1.2.13 \ + && cd /tmp/dep_zlib-1.2.13 \ + && cat /tmp/zlib.tar.gz | tar --strip-components=1 -xzC . \ + && echo "zlib-1.2.13: configure..." && ./configure --static --prefix="$HOME"/staticdeps > /dev/null \ + && echo "zlib-1.2.13: make..." && make -s -j4 \ + && echo "zlib-1.2.13: make install..." && make -s install \ + && rm -rf /tmp/dep_zlib-1.2.13 + fi + + # install musl + if [[ ! -f ~/staticdeps/bin/x86_64-linux-musl-gcc ]]; then + curl -L https://musl.libc.org/releases/musl-1.2.2.tar.gz -o /tmp/musl.tar.gz + + mkdir -p /tmp/dep_musl-1.2.2 \ + && cd /tmp/dep_musl-1.2.2 \ + && cat /tmp/musl.tar.gz | tar --strip-components=1 -xzC . \ + && echo "musl-1.2.2: configure..." && ./configure --disable-shared --prefix="$HOME"/staticdeps > /dev/null \ + && echo "musl-1.2.2: make..." && make -s -j4 \ + && echo "musl-1.2.2: make install..." && make -s install \ + && rm -rf /tmp/dep_musl-1.2.2 + + # native-image expects to find an executable at this path. + ln -s ~/staticdeps/bin/musl-gcc ~/staticdeps/bin/x86_64-linux-musl-gcc + fi + name: Set up environment + shell: '#!/bin/bash -exo pipefail' + - save_cache: + paths: + - ~/staticdeps + key: staticdeps-amd64 + name: Save statics deps to cache + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:linuxExecutableAmd64 pkl-core:testLinuxExecutableAmd64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + JAVA_HOME: /jdk + resource_class: xlarge + docker: + - image: oraclelinux:8-slim + pkl-cli-macOS-aarch64-release: + steps: + - checkout + - run: + command: git apply patches/graalVm23.patch + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:macExecutableAarch64 pkl-core:testMacExecutableAarch64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + resource_class: macos.m1.large.gen1 + macos: + xcode: 15.2.0 + pkl-cli-linux-aarch64-release: + steps: + - checkout + - restore_cache: + key: staticdeps-aarch64 + name: Restore static deps from cache + - run: + command: |- + sed -ie '/\[ol8_codeready_builder\]/,/^$/s/enabled=0/enabled=1/g' /etc/yum.repos.d/oracle-linux-ol8.repo \ + && microdnf -y install util-linux tree coreutils-single findutils curl tar gzip git zlib-devel gcc-c++ make openssl glibc-langpack-en libstdc++-static \ + && microdnf clean all \ + && rm -rf /var/cache/dnf + + # install jdk + curl -L \ + https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.20.1%2B1/OpenJDK11U-jdk_aarch64_linux_hotspot_11.0.20.1_1.tar.gz -o /tmp/jdk.tar.gz + + mkdir /jdk \ + && cd /jdk \ + && cat /tmp/jdk.tar.gz | tar --strip-components=1 -xzC . + + mkdir -p ~/staticdeps/bin + + cp /usr/lib/gcc/aarch64-redhat-linux/8/libstdc++.a ~/staticdeps + + # install zlib + if [[ ! -f ~/staticdeps/include/zlib.h ]]; then + curl -L https://github.com/madler/zlib/releases/download/v1.2.13/zlib-1.2.13.tar.gz -o /tmp/zlib.tar.gz + + mkdir -p /tmp/dep_zlib-1.2.13 \ + && cd /tmp/dep_zlib-1.2.13 \ + && cat /tmp/zlib.tar.gz | tar --strip-components=1 -xzC . \ + && echo "zlib-1.2.13: configure..." && ./configure --static --prefix="$HOME"/staticdeps > /dev/null \ + && echo "zlib-1.2.13: make..." && make -s -j4 \ + && echo "zlib-1.2.13: make install..." && make -s install \ + && rm -rf /tmp/dep_zlib-1.2.13 + fi + name: Set up environment + shell: '#!/bin/bash -exo pipefail' + - save_cache: + paths: + - ~/staticdeps + key: staticdeps-aarch64 + name: Save statics deps to cache + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:linuxExecutableAarch64 pkl-core:testLinuxExecutableAarch64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + JAVA_HOME: /jdk + resource_class: arm.xlarge + docker: + - image: arm64v8/oraclelinux:8-slim + pkl-cli-linux-alpine-amd64-release: + steps: + - checkout + - restore_cache: + key: staticdeps-amd64 + name: Restore static deps from cache + - run: + command: |- + sed -ie '/\[ol8_codeready_builder\]/,/^$/s/enabled=0/enabled=1/g' /etc/yum.repos.d/oracle-linux-ol8.repo \ + && microdnf -y install util-linux tree coreutils-single findutils curl tar gzip git zlib-devel gcc-c++ make openssl glibc-langpack-en libstdc++-static \ + && microdnf clean all \ + && rm -rf /var/cache/dnf + + # install jdk + curl -L \ + https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.20.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.20.1_1.tar.gz -o /tmp/jdk.tar.gz + + mkdir /jdk \ + && cd /jdk \ + && cat /tmp/jdk.tar.gz | tar --strip-components=1 -xzC . + + mkdir -p ~/staticdeps/bin + + cp /usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.a ~/staticdeps + + # install zlib + if [[ ! -f ~/staticdeps/include/zlib.h ]]; then + curl -L https://github.com/madler/zlib/releases/download/v1.2.13/zlib-1.2.13.tar.gz -o /tmp/zlib.tar.gz + + mkdir -p /tmp/dep_zlib-1.2.13 \ + && cd /tmp/dep_zlib-1.2.13 \ + && cat /tmp/zlib.tar.gz | tar --strip-components=1 -xzC . \ + && echo "zlib-1.2.13: configure..." && ./configure --static --prefix="$HOME"/staticdeps > /dev/null \ + && echo "zlib-1.2.13: make..." && make -s -j4 \ + && echo "zlib-1.2.13: make install..." && make -s install \ + && rm -rf /tmp/dep_zlib-1.2.13 + fi + + # install musl + if [[ ! -f ~/staticdeps/bin/x86_64-linux-musl-gcc ]]; then + curl -L https://musl.libc.org/releases/musl-1.2.2.tar.gz -o /tmp/musl.tar.gz + + mkdir -p /tmp/dep_musl-1.2.2 \ + && cd /tmp/dep_musl-1.2.2 \ + && cat /tmp/musl.tar.gz | tar --strip-components=1 -xzC . \ + && echo "musl-1.2.2: configure..." && ./configure --disable-shared --prefix="$HOME"/staticdeps > /dev/null \ + && echo "musl-1.2.2: make..." && make -s -j4 \ + && echo "musl-1.2.2: make install..." && make -s install \ + && rm -rf /tmp/dep_musl-1.2.2 + + # native-image expects to find an executable at this path. + ln -s ~/staticdeps/bin/musl-gcc ~/staticdeps/bin/x86_64-linux-musl-gcc + fi + name: Set up environment + shell: '#!/bin/bash -exo pipefail' + - save_cache: + paths: + - ~/staticdeps + key: staticdeps-amd64 + name: Save statics deps to cache + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:alpineExecutableAmd64 pkl-core:testAlpineExecutableAmd64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + JAVA_HOME: /jdk + resource_class: xlarge + docker: + - image: oraclelinux:8-slim + pkl-cli-macOS-amd64-snapshot: + steps: + - checkout + - run: + command: /usr/sbin/softwareupdate --install-rosetta --agree-to-license + name: Installing Rosetta 2 + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace pkl-cli:macExecutableAmd64 pkl-core:testMacExecutableAmd64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + resource_class: macos.m1.large.gen1 + macos: + xcode: 15.2.0 + pkl-cli-linux-amd64-snapshot: + steps: + - checkout + - restore_cache: + key: staticdeps-amd64 + name: Restore static deps from cache + - run: + command: |- + sed -ie '/\[ol8_codeready_builder\]/,/^$/s/enabled=0/enabled=1/g' /etc/yum.repos.d/oracle-linux-ol8.repo \ + && microdnf -y install util-linux tree coreutils-single findutils curl tar gzip git zlib-devel gcc-c++ make openssl glibc-langpack-en libstdc++-static \ + && microdnf clean all \ + && rm -rf /var/cache/dnf + + # install jdk + curl -L \ + https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.20.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.20.1_1.tar.gz -o /tmp/jdk.tar.gz + + mkdir /jdk \ + && cd /jdk \ + && cat /tmp/jdk.tar.gz | tar --strip-components=1 -xzC . + + mkdir -p ~/staticdeps/bin + + cp /usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.a ~/staticdeps + + # install zlib + if [[ ! -f ~/staticdeps/include/zlib.h ]]; then + curl -L https://github.com/madler/zlib/releases/download/v1.2.13/zlib-1.2.13.tar.gz -o /tmp/zlib.tar.gz + + mkdir -p /tmp/dep_zlib-1.2.13 \ + && cd /tmp/dep_zlib-1.2.13 \ + && cat /tmp/zlib.tar.gz | tar --strip-components=1 -xzC . \ + && echo "zlib-1.2.13: configure..." && ./configure --static --prefix="$HOME"/staticdeps > /dev/null \ + && echo "zlib-1.2.13: make..." && make -s -j4 \ + && echo "zlib-1.2.13: make install..." && make -s install \ + && rm -rf /tmp/dep_zlib-1.2.13 + fi + + # install musl + if [[ ! -f ~/staticdeps/bin/x86_64-linux-musl-gcc ]]; then + curl -L https://musl.libc.org/releases/musl-1.2.2.tar.gz -o /tmp/musl.tar.gz + + mkdir -p /tmp/dep_musl-1.2.2 \ + && cd /tmp/dep_musl-1.2.2 \ + && cat /tmp/musl.tar.gz | tar --strip-components=1 -xzC . \ + && echo "musl-1.2.2: configure..." && ./configure --disable-shared --prefix="$HOME"/staticdeps > /dev/null \ + && echo "musl-1.2.2: make..." && make -s -j4 \ + && echo "musl-1.2.2: make install..." && make -s install \ + && rm -rf /tmp/dep_musl-1.2.2 + + # native-image expects to find an executable at this path. + ln -s ~/staticdeps/bin/musl-gcc ~/staticdeps/bin/x86_64-linux-musl-gcc + fi + name: Set up environment + shell: '#!/bin/bash -exo pipefail' + - save_cache: + paths: + - ~/staticdeps + key: staticdeps-amd64 + name: Save statics deps to cache + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace pkl-cli:linuxExecutableAmd64 pkl-core:testLinuxExecutableAmd64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + JAVA_HOME: /jdk + resource_class: xlarge + docker: + - image: oraclelinux:8-slim + pkl-cli-macOS-aarch64-snapshot: + steps: + - checkout + - run: + command: git apply patches/graalVm23.patch + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace pkl-cli:macExecutableAarch64 pkl-core:testMacExecutableAarch64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + resource_class: macos.m1.large.gen1 + macos: + xcode: 15.2.0 + pkl-cli-linux-aarch64-snapshot: + steps: + - checkout + - restore_cache: + key: staticdeps-aarch64 + name: Restore static deps from cache + - run: + command: |- + sed -ie '/\[ol8_codeready_builder\]/,/^$/s/enabled=0/enabled=1/g' /etc/yum.repos.d/oracle-linux-ol8.repo \ + && microdnf -y install util-linux tree coreutils-single findutils curl tar gzip git zlib-devel gcc-c++ make openssl glibc-langpack-en libstdc++-static \ + && microdnf clean all \ + && rm -rf /var/cache/dnf + + # install jdk + curl -L \ + https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.20.1%2B1/OpenJDK11U-jdk_aarch64_linux_hotspot_11.0.20.1_1.tar.gz -o /tmp/jdk.tar.gz + + mkdir /jdk \ + && cd /jdk \ + && cat /tmp/jdk.tar.gz | tar --strip-components=1 -xzC . + + mkdir -p ~/staticdeps/bin + + cp /usr/lib/gcc/aarch64-redhat-linux/8/libstdc++.a ~/staticdeps + + # install zlib + if [[ ! -f ~/staticdeps/include/zlib.h ]]; then + curl -L https://github.com/madler/zlib/releases/download/v1.2.13/zlib-1.2.13.tar.gz -o /tmp/zlib.tar.gz + + mkdir -p /tmp/dep_zlib-1.2.13 \ + && cd /tmp/dep_zlib-1.2.13 \ + && cat /tmp/zlib.tar.gz | tar --strip-components=1 -xzC . \ + && echo "zlib-1.2.13: configure..." && ./configure --static --prefix="$HOME"/staticdeps > /dev/null \ + && echo "zlib-1.2.13: make..." && make -s -j4 \ + && echo "zlib-1.2.13: make install..." && make -s install \ + && rm -rf /tmp/dep_zlib-1.2.13 + fi + name: Set up environment + shell: '#!/bin/bash -exo pipefail' + - save_cache: + paths: + - ~/staticdeps + key: staticdeps-aarch64 + name: Save statics deps to cache + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace pkl-cli:linuxExecutableAarch64 pkl-core:testLinuxExecutableAarch64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + JAVA_HOME: /jdk + resource_class: arm.xlarge + docker: + - image: arm64v8/oraclelinux:8-slim + pkl-cli-linux-alpine-amd64-snapshot: + steps: + - checkout + - restore_cache: + key: staticdeps-amd64 + name: Restore static deps from cache + - run: + command: |- + sed -ie '/\[ol8_codeready_builder\]/,/^$/s/enabled=0/enabled=1/g' /etc/yum.repos.d/oracle-linux-ol8.repo \ + && microdnf -y install util-linux tree coreutils-single findutils curl tar gzip git zlib-devel gcc-c++ make openssl glibc-langpack-en libstdc++-static \ + && microdnf clean all \ + && rm -rf /var/cache/dnf + + # install jdk + curl -L \ + https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.20.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.20.1_1.tar.gz -o /tmp/jdk.tar.gz + + mkdir /jdk \ + && cd /jdk \ + && cat /tmp/jdk.tar.gz | tar --strip-components=1 -xzC . + + mkdir -p ~/staticdeps/bin + + cp /usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.a ~/staticdeps + + # install zlib + if [[ ! -f ~/staticdeps/include/zlib.h ]]; then + curl -L https://github.com/madler/zlib/releases/download/v1.2.13/zlib-1.2.13.tar.gz -o /tmp/zlib.tar.gz + + mkdir -p /tmp/dep_zlib-1.2.13 \ + && cd /tmp/dep_zlib-1.2.13 \ + && cat /tmp/zlib.tar.gz | tar --strip-components=1 -xzC . \ + && echo "zlib-1.2.13: configure..." && ./configure --static --prefix="$HOME"/staticdeps > /dev/null \ + && echo "zlib-1.2.13: make..." && make -s -j4 \ + && echo "zlib-1.2.13: make install..." && make -s install \ + && rm -rf /tmp/dep_zlib-1.2.13 + fi + + # install musl + if [[ ! -f ~/staticdeps/bin/x86_64-linux-musl-gcc ]]; then + curl -L https://musl.libc.org/releases/musl-1.2.2.tar.gz -o /tmp/musl.tar.gz + + mkdir -p /tmp/dep_musl-1.2.2 \ + && cd /tmp/dep_musl-1.2.2 \ + && cat /tmp/musl.tar.gz | tar --strip-components=1 -xzC . \ + && echo "musl-1.2.2: configure..." && ./configure --disable-shared --prefix="$HOME"/staticdeps > /dev/null \ + && echo "musl-1.2.2: make..." && make -s -j4 \ + && echo "musl-1.2.2: make install..." && make -s install \ + && rm -rf /tmp/dep_musl-1.2.2 + + # native-image expects to find an executable at this path. + ln -s ~/staticdeps/bin/musl-gcc ~/staticdeps/bin/x86_64-linux-musl-gcc + fi + name: Set up environment + shell: '#!/bin/bash -exo pipefail' + - save_cache: + paths: + - ~/staticdeps + key: staticdeps-amd64 + name: Save statics deps to cache + - run: + command: |- + export PATH=~/staticdeps/bin:$PATH + ./gradlew --info --stacktrace pkl-cli:alpineExecutableAmd64 pkl-core:testAlpineExecutableAmd64 + name: gradle buildNative + - persist_to_workspace: + root: '.' + paths: + - pkl-cli/build/executable/ + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + JAVA_HOME: /jdk + resource_class: xlarge + docker: + - image: oraclelinux:8-slim + gradle-check-jdk11: + steps: + - checkout + - run: + command: ./gradlew --info --stacktrace check + name: gradle check + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + docker: + - image: cimg/openjdk:11.0 + gradle-check-jdk17: + steps: + - checkout + - run: + command: ./gradlew --info --stacktrace check + name: gradle check + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + docker: + - image: cimg/openjdk:17.0 + bench: + steps: + - checkout + - run: + command: ./gradlew --info --stacktrace bench:jmh + name: bench:jmh + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + docker: + - image: cimg/openjdk:11.0 + gradle-compatibility: + steps: + - checkout + - run: + command: |- + ./gradlew --info --stacktrace :pkl-gradle:build \ + :pkl-gradle:compatibilityTestReleases \ + :pkl-gradle:compatibilityTestCandidate + name: gradle compatibility + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + docker: + - image: cimg/openjdk:11.0 + deploy-snapshot: + steps: + - checkout + - attach_workspace: + at: '.' + - run: + command: ./gradlew --info --stacktrace publishToSonatype + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + docker: + - image: cimg/openjdk:11.0 + deploy-release: + steps: + - checkout + - attach_workspace: + at: '.' + - run: + command: ./gradlew --info --stacktrace -DreleaseBuild=true publishToSonatype closeAndReleaseSonatypeStagingRepository + - run: + command: |- + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \; + name: Gather test results + when: always + - store_test_results: + path: ~/test-results + environment: + LANG: en_US.UTF-8 + docker: + - image: cimg/openjdk:11.0 + github-release: + steps: + - attach_workspace: + at: '.' + - run: + command: |- + gh release create "${CIRCLE_TAG}" \ + --title "${CIRCLE_TAG}" \ + --target "${CIRCLE_SHA1}" \ + --verify-tag \ + --notes "Release notes: https://pkl-lang.org/main/current/release-notes/changelog.html#release-${CIRCLE_TAG}" \ + --repo "${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" \ + pkl-cli/build/executable/* + name: Publish release on GitHub + docker: + - image: maniator/gh:v2.40.1 + trigger-docsite-build: + steps: + - run: + command: |- + curl --location \ + --request POST \ + --header "Content-Type: application/json" \ + -u "${CIRCLE_TOKEN}:" \ + --data '{ "branch": "main" }' \ + "https://circleci.com/api/v2/project/github/apple/pkl-lang.org/pipeline" + name: Triggering docsite build + docker: + - image: cimg/base:current + trigger-package-docs-build: + steps: + - run: + command: |- + curl --location \ + --request POST \ + --header "Content-Type: application/json" \ + -u "${CIRCLE_TOKEN}:" \ + --data '{ "branch": "main" }' \ + "https://circleci.com/api/v2/project/github/apple/pkl-package-docs/pipeline" + name: Triggering docsite build + docker: + - image: cimg/base:current +workflows: + prb: + jobs: + - hold: + type: approval + - pr-approval/authenticate: + context: pkl-pr-approval + - gradle-check-jdk11: + requires: + - hold + - pr-approval/authenticate + - gradle-check-jdk17: + requires: + - hold + - pr-approval/authenticate + when: + matches: + value: << pipeline.git.branch >> + pattern: ^pull/\d+(/head)?$ + main: + jobs: + - gradle-check-jdk11 + - gradle-check-jdk17 + - bench + - pkl-cli-macOS-amd64-snapshot + - pkl-cli-linux-amd64-snapshot + - pkl-cli-macOS-aarch64-snapshot + - pkl-cli-linux-aarch64-snapshot + - pkl-cli-linux-alpine-amd64-snapshot + - deploy-snapshot: + requires: + - gradle-check-jdk11 + - gradle-check-jdk17 + - bench + - pkl-cli-macOS-amd64-snapshot + - pkl-cli-linux-amd64-snapshot + - pkl-cli-macOS-aarch64-snapshot + - pkl-cli-linux-aarch64-snapshot + - pkl-cli-linux-alpine-amd64-snapshot + context: pkl-maven-release + - trigger-docsite-build: + requires: + - deploy-snapshot + context: + - pkl-pr-approval + when: + equal: + - main + - << pipeline.git.branch >> + release: + jobs: + - gradle-check-jdk11: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - gradle-check-jdk17: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - bench: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - pkl-cli-macOS-amd64-release: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - pkl-cli-linux-amd64-release: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - pkl-cli-macOS-aarch64-release: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - pkl-cli-linux-aarch64-release: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - pkl-cli-linux-alpine-amd64-release: + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - github-release: + requires: + - gradle-check-jdk11 + - gradle-check-jdk17 + - bench + - pkl-cli-macOS-amd64-release + - pkl-cli-linux-amd64-release + - pkl-cli-macOS-aarch64-release + - pkl-cli-linux-aarch64-release + - pkl-cli-linux-alpine-amd64-release + context: pkl-github-release + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - deploy-release: + requires: + - github-release + context: pkl-maven-release + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ + - trigger-package-docs-build: + requires: + - deploy-release + context: + - pkl-pr-approval + filters: + branches: + ignore: /.*/ + tags: + only: /^v?\d+\.\d+\.\d+$/ diff --git a/.circleci/jobs/BuildNativeJob.pkl b/.circleci/jobs/BuildNativeJob.pkl new file mode 100644 index 00000000..3d835c5c --- /dev/null +++ b/.circleci/jobs/BuildNativeJob.pkl @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// +/// Builds the native `pkl` CLI +extends "GradleJob.pkl" + +// TODO(oss) replace these with package imports +import ".../pkl-pantry/packages/com.circleci.v2/CircleCI.pkl" +import ".../pkl-pantry/packages/pkl.experimental.uri/URI.pkl" + +/// The OS to run on +os: "macOS"|"linux" + +/// The architecture to use +arch: "amd64"|"aarch64" + +/// Whether to link to musl. Otherwise, links to glibc. +musl: Boolean = false + +local setupLinuxEnvironment: CircleCI.RunStep = + let (jdkVersion = "11.0.20.1+1") + let (muslVersion = "1.2.2") + let (zlibVersion = "1.2.13") + let (jdkVersionEncoded = URI.encodeComponent(jdkVersion)) + let (jdkVersionAlt = jdkVersion.replaceLast("+", "_")) + let (majorJdkVersion = jdkVersion.split(".").first) + new { + name = "Set up environment" + shell = "#!/bin/bash -exo pipefail" + command = new Listing { + #""" + sed -ie '/\[ol8_codeready_builder\]/,/^$/s/enabled=0/enabled=1/g' /etc/yum.repos.d/oracle-linux-ol8.repo \ + && microdnf -y install util-linux tree coreutils-single findutils curl tar gzip git zlib-devel gcc-c++ make openssl glibc-langpack-en libstdc++-static \ + && microdnf clean all \ + && rm -rf /var/cache/dnf + + # install jdk + curl -L \ + https://github.com/adoptium/temurin\#(majorJdkVersion)-binaries/releases/download/jdk-\#(jdkVersionEncoded)/OpenJDK\#(majorJdkVersion)U-jdk_\#(if (arch == "amd64") "x64" else "aarch64")_linux_hotspot_\#(jdkVersionAlt).tar.gz -o /tmp/jdk.tar.gz + + mkdir /jdk \ + && cd /jdk \ + && cat /tmp/jdk.tar.gz | tar --strip-components=1 -xzC . + + mkdir -p ~/staticdeps/bin + + cp /usr/lib/gcc/\#(if (arch == "amd64") "x86_64" else "aarch64")-redhat-linux/8/libstdc++.a ~/staticdeps + + # install zlib + if [[ ! -f ~/staticdeps/include/zlib.h ]]; then + curl -L https://github.com/madler/zlib/releases/download/v\#(zlibVersion)/zlib-\#(zlibVersion).tar.gz -o /tmp/zlib.tar.gz + + mkdir -p /tmp/dep_zlib-\#(zlibVersion) \ + && cd /tmp/dep_zlib-\#(zlibVersion) \ + && cat /tmp/zlib.tar.gz | tar --strip-components=1 -xzC . \ + && echo "zlib-\#(zlibVersion): configure..." && ./configure --static --prefix="$HOME"/staticdeps > /dev/null \ + && echo "zlib-\#(zlibVersion): make..." && make -s -j4 \ + && echo "zlib-\#(zlibVersion): make install..." && make -s install \ + && rm -rf /tmp/dep_zlib-\#(zlibVersion) + fi + """# + // don't need musl on aarch because GraalVM only supports musl builds on x86 + when (arch == "amd64") { + #""" + # install musl + if [[ ! -f ~/staticdeps/bin/x86_64-linux-musl-gcc ]]; then + curl -L https://musl.libc.org/releases/musl-\#(muslVersion).tar.gz -o /tmp/musl.tar.gz + + mkdir -p /tmp/dep_musl-\#(muslVersion) \ + && cd /tmp/dep_musl-\#(muslVersion) \ + && cat /tmp/musl.tar.gz | tar --strip-components=1 -xzC . \ + && echo "musl-\#(muslVersion): configure..." && ./configure --disable-shared --prefix="$HOME"/staticdeps > /dev/null \ + && echo "musl-\#(muslVersion): make..." && make -s -j4 \ + && echo "musl-\#(muslVersion): make install..." && make -s install \ + && rm -rf /tmp/dep_musl-\#(muslVersion) + + # native-image expects to find an executable at this path. + ln -s ~/staticdeps/bin/musl-gcc ~/staticdeps/bin/x86_64-linux-musl-gcc + fi + """# + } + }.join("\n\n") + } + +steps { + when (os == "linux") { + new CircleCI.RestoreCacheStep { + name = "Restore static deps from cache" + key = "staticdeps-\(arch)" + } + setupLinuxEnvironment + new CircleCI.SaveCacheStep { + name = "Save statics deps to cache" + key = "staticdeps-\(arch)" + paths { + "~/staticdeps" + } + } + } + when (os == "macOS" && arch == "amd64") { + new CircleCI.RunStep { + name = "Installing Rosetta 2" + command = """ + /usr/sbin/softwareupdate --install-rosetta --agree-to-license + """ + } + } + // If building macOS/aarch64, we need to use GraalVM 23. + // We can't use GraalVM 23 for any other build because we need to support Java 11, which was + // dropped in GraalVM 23. + when (os == "macOS" && arch == "aarch64") { + new CircleCI.RunStep { + command = "git apply patches/graalVm23.patch" + } + } + new CircleCI.RunStep { + name = "gradle buildNative" + local _os = + if (os == "macOS") "mac" + else if (musl) "alpine" + else "linux" + local jobName = "\(_os)Executable\(arch.capitalize())" + command = #""" + export PATH=~/staticdeps/bin:$PATH + ./gradlew \#(module.gradleArgs) pkl-cli:\#(jobName) pkl-core:test\#(jobName.capitalize()) + """# + } + new CircleCI.PersistToWorkspaceStep { + root = "." + paths { + "pkl-cli/build/executable/" + } + } +} + +job { + when (os == "macOS") { + macos { + xcode = "15.2.0" + } + // Use M1 for all architectures. We build amd64/aarch64 based on the GraalVM version, + // which gets patched in via `git apply patches/graalVm23.patch`. + resource_class = "macos.m1.large.gen1" + } else { + docker { + new { + image = if (arch == "aarch64") "arm64v8/oraclelinux:8-slim" else "oraclelinux:8-slim" + } + } + environment { + ["JAVA_HOME"] = "/jdk" + } + resource_class = if (arch == "aarch64") "arm.xlarge" else "xlarge" + } +} diff --git a/.circleci/jobs/DeployJob.pkl b/.circleci/jobs/DeployJob.pkl new file mode 100644 index 00000000..292ce49c --- /dev/null +++ b/.circleci/jobs/DeployJob.pkl @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// +extends "GradleJob.pkl" + +import ".../pkl-pantry/packages/com.circleci.v2/CircleCI.pkl" + +local self = this + +command: String + +job { + docker { + new { image = "cimg/openjdk:11.0" } + } +} + +steps { + new CircleCI.AttachWorkspaceStep { at = "." } + new CircleCI.RunStep { + command = "./gradlew \(self.gradleArgs) \(module.command)" + } +} diff --git a/.circleci/jobs/GradleCheckJob.pkl b/.circleci/jobs/GradleCheckJob.pkl new file mode 100644 index 00000000..bf45c1b0 --- /dev/null +++ b/.circleci/jobs/GradleCheckJob.pkl @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// +extends "GradleJob.pkl" + +import ".../pkl-pantry/packages/com.circleci.v2/CircleCI.pkl" + +javaVersion: "11.0"|"17.0" + +steps { + new CircleCI.RunStep { + name = "gradle check" + command = "./gradlew \(module.gradleArgs) check" + } +} + +job { + docker { + new { + image = "cimg/openjdk:\(javaVersion)" + } + } +} diff --git a/.circleci/jobs/GradleJob.pkl b/.circleci/jobs/GradleJob.pkl new file mode 100644 index 00000000..8c5f9e68 --- /dev/null +++ b/.circleci/jobs/GradleJob.pkl @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// +abstract module GradleJob + +import ".../pkl-pantry/packages/com.circleci.v2/CircleCI.pkl" + +/// Whether this is a release build or not. +isRelease: Boolean = false + +fixed gradleArgs = new Listing { + "--info" + "--stacktrace" + when (isRelease) { + "-DreleaseBuild=true" + } +}.join(" ") + +steps: Listing + +job: CircleCI.Job = new { + environment { + ["LANG"] = "en_US.UTF-8" + } + steps { + "checkout" + ...module.steps + new CircleCI.RunStep { + // find all test results and write them to the home dir + name = "Gather test results" + command = """ + mkdir ~/test-results/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \\; + """ + `when` = "always" + } + new CircleCI.StoreTestResults { + path = "~/test-results" + } + } +} diff --git a/.circleci/jobs/SimpleGradleJob.pkl b/.circleci/jobs/SimpleGradleJob.pkl new file mode 100644 index 00000000..0c95ed38 --- /dev/null +++ b/.circleci/jobs/SimpleGradleJob.pkl @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// +extends "GradleJob.pkl" + +import ".../pkl-pantry/packages/com.circleci.v2/CircleCI.pkl" + +name: String = command + +command: String + +steps { + new CircleCI.RunStep { + name = module.name + command = """ + ./gradlew \(module.gradleArgs) \(module.command) + """ + } +} + +job { + docker { + new { image = "cimg/openjdk:11.0" } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cec17f8b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +# don't trim whitespace within (say) a Pkl multiline string +trim_trailing_whitespace = false +insert_final_newline = true +max_line_length = 100 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..896495e3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# linguist-generated would suppress files in diffs +**/src/test/files/** linguist-vendored + +/docs/** linguist-documentation + +*.pkl linguist-language=Groovy diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..82ce892e --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,8 @@ +#!/bin/sh + +files=`git diff --cached --name-status` + +if [[ $files =~ .circleci/config.pkl ]]; then + pkl eval .circleci/config.pkl -o .circleci/config.yml + git add .circleci/config.yml +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ae9f1156 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# macOS +.DS_STORE + +# Gradle +.gradle/ +build/ +generated/ + +# IntelliJ +.idea/ +!.idea/codestyles/ +!.idea/inspectionProfiles/ +!.idea/runConfigurations/ +!.idea/scopes/ +!.idea/vcs.xml + +# :pkl-core:makeIntelliJAntlrPluginHappy +gen/ +PklLexer.tokens diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..afb7ce46 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,600 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..effb888a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,46 @@ + + + + \ No newline at end of file diff --git a/.idea/scopes/AllExceptTruffleAst.xml b/.idea/scopes/AllExceptTruffleAst.xml new file mode 100644 index 00000000..1ace08fb --- /dev/null +++ b/.idea/scopes/AllExceptTruffleAst.xml @@ -0,0 +1,3 @@ + + + diff --git a/.idea/scopes/AllProjects.xml b/.idea/scopes/AllProjects.xml new file mode 100644 index 00000000..c30eebc8 --- /dev/null +++ b/.idea/scopes/AllProjects.xml @@ -0,0 +1,3 @@ + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.java-version b/.java-version new file mode 100644 index 00000000..03b6389f --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17.0 diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc new file mode 100644 index 00000000..3dee37a7 --- /dev/null +++ b/CODE_OF_CONDUCT.adoc @@ -0,0 +1,78 @@ +== Code of Conduct + +=== Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our +project and our community a harassment-free experience for everyone, +regardless of age, body size, disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +=== Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual +attention or advances +* Trolling, insulting/derogatory comments, and personal or political +attacks +* Public or private harassment +* Publishing others’ private information, such as a physical or +electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a +professional setting + +=== Our Responsibilities + +Project maintainers are responsible for clarifying the standards of +acceptable behavior and are expected to take appropriate and fair +corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, +or reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, or to ban +temporarily or permanently any contributor for other behaviors that they +deem inappropriate, threatening, offensive, or harmful. + +=== Scope + +This Code of Conduct applies within all project spaces, and it also +applies when an individual is representing the project or its community +in public spaces. Examples of representing a project or community +include using an official project e-mail address, posting via an +official social media account, or acting as an appointed representative +at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +=== Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may +be reported by contacting the open source team at +opensource-conduct@group.apple.com. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. The project team is obligated to +maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted +separately. + +Project maintainers who do not follow or enforce the Code of Conduct in +good faith may face temporary or permanent repercussions as determined +by other members of the project’s leadership. + +=== Attribution + +This Code of Conduct is adapted from the +https://www.contributor-covenant.org[Contributor Covenant], version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 00000000..a7d655bc --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,58 @@ +:uri-github-discussion: https://github.com/apple/pkl/discussions +:uri-github-issue-pkl: https://github.com/apple/pkl/issues/new +:uri-seven-rules: https://cbea.ms/git-commit/#seven-rules + += Pkl Contributor's Guide + +Welcome to the Pkl community, and thank you for contributing! +This guide explains how to get involved. + +* <> +* <> +* <> + +== Licensing + +Pkl is released under the Apache 2.0 license. +This is why we require that, by submitting a pull request, you acknowledge that you have the right to license your contribution to Apple and the community, and agree that your contribution is licensed under the Apache 2.0 license. + +== Issue Tracking + +To file a bug or feature request, use {uri-github-issue-pkl}[GitHub]. +Be sure to include the following information: + +* Context +** What are/were you trying to achieve? +** What's the impact of this bug/feature? + +For bug reports, additionally include the following information: + +* The output of `pkl --version`. +* The complete error message. +* The simplest possible steps to reproduce. + +== Pull Requests + +When preparing a pull request, follow this checklist: + +* Imitate the conventions of surrounding code. +* Format code with `./gradlew spotlessApply` (otherwise the build will fail). +* Verify that both the JVM build (`./gradlew build`) and native build (`./gradlew buildNative`) succeed. +* Follow the {uri-seven-rules}[seven rules] of great Git commit messages: +** Separate subject from body with a blank line. +** Limit the subject line to 50 characters.footnote:not-enforced[This rule is not enforced in the Pkl project.] +** Capitalize the subject line. +** Do not end the subject line with a period. +** Use the imperative mood in the subject line. +** Wrap the body at 72 characters.footnote:not-enforced[] +** Use the body to explain what and why vs. how. + +IMPORTANT: If you plan to make substantial changes or add new features, +we encourage you to first discuss them with the wider Pkl developer community. +You can do this by filing a {uri-github-issue-pkl}[GitHub Issue] or by starting +{uri-github-discussion}[GitHub Discussion]. +This will save time and increases the chance of your pull request being accepted. + +== Maintainers + +The project’s maintainers (those with write access to the upstream repository) are listed in link:MAINTAINERS.adoc[]. diff --git a/DEVELOPMENT.adoc b/DEVELOPMENT.adoc new file mode 100644 index 00000000..c2dbb377 --- /dev/null +++ b/DEVELOPMENT.adoc @@ -0,0 +1,101 @@ += Development +:uri-gng: https://gng.dsun.org +:uri-jenv: https://www.jenv.be +:uri-intellij: https://www.jetbrains.com/idea/download/ +:uri-jdk: https://adoptopenjdk.net/releases.html + +== Setup + +. (mandatory) Install {uri-jdk}[OpenJDK 11 HotSpot] (as long as we support JDK11) +. (mandatory) Setup Gradle on your system +. (recommended) Install {uri-intellij}[IntelliJ IDEA 2023.x] + +To import the project into IntelliJ, go to File->Open and select the project's root directory. +If the project is opened but not imported, look for a popup in the lower right corner +and click its "Import Gradle Project" link. +. (recommended) Install {uri-gng}[gng] + +_gng_ enables to run Gradle commands with `gw` (instead of `./gradlew`) from any subdirectory. +. (recommended) Install {uri-jenv}[jenv] and plugins + +_jenv_ use specific JDK versions in certain subdirectories. _Pkl_ comes with a `.java-version` file specifying JDK 17. + +Enable _jenv_ plugins for better handling by `gradle`: ++ +[source,shell] +---- +jenv enable-plugin gradle +jenv enable-plugin export +---- + +== Common Build Commands + +[source,shell] +---- +gw clean +gw test +gw build # build everything except native executables +gw buildNative # build macOS executable on macOS, + # Linux and Alpine executables on Linux +gw pkldoc # generate standard library docs + +pkl-cli/build/executable/jpkl # run Java executable +pkl-cli/build/executable/pkl-macos-amd64 # run Mac executable +---- + +== Update Gradle + +. Go to https://gradle.org/release-checksums/ and copy the checksum for the new Gradle version +. Run the following command *twice* (until it prints UP-TO-DATE): ++ +[source,shell] +---- +gw wrapper --gradle-version [version] --gradle-distribution-sha256-sum [sha] +---- +. Commit the updated wrapper files + +== Update Dependencies + +. (optional) Update _gradle/libs.version.toml_ +based on version information from https://search.maven.org, https://plugins.gradle.org, and GitHub repos +. Run `gw updateDependencyLocks` +. Validate changes with `gw build buildNative` +. Review and commit the updated dependency lock files + +== Code Generation + +* Truffle code generation is performed by Truffle's annotation processor, which runs as part of task `:pkl-core:compileJava` +** Output dir is `generated/truffle/` +* ANTLR code generation is performed by task `:pkl-core:generateGrammarSource` +** Output dir is `generated/antlr/` + +== Resources + +=== ANTLR + +* https://github.com/antlr/antlr4/blob/main/doc/index.md[Documentation] +* https://groups.google.com/forum/#!forum/antlr-discussion[Forums] +* https://github.com/mobileink/lab.clj.antlr/tree/main/doc[Some third-party docs] + +=== Truffle + +* http://ssw.jku.at/Research/Projects/JVM/Truffle.html[Homepage] +* https://github.com/graalvm/truffle[GitHub] +* http://lafo.ssw.uni-linz.ac.at/javadoc/truffle/latest/[Javadoc] +* http://mail.openjdk.java.net/pipermail/graal-dev/[Mailing List] +* https://medium.com/@octskyward/graal-truffle-134d8f28fb69#.2db370y2g[Graal & Truffle (Article)] +* https://comserv.cs.ut.ee/home/files/Pool_ComputerScience_2016.pdf?study=ATILoputoo&reference=6319668E7151D556131810BC3F4A627D7FEF5F3B[Truffle Overview (see chapter 1)] +* https://gist.github.com/smarr/d1f8f2101b5cc8e14e12[Truffle: Languages and Material] +* https://github.com/smarr/truffle-notes[Truffle Notes] +* https://wiki.openjdk.java.net/display/Graal/Truffle+FAQ+and+Guidelines[Truffle FAQ] + +=== Other Config Languages + +* https://github.com/google/jsonnet[Jsonnet] +* https://github.com/dhall-lang/dhall-lang[Dhall] +* https://cuelang.org[CUE] +* https://nickel-lang.org[Nickel] +* https://kcl-lang.io[KCL] +* https://github.com/google/skylark[Skylark] +* https://github.com/typesafehub/config[Typesafe Config] +* https://www.flabbergast.org[Flabbergast] +(defunct, http://artefacts.masella.name/2015-srecon-andre_masella.pdf[paper]) +* https://medium.com/@MrJamesFisher/nix-by-example-a0063a1a4c55[Nix by example: The Nix expression language] +* http://lethalman.blogspot.co.at/2014/07/nix-pill-4-basics-of-language.html[Nix pill 4: the basics of the language] +* https://docs.puppetlabs.com/puppet/latest/reference/lang_summary.html[Puppet Configuration Language] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/MAINTAINERS.adoc b/MAINTAINERS.adoc new file mode 100644 index 00000000..b79b8d2f --- /dev/null +++ b/MAINTAINERS.adoc @@ -0,0 +1,11 @@ += MAINTAINERS + +This page lists all active Maintainers of this repository. + +See link:CONTRIBUTING.adoc[] for general contribution guidelines. + +== Maintainers (in alphabetical order) + +* https://github.com/bioball[Daniel Chao] +* https://github.com/stackoverflow[Islon Scherer] +* https://github.com/holzensp[Philip Hölzenspies] diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..1389b135 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,106 @@ +Copyright © 2024 Apple Inc. and the Pkl project authors + + +Portions of this software were originally based on 'SnakeYAML' developed by Andrey Somov. +(https://bitbucket.org/asomov/snakeyaml-engine/) + +The Apache License +Copyright © 2008-2010 Andrey Somov +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 http://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. + +Portions of this software were originally based on 'jline3' developed by the JLine authors. +(https://github.com/jline/jline3) + +Copyright (c) 2002-2023, the original author(s) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following +conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of JLine nor the names of its contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. + +Portions of this software were originally based on 'minimal-json' developed by EclipseSource. +(https://github.com/ralfstx/minimal-json) + +Copyright (c) 2015, 2016 EclipseSource. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Portions of this software were originally based on 'gson' developed by the Google Inc. +(https://github.com/google/gson) + +The Apache License +Copyright © 2008 Google Inc +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 http://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. + +Portions of this software were originally based on 'guava' developed by Google Inc. +(https://github.com/google/guava) + +The Apache License +Copyright © 2009 The Guava Authors +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 http://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. + +Portions of this software were originally based on 'java-string-similarity' developed by Thibault Debatty. +(https://github.com/tdebatty/java-string-similarity) + +Copyright 2015 Thibault Debatty + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Portions of this software were originally based on 'xerxes2-j' developed by the Apache Software Foundation. +(https://github.com/apache/xerces2-j) + +The Apache License +Copyright © 1999-2018 The Apache Software Foundation +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 http://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. + +Portions of this software were originally based on 'scroll-into-view-if-needed' developed by Cody Olsen. +(https://github.com/scroll-into-view/scroll-into-view-if-needed) + + +Copyright (c) 2023 Cody Olsen + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This product ships with third party licenses that may be distributed under a different license. +This information is detailed in THIRD-PARTY-NOTICES.txt. + +Portions of this software includes code from "Gradle" by Gradle, Inc. + +Copyright 2015 the original author or authors. + +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. diff --git a/README.adoc b/README.adoc new file mode 100644 index 00000000..adfd46c4 --- /dev/null +++ b/README.adoc @@ -0,0 +1,36 @@ += Pkl + +:uri-homepage: https://pkl-lang.org +:uri-docs: {uri-homepage}/main/current +:uri-docs-introduction: {uri-docs}/introduction +:uri-docs-release-notes: {uri-docs}/release-notes +:uri-docs-language: {uri-docs}/language +:uri-docs-tools: {uri-docs}/tools +:uri-github-issue: https://github.com/apple/pkl/issues +:uri-github-discussions: https://github.com/apple/pkl/discussions +:uri-pkl-examples: https://pkl-lang.org/main/current/examples.html +:uri-installation: https://pkl-lang.org/main/current/pkl-cli/index.html#installation +:uri-lang-reference: https://pkl-lang.org/main/current/language-reference/index.html + +A configuration as code language with rich validation and tooling. + +== Quick Links + +* {uri-installation}[Installation] +* {uri-lang-reference}[Language Reference] + +== Documentation + +* {uri-homepage}[Home Page] +** {uri-docs-introduction}[Introduction] +** {uri-docs-language}[Language] +** {uri-docs-tools}[Tools] +** {uri-pkl-examples}[Examples] +** {uri-docs-release-notes}[Release Notes] + +== Community + +We'd love to hear from you! + +* Create an {uri-github-issue}[issue] +* Ask a question on {uri-github-discussions}[GitHub Discussions] diff --git a/SECURITY.adoc b/SECURITY.adoc new file mode 100644 index 00000000..8309a366 --- /dev/null +++ b/SECURITY.adoc @@ -0,0 +1,13 @@ += Security + +For the protection of our community, the Pkl team does not disclose, discuss, or confirm security issues until our investigation is complete and any necessary updates are generally available. + +== Reporting a security vulnerability + +If you have discovered a security vulnerability within the Pkl project, please report it to us. +We welcome reports from everyone, including security researchers, developers, and users. + +Security vulnerabilities may be reported on the link:https://security.apple.com/submit[Report a vulnerability] form. +When submitting a vulnerability, select "Apple Devices and Software" as the affected platform, and "Open Source" as the affected area. + +For more information, see https://pkl-lang.org/security.html. diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt new file mode 100644 index 00000000..c79c5a8f --- /dev/null +++ b/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,376 @@ +Pkl ships with third-party libraries that may be distributed under a different license than +Pkl's own license. +These libraries and their licenses are listed below: + +1) Clikt (https://github.com/ajalt/clikt) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2018 AJ Alt +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 http://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. + +2) JavaPoet (http://github.com/square/javapoet/) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2015 Square, Inc. +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 http://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. + +3) KotlinPoet (https://github.com/square/kotlinpoet) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2017 Square, Inc. +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 http://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. + +4) ANTLR 4 Runtime (Optimized) (http://tunnelvisionlabs.com) +POM License: The BSD License - http://www.antlr.org/license.html + +Copyright (c) 2012 Terence Parr and Sam Harwell +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +5) GeantyRef (https://github.com/leangen/geantyref) +Manifest license URL: https://www.apache.org/licenses/LICENSE-2.0 + +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2017 Kaqqao +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 http://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. + +6) commonmark-java core +POM License: The 2-Clause BSD License - https://opensource.org/licenses/BSD-2-Clause + +Embedded license: + + **************************************** + +Copyright (c) 2015, Atlassian Pty Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +7) commonmark-java extension for tables +POM License: The 2-Clause BSD License - https://opensource.org/licenses/BSD-2-Clause + +Embedded license: + + **************************************** + +Copyright (c) 2015, Atlassian Pty Ltd +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +8) jansi (http://fusesource.com/) +Manifest license URL: https://www.apache.org/licenses/LICENSE-2.0 + +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © Fusesource 2023 +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 http://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. + +9) Graal Sdk (https://github.com/oracle/graal) +POM License: Universal Permissive License, Version 1.0 - http://opensource.org/licenses/UPL + +10) Truffle API (http://openjdk.java.net/projects/graal) +POM License: Universal Permissive License, Version 1.0 - http://opensource.org/licenses/UPL + +11) IntelliJ IDEA Annotations (http://www.jetbrains.org) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © Jetbrains 2023 +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 http://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. + +12) kotlin-reflect (https://kotlinlang.org/) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © Wuseal 2018 +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 http://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. + +13) kotlin-stdlib (https://kotlinlang.org/) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributor +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 http://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. + +14) kotlin-stdlib-common (https://kotlinlang.org/) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2023 Kotlin Team +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 http://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. + +15) kotlin-stdlib-jdk7 (https://kotlinlang.org/) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2023 Kotlin Team +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 http://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. + +16) kotlin-stdlib-jdk8 (https://kotlinlang.org/) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2023 Kotlin Team +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 http://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. + +17) kotlinx.html (https://github.com/Kotlin/kotlinx.html) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2017 Yole +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 http://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. + +18) kotlinx-serialization-core (https://github.com/Kotlin/kotlinx.serialization) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +2017-2019 JetBrains s.r.o and respective authors and developers +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 http://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. + +19) kotlinx-serialization-json (https://github.com/Kotlin/kotlinx.serialization) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +2017-2019 JetBrains s.r.o and respective authors and developers +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 http://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. + +20) JLine Reader +Manifest license URL: https://opensource.org/licenses/BSD-3-Clause + +POM License: The 3-Clause BSD License - https://opensource.org/licenses/BSD-3-Clause + +Copyright (c) 2002-2023, the original author(s) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following +conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of JLine nor the names of its contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +21) JLine Terminal +Manifest license URL: https://opensource.org/licenses/BSD-3-Clause + +POM License: The 3-Clause BSD License - https://opensource.org/licenses/BSD-3-Clause + +Copyright (c) 2002-2023, the original author(s) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following +conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of JLine nor the names of its contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +22) JLine JANSI Terminal +Manifest license URL: https://opensource.org/licenses/BSD-3-Clause + +POM License: The 3-Clause BSD License - https://opensource.org/licenses/BSD-3-Clause + +Copyright (c) 2002-2023, the original author(s) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following +conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of JLine nor the names of its contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +23) msgpack-core (https://msgpack.org/) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2016 Sadayuki Furuhashi, Muga Nishizawa, Taro L. Saito, Mitsunori Komatsu, Ozawa Tsuyoshi +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 http://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. + +24) Paguro (https://github.com/GlenKPeterson/Paguro) +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2021 Glen K. Peterson +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 http://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. + +POM License: Eclipse Public License - v 1.0 - http://www.eclipse.org/legal/epl-v10.html + +NOTE: The files in this project that came from Clojure (Persistent...) MUST only be used under the Eclipse 1.0 license. +At the user's choice, any other files in this project can be used under Eclipse 1.0 or the Apache 2.0 license. + +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). +ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and + +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the Program. + +Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. + +25) SnakeYAML Engine (http://www.snakeyaml.org) +Manifest license URL: https://www.apache.org/licenses/LICENSE-2.0 + +POM License: Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + +The Apache License +Copyright © 2008-2010 Andrey Somov +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 http://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. + diff --git a/bench/bench.gradle.kts b/bench/bench.gradle.kts new file mode 100644 index 00000000..c2667480 --- /dev/null +++ b/bench/bench.gradle.kts @@ -0,0 +1,48 @@ +plugins { + pklAllProjects + pklJavaLibrary + pklGraalVm + id("me.champeau.jmh") +} + +val truffle: Configuration by configurations.creating +val graal: Configuration by configurations.creating + +@Suppress("UnstableApiUsage") +dependencies { + jmh(project(":pkl-core")) + // necessary because antlr4-runtime is declared as implementation dependency in pkl-core.gradle + jmh(libs.antlrRuntime) + truffle(libs.truffleApi) + graal(libs.graalCompiler) +} + +jmh { + //include = ["fib_class_java"] + //include = ["fib_class_constrained1", "fib_class_constrained2"] + jmhVersion.set(libs.versions.jmh) + // jvmArgsAppend = "-Dgraal.TruffleCompilationExceptionsAreFatal=true " + + // "-Dgraal.Dump=Truffle,TruffleTree -Dgraal.TraceTruffleCompilation=true " + + // "-Dgraal.TruffleFunctionInlining=false" + jvm.set("${buildInfo.graalVm.baseDir}/bin/java") + // see: https://docs.oracle.com/en/graalvm/enterprise/20/docs/graalvm-as-a-platform/implement-language/#disable-class-path-separation + jvmArgs.set( + listOf( + // one JVM arg per list element doesn't work, but the following does + "-Dgraalvm.locatorDisabled=true --module-path=${truffle.asPath} --upgrade-module-path=${graal.asPath}" + ) + ) + includeTests.set(false) + //threads = Runtime.runtime.availableProcessors() / 2 + 1 + //synchronizeIterations = false +} + +tasks.named("jmh") { + dependsOn(":installGraalVm") +} + +// Prevent this error which occurs when building in IntelliJ: +// "Entry org/pkl/core/fib_class_typed.pkl is a duplicate but no duplicate handling strategy has been set." +tasks.processJmhResources { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} diff --git a/bench/gradle.lockfile b/bench/gradle.lockfile new file mode 100644 index 00000000..0673df5a --- /dev/null +++ b/bench/gradle.lockfile @@ -0,0 +1,43 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.tunnelvisionlabs:antlr4-runtime:4.9.0=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +net.sf.jopt-simple:jopt-simple:5.0.4=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +org.apache.commons:commons-math3:3.2=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.compiler:compiler:22.3.1=graal +org.graalvm.sdk:graal-sdk:22.3.1=graal,jmh,jmhRuntimeClasspath,truffle +org.graalvm.truffle:truffle-api:22.3.1=graal,jmh,jmhRuntimeClasspath,truffle +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=kotlinCompilerClasspath,kotlinCompilerPluginClasspathJmh,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.openjdk.jmh:jmh-core:1.36=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +org.openjdk.jmh:jmh-generator-asm:1.36=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +org.openjdk.jmh:jmh-generator-bytecode:1.36=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +org.openjdk.jmh:jmh-generator-reflection:1.36=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=jmh,jmhRuntimeClasspath +org.ow2.asm:asm:9.0=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=jmh,jmhRuntimeClasspath +empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileClasspath,compileOnly,compileOnlyDependenciesMetadata,default,implementationDependenciesMetadata,intransitiveDependenciesMetadata,jmhAnnotationProcessor,jmhApiDependenciesMetadata,jmhCompile,jmhCompileOnly,jmhCompileOnlyDependenciesMetadata,jmhIntransitiveDependenciesMetadata,jmhKotlinScriptDef,jmhKotlinScriptDefExtensions,jmhRuntime,jmhRuntimeOnlyDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeClasspath,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/bench/src/jmh/java/org/pkl/core/Fibonacci.java b/bench/src/jmh/java/org/pkl/core/Fibonacci.java new file mode 100644 index 00000000..1e8d25e5 --- /dev/null +++ b/bench/src/jmh/java/org/pkl/core/Fibonacci.java @@ -0,0 +1,104 @@ +/** + * 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; + +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; + +@SuppressWarnings("unused") +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 2) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(1) +public class Fibonacci { + @Benchmark + public long fib_class_java() { + return new FibJavaImpl().fib(35); + } + + @Benchmark + public long fib_class() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_class.pkl")); + return (long) module.getProperties().get("result"); + } + } + + @Benchmark + public long fib_class_explicitThis() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_class_explicitThis.pkl")); + return (long) module.getProperties().get("result"); + } + } + + @Benchmark + public long fib_class_typed() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_class_typed.pkl")); + return (long) module.getProperties().get("result"); + } + } + + @Benchmark + public long fib_class_constrained1() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_class_constrained1.pkl")); + return (long) module.getProperties().get("result"); + } + } + + @Benchmark + public long fib_class_constrained2() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_class_constrained2.pkl")); + return (long) module.getProperties().get("result"); + } + } + + @Benchmark + public long fib_module() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_module.pkl")); + return (long) module.getProperties().get("result"); + } + } + + @Benchmark + public long fib_module_explicitThis() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_module_explicitThis.pkl")); + return (long) module.getProperties().get("result"); + } + } + + @Benchmark + public long fib_lambda() { + try (var evaluator = Evaluator.preconfigured()) { + var module = evaluator.evaluate(modulePath("org/pkl/core/fib_lambda.pkl")); + return (long) module.getProperties().get("result"); + } + } +} + +// kept similar to pkl code (class, instance method, long argument) +class FibJavaImpl { + long fib(long n) { + return n < 2 ? n : fib(n - 1) + fib(n - 2); + } +} diff --git a/bench/src/jmh/java/org/pkl/core/ListSort.java b/bench/src/jmh/java/org/pkl/core/ListSort.java new file mode 100644 index 00000000..9ba208f3 --- /dev/null +++ b/bench/src/jmh/java/org/pkl/core/ListSort.java @@ -0,0 +1,143 @@ +/** + * 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; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.util.TempFile; +import org.openjdk.jmh.util.TempFileManager; +import org.pkl.core.module.ModuleKeyFactories; +import org.pkl.core.repl.ReplRequest; +import org.pkl.core.repl.ReplResponse; +import org.pkl.core.repl.ReplServer; +import org.pkl.core.resource.ResourceReaders; +import org.pkl.core.util.IoUtils; + +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 2) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(1) +@SuppressWarnings("unused") +public class ListSort { + private static final ReplServer repl = + new ReplServer( + SecurityManagers.defaultManager, + Loggers.stdErr(), + List.of(ModuleKeyFactories.standardLibrary), + List.of(ResourceReaders.file()), + Map.of(), + Map.of(), + null, + null, + null, + IoUtils.getCurrentWorkingDir(), + StackFrameTransformers.defaultTransformer); + private static final List list = new ArrayList<>(100000); + + static { + var random = new Random(2786433088656064171L); + for (var i = 0; i < 100000; i++) { + list.add(random.nextLong()); + } + + TempFile tempFile; + try { + tempFile = new TempFileManager().create("bench-nums.txt"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + try (var fw = new FileWriter(tempFile.getAbsolutePath())) { + for (var elem : list) { + fw.append(elem.toString()).append('\n'); + } + fw.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + var responses = + repl.handleRequest( + new ReplRequest.Eval( + "setup", + "import \"pkl:test\"\n" + + "random = test.random\n" + + "nums = read(\"file://" + + tempFile.getAbsolutePath() + + "\").text.split(\"\\n\").dropLast(1).map((it) -> it.toInt())\n" + + "cmp = (x, y) -> if (x < y) -1 else if (x == y) 0 else 1", + false, + false)); + if (!responses.isEmpty()) { + throw new AssertionError(responses.get(0)); + } + } + + @Benchmark + public String sortPkl() { + var response = + repl.handleRequest( + // append `.length` to avoid rendering the list + new ReplRequest.Eval("sort", "nums.sort().length", false, false)) + .get(0); + if (!(response instanceof ReplResponse.EvalSuccess)) { + throw new AssertionError(response); + } + return ((ReplResponse.EvalSuccess) response).getResult(); + } + + @Benchmark + public String sortWithPkl() { + var response = + repl.handleRequest( + // append `.length` to avoid rendering the list + new ReplRequest.Eval("sort", "nums.sortWith(cmp).length", false, false)) + .get(0); + if (!(response instanceof ReplResponse.EvalSuccess)) { + throw new AssertionError(response); + } + return ((ReplResponse.EvalSuccess) response).getResult(); + } + + // note that this is an uneven comparison + // (timsort vs. merge sort, java.util.ArrayList vs. persistent vector + @Benchmark + public List sortJava() { + return sort(list); + } + + private List sort(List self) { + var array = self.toArray(); + Arrays.sort(array); + return Arrays.asList(array); + } + + // note that this is an uneven comparison + // (timsort vs. merge sort, java.util.ArrayList vs. persistent vector + @Benchmark + public List sortWithJava() { + return sortWith(list, Comparator.comparingLong(x -> (long) x)); + } + + private List sortWith(List self, Comparator comparator) { + var array = self.toArray(); + Arrays.sort(array, comparator); + return Arrays.asList(array); + } +} diff --git a/bench/src/jmh/java/org/pkl/core/parser/ParserBenchmark.java b/bench/src/jmh/java/org/pkl/core/parser/ParserBenchmark.java new file mode 100644 index 00000000..9442786e --- /dev/null +++ b/bench/src/jmh/java/org/pkl/core/parser/ParserBenchmark.java @@ -0,0 +1,59 @@ +/** + * 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.parser; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; + +@SuppressWarnings("unused") +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 2) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(1) +public class ParserBenchmark { + // One-time execution of this code took ~10s until moving rule alternative + // for parenthesized expression after alternative for anonymous function. + @Benchmark + public void run() { + new Parser() + .parseModule( + "a1 {\n" + + " a2 {\n" + + " a3 {\n" + + " a4 {\n" + + " a5 {\n" + + " a6 {\n" + + " a7 {\n" + + " a8 {\n" + + " a9 {\n" + + " a10 {\n" + + " a11 {\n" + + " a12 {\n" + + " a13 = map(map(map((x) -> 1)))\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + } +} diff --git a/bench/src/jmh/resources/org/pkl/core/fib_class.pkl b/bench/src/jmh/resources/org/pkl/core/fib_class.pkl new file mode 100644 index 00000000..e20327a4 --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_class.pkl @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +class Fibonacci { + function fib(n) = if (n < 2) n else fib(n - 1) + fib(n - 2) +} +result = new Fibonacci {}.fib(35) diff --git a/bench/src/jmh/resources/org/pkl/core/fib_class_constrained1.pkl b/bench/src/jmh/resources/org/pkl/core/fib_class_constrained1.pkl new file mode 100644 index 00000000..a660ad02 --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_class_constrained1.pkl @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +class Fibonacci { + function fib(n: Int(this >= 0)): Int(this >= 0) = if (n < 2) n else fib(n - 1) + fib(n - 2) +} +result = new Fibonacci {}.fib(35) diff --git a/bench/src/jmh/resources/org/pkl/core/fib_class_constrained2.pkl b/bench/src/jmh/resources/org/pkl/core/fib_class_constrained2.pkl new file mode 100644 index 00000000..1145b440 --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_class_constrained2.pkl @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +class Fibonacci { + function fib(n: Int(isPositive)): Int(isPositive) = if (n < 2) n else fib(n - 1) + fib(n - 2) +} +result = new Fibonacci {}.fib(35) diff --git a/bench/src/jmh/resources/org/pkl/core/fib_class_explicitThis.pkl b/bench/src/jmh/resources/org/pkl/core/fib_class_explicitThis.pkl new file mode 100644 index 00000000..db7e1a21 --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_class_explicitThis.pkl @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +class Fibonacci { + function fib(n) = if (n < 2) n else this.fib(n - 1) + this.fib(n - 2) +} +result = new Fibonacci {}.fib(35) diff --git a/bench/src/jmh/resources/org/pkl/core/fib_class_typed.pkl b/bench/src/jmh/resources/org/pkl/core/fib_class_typed.pkl new file mode 100644 index 00000000..7f797347 --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_class_typed.pkl @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +class Fibonacci { + function fib(n: Int): Int = if (n < 2) n else fib(n - 1) + fib(n - 2) +} +result = new Fibonacci {}.fib(35) diff --git a/bench/src/jmh/resources/org/pkl/core/fib_lambda.pkl b/bench/src/jmh/resources/org/pkl/core/fib_lambda.pkl new file mode 100644 index 00000000..7cdc930e --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_lambda.pkl @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +hidden fib = (n) -> if (n < 2) n else fib.apply(n - 1) + fib.apply(n - 2) + +result = fib.apply(35) \ No newline at end of file diff --git a/bench/src/jmh/resources/org/pkl/core/fib_module.pkl b/bench/src/jmh/resources/org/pkl/core/fib_module.pkl new file mode 100644 index 00000000..4ff96ed9 --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_module.pkl @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +function fib(n) = if (n < 2) n else fib(n - 1) + fib(n - 2) +result = fib(35) \ No newline at end of file diff --git a/bench/src/jmh/resources/org/pkl/core/fib_module_explicitThis.pkl b/bench/src/jmh/resources/org/pkl/core/fib_module_explicitThis.pkl new file mode 100644 index 00000000..3617d544 --- /dev/null +++ b/bench/src/jmh/resources/org/pkl/core/fib_module_explicitThis.pkl @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +function fib(n) = if (n < 2) n else this.fib(n - 1) + this.fib(n - 2) +result = fib(35) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..b14d5434 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,67 @@ +// https://youtrack.jetbrains.com/issue/KTIJ-19369 +@file:Suppress("DSL_SCOPE_VIOLATION") + +import org.jetbrains.gradle.ext.ActionDelegationConfig +import org.jetbrains.gradle.ext.ActionDelegationConfig.TestRunner.PLATFORM +import org.jetbrains.gradle.ext.ProjectSettings +import org.jetbrains.gradle.ext.TaskTriggersConfig + +plugins { + pklAllProjects + pklGraalVm + + alias(libs.plugins.ideaExt) + alias(libs.plugins.jmh) apply false + alias(libs.plugins.nexusPublish) +} + +nexusPublishing { + repositories { + sonatype { + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + } + } +} + +idea { + project { + this as ExtensionAware + configure { + this as ExtensionAware + configure { + delegateBuildRunToGradle = true + testRunner = PLATFORM + } + configure { + afterSync(provider { project(":pkl-core").tasks.named("makeIntelliJAntlrPluginHappy") }) + } + } + } +} + +val clean by tasks.registering(Delete::class) { + delete(buildDir) +} + +val printVersion by tasks.registering { + doFirst { println(buildInfo.pklVersion) } +} + +val message = """ +==== +Gradle version : ${gradle.gradleVersion} +Java version : ${System.getProperty("java.version")} +isParallel : ${gradle.startParameter.isParallelProjectExecutionEnabled} +maxWorkerCount : ${gradle.startParameter.maxWorkerCount} +Architecture : ${buildInfo.arch} + +Project Version : ${project.version} +Pkl Version : ${buildInfo.pklVersion} +Pkl Non-Unique Version : ${buildInfo.pklVersionNonUnique} +Git Commit ID : ${buildInfo.commitId} +==== +""" + +val formattedMessage = message.replace("\n====", "\n" + "=".repeat(message.lines().maxByOrNull { it.length }!!.length)) +logger.info(formattedMessage) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..73eb3672 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `kotlin-dsl` +} + +dependencies { + implementation(libs.downloadTaskPlugin) + implementation(libs.spotlessPlugin) + implementation(libs.kotlinPlugin) { + exclude(module = "kotlin-android-extensions") + } + implementation(libs.shadowPlugin) +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..3e4a13a6 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,24 @@ +@file:Suppress("UnstableApiUsage") + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +// makes ~/.gradle/init.gradle unnecessary and ~/.gradle/gradle.properties optional +dependencyResolutionManagement { + // use same version catalog as main build + versionCatalogs { + register("libs") { + from(files("../gradle/libs.versions.toml")) + } + } + + repositories { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + mavenCentral() + gradlePluginPortal() + } +} diff --git a/buildSrc/src/main/kotlin/BuildInfo.kt b/buildSrc/src/main/kotlin/BuildInfo.kt new file mode 100644 index 00000000..e8d85daa --- /dev/null +++ b/buildSrc/src/main/kotlin/BuildInfo.kt @@ -0,0 +1,144 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +import java.io.File +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.artifacts.VersionConstraint +import org.gradle.kotlin.dsl.getByType + +// `buildInfo` in main build scripts +// `project.extensions.getByType()` in precompiled script plugins +open class BuildInfo(project: Project) { + val self = this + + inner class GraalVm { + val homeDir: String by lazy { + System.getenv("GRAALVM_HOME") ?: "${System.getProperty("user.home")}/.graalvm" + } + + val version: String by lazy { + libs.findVersion("graalVm").get().toString() + } + + val isGraal22: Boolean by lazy { + version.startsWith("22") + } + + val arch by lazy { + if (os.isMacOsX && isGraal22) { + "amd64" + } else { + self.arch + } + } + + val osName: String by lazy { + when { + os.isMacOsX && isGraal22 -> "darwin" + os.isMacOsX -> "macos" + os.isLinux -> "linux" + else -> throw RuntimeException("${os.familyName} is not supported.") + } + } + + val baseName: String by lazy { + if (graalVm.isGraal22) { + "graalvm-ce-java11-${osName}-${arch}-${version}" + } else { + "graalvm-jdk-${graalVM23JdkVersion}_${osName}-${arch}_bin" + } + } + + val graalVM23JdkVersion: String by lazy { + libs.findVersion("graalVM23JdkVersion").get().requiredVersion + } + + val downloadUrl: String by lazy { + if (isGraal22) { + "https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-" + + "${version}/$baseName.tar.gz" + } else { + val jdkMajor = graalVM23JdkVersion.takeWhile { it != '.' } + "https://download.oracle.com/graalvm/$jdkMajor/archive/$baseName.tar.gz" + } + } + + val installDir: File by lazy { + File(homeDir, baseName) + } + + val baseDir: String by lazy { + if (os.isMacOsX) "$installDir/Contents/Home" else installDir.toString() + } + } + + /** + * Same logic as [org.gradle.internal.os.OperatingSystem#arch], which is protected. + */ + val arch: String by lazy { + when (val arch = System.getProperty("os.arch")) { + "x86" -> "i386" + "x86_64" -> "amd64" + "powerpc" -> "ppc" + else -> arch + } + } + + val graalVm: GraalVm = GraalVm() + + val isCiBuild: Boolean by lazy { + System.getenv("CI") != null + } + + val isReleaseBuild: Boolean by lazy { + java.lang.Boolean.getBoolean("releaseBuild") + } + + val os: org.gradle.internal.os.OperatingSystem by lazy { + org.gradle.internal.os.OperatingSystem.current() + } + + // could be `commitId: Provider = project.provider { ... }` + val commitId: String by lazy { + // only run command once per build invocation + if (project === project.rootProject) { + Runtime.getRuntime() + .exec("git rev-parse --short HEAD", arrayOf(), project.rootDir) + .inputStream.reader().readText().trim() + } else { + project.rootProject.extensions.getByType(BuildInfo::class.java).commitId + } + } + + val commitish: String by lazy { + if (isReleaseBuild) project.version.toString() else commitId + } + + val pklVersion: String by lazy { + if (isReleaseBuild) { + project.version.toString() + } else { + project.version.toString().replace("-SNAPSHOT", "-dev+$commitId") + } + } + + val pklVersionNonUnique: String by lazy { + if (isReleaseBuild) { + project.version.toString() + } else { + project.version.toString().replace("-SNAPSHOT", "-dev") + } + } + + // https://melix.github.io/blog/2021/03/version-catalogs-faq.html#_but_how_can_i_use_the_catalog_in_em_plugins_em_defined_in_code_buildsrc_code + val libs: VersionCatalog by lazy { + project.extensions.getByType().named("libs") + } + + init { + if (!isReleaseBuild) { + project.version = "${project.version}-SNAPSHOT" + } + } +} diff --git a/buildSrc/src/main/kotlin/ExecutableJar.kt b/buildSrc/src/main/kotlin/ExecutableJar.kt new file mode 100644 index 00000000..70b79706 --- /dev/null +++ b/buildSrc/src/main/kotlin/ExecutableJar.kt @@ -0,0 +1,47 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.listProperty + +/** + * Builds a self-contained Pkl CLI Jar that is directly executable on *nix + * and executable with `java -jar` on Windows. + * + * For direct execution, the `java` command must be on the PATH. + * + * https://skife.org/java/unix/2011/06/20/really_executable_jars.html + */ +open class ExecutableJar : DefaultTask() { + @get:InputFile + val inJar: RegularFileProperty = project.objects.fileProperty() + + @get:OutputFile + val outJar: RegularFileProperty = project.objects.fileProperty() + + @get:Input + val jvmArgs: ListProperty = project.objects.listProperty() + + @TaskAction + fun buildJar() { + val inFile = inJar.get().asFile + val outFile = outJar.get().asFile + val escapedJvmArgs = jvmArgs.get().joinToString(separator = " ") { "\"$it\"" } + + val startScript = """ + #!/bin/sh + exec java $escapedJvmArgs -jar $0 "$@" + """.trim().trimMargin() + "\n\n\n" + + outFile.outputStream().use { outStream -> + startScript.byteInputStream().use { it.copyTo(outStream) } + inFile.inputStream().use { it.copyTo(outStream) } + } + + // chmod a+x + outFile.setExecutable(true, false) + } +} diff --git a/buildSrc/src/main/kotlin/GradlePluginTests.kt b/buildSrc/src/main/kotlin/GradlePluginTests.kt new file mode 100644 index 00000000..35c9e867 --- /dev/null +++ b/buildSrc/src/main/kotlin/GradlePluginTests.kt @@ -0,0 +1,7 @@ +import org.gradle.util.GradleVersion + +open class GradlePluginTests { + lateinit var minGradleVersion: GradleVersion + lateinit var maxGradleVersion: GradleVersion + var skippedGradleVersions: List = listOf() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/GradleVersionInfo.kt b/buildSrc/src/main/kotlin/GradleVersionInfo.kt new file mode 100644 index 00000000..6bd1cf16 --- /dev/null +++ b/buildSrc/src/main/kotlin/GradleVersionInfo.kt @@ -0,0 +1,68 @@ +import java.net.URL +import org.gradle.util.GradleVersion +import groovy.json.JsonSlurper + +@Suppress("unused") +class GradleVersionInfo(json: Map) { + val version: String by json + + val gradleVersion: GradleVersion by lazy { GradleVersion.version(version) } + + val isReleaseVersion: Boolean by lazy { + // for some reason, `gradleVersion == gradleVersion.baseVersion` is a compile error + gradleVersion.version == gradleVersion.baseVersion.version + } + + val buildTime: String by json + + val current: Boolean by json + + val snapshot: Boolean by json + + val nightly: Boolean by json + + val releaseNightly: Boolean by json + + val activeRc: Boolean by json + + val rcFor: String by json + + val milestoneFor: String by json + + val broken: Boolean by json + + val downloadUrl: String by json + + val checksumUrl: String by json + + val wrapperChecksumUrl: String by json + + companion object { + private fun fetchAll(): List = fetchMultiple("https://services.gradle.org/versions/all") + + fun fetchReleases(): List = fetchAll().filter { it.isReleaseVersion } + + fun fetchCurrent(): GradleVersionInfo = fetchSingle("https://services.gradle.org/versions/current") + + fun fetchRc(): GradleVersionInfo? = fetchSingleOrNull("https://services.gradle.org/versions/release-candidate") + + fun fetchNightly(): GradleVersionInfo = fetchSingle("https://services.gradle.org/versions/nightly") + + private fun fetchSingle(url: String): GradleVersionInfo { + @Suppress("UNCHECKED_CAST") + return GradleVersionInfo(JsonSlurper().parse(URL(url)) as Map) + } + + private fun fetchSingleOrNull(url: String): GradleVersionInfo? { + @Suppress("UNCHECKED_CAST") + val json = JsonSlurper().parse(URL(url)) as Map + return if (json.isEmpty()) null else GradleVersionInfo(json) + } + + private fun fetchMultiple(url: String): List { + @Suppress("UNCHECKED_CAST") + return (JsonSlurper().parse(URL(url)) as List>) + .map { GradleVersionInfo(it) } + } + } +} diff --git a/buildSrc/src/main/kotlin/HtmlValidator.kt b/buildSrc/src/main/kotlin/HtmlValidator.kt new file mode 100644 index 00000000..07eca124 --- /dev/null +++ b/buildSrc/src/main/kotlin/HtmlValidator.kt @@ -0,0 +1,6 @@ +import org.gradle.api.Project +import org.gradle.api.file.FileCollection + +open class HtmlValidator(project: Project) { + var sources: FileCollection = project.files() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/MergeSourcesJars.kt b/buildSrc/src/main/kotlin/MergeSourcesJars.kt new file mode 100644 index 00000000..e5defc49 --- /dev/null +++ b/buildSrc/src/main/kotlin/MergeSourcesJars.kt @@ -0,0 +1,115 @@ +import java.io.File +import java.util.regex.Matcher +import java.util.regex.Pattern +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileVisitDetails +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.mapProperty + +open class MergeSourcesJars : DefaultTask() { + @get:InputFiles + val inputJars: ConfigurableFileCollection = project.objects.fileCollection() + + @get:InputFiles + val mergedBinaryJars: ConfigurableFileCollection = project.objects.fileCollection() + + @get:Input + val relocatedPackages: MapProperty = project.objects.mapProperty() + + @get:Input + var sourceFileExtensions: ListProperty = project.objects.listProperty() + .convention(listOf(".java", ".kt")) + + @get:OutputFile + val outputJar: RegularFileProperty = project.objects.fileProperty() + + @TaskAction + @Suppress("unused") + fun merge() { + val binaryPaths = collectBinaryPaths() + + val relocatedPkgs = relocatedPackages.get() + + val relocatedPaths = relocatedPkgs.entries.associate { (key, value) -> toPath(key) to toPath(value) } + + // use negative lookbehind to match any that don't precede with + // a word or a period character. should catch most cases. + val importPattern = Pattern.compile("(? { + val result = mutableSetOf() + for (jar in mergedBinaryJars) { + // as of Gradle 2.4 doesn't visit dirs despite the claims + project.zipTree(jar).visit { + val details = this + if (details.isDirectory) return@visit // avoid adding empty dirs + result.add(details.relativePath.parent.pathString) + } + } + return result + } + + private fun fixImports( + relocatedPkgs: Map, + details: FileVisitDetails, + sourceText: String, + importPattern: Pattern + ): String { + val matcher = importPattern.matcher(sourceText) + val buffer = StringBuffer() + logger.debug("Inspecting file: {}", details.relativePath) + while (matcher.find()) { + val newStat = relocatedPkgs[matcher.group(2)] + logger.debug("Old: {}", matcher.group()) + logger.debug("New: {}", newStat) + matcher.appendReplacement(buffer, Matcher.quoteReplacement(newStat)) + } + matcher.appendTail(buffer) + return buffer.toString() + } + + private fun toPath(packageName: String): String = packageName.replace(".", "/") +} diff --git a/buildSrc/src/main/kotlin/ResolveSourcesJars.kt b/buildSrc/src/main/kotlin/ResolveSourcesJars.kt new file mode 100644 index 00000000..350b55c0 --- /dev/null +++ b/buildSrc/src/main/kotlin/ResolveSourcesJars.kt @@ -0,0 +1,43 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.result.ResolvedArtifactResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.jvm.JvmLibrary +import org.gradle.kotlin.dsl.property +import org.gradle.language.base.artifact.SourcesArtifact + +open class ResolveSourcesJars : DefaultTask() { + @get:InputFiles + val configuration: Property = project.objects.property() + + @get:OutputDirectory + val outputDir: DirectoryProperty = project.objects.directoryProperty() + + @TaskAction + @Suppress("UnstableApiUsage", "unused") + fun resolve() { + val componentIds = configuration.get().incoming.resolutionResult.allDependencies.map { + (it as ResolvedDependencyResult).selected.id + } + + val resolutionResult = project.dependencies.createArtifactResolutionQuery() + .forComponents(componentIds) + .withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java) + .execute() + + val resolvedJars = resolutionResult.resolvedComponents + .flatMap { it.getArtifacts(SourcesArtifact::class.java) } + .map { (it as ResolvedArtifactResult).file } + + // copying to an output dir because I don't know how else to describe task outputs + project.sync { + from(resolvedJars) + into(outputDir) + } + } +} diff --git a/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts new file mode 100644 index 00000000..4daf287a --- /dev/null +++ b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts @@ -0,0 +1,95 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +val buildInfo = extensions.create("buildInfo", project) + +dependencyLocking { + lockAllConfigurations() +} + +configurations { + val rejectedVersionSuffix = Regex("-alpha|-beta|-eap|-m|-rc|-snapshot", RegexOption.IGNORE_CASE) + configureEach { + resolutionStrategy { + componentSelection { + all { + if (rejectedVersionSuffix.containsMatchIn(candidate.version)) { + reject("Rejected dependency $candidate " + + "because it has a prelease version suffix matching `$rejectedVersionSuffix`.") + } + } + } + } + } +} + +plugins.withType(JavaPlugin::class).configureEach { + val java = project.extensions.getByType() + java.sourceCompatibility = JavaVersion.VERSION_11 + java.targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xjsr305=strict", "-Xjvm-default=all") + } +} + +plugins.withType(IdeaPlugin::class).configureEach { + val errorMessage = "Use IntelliJ Gradle import instead of running the `idea` task. See README for more information." + + tasks.named("idea") { + doFirst { + throw GradleException(errorMessage) + } + } + tasks.named("ideaModule") { + doFirst { + throw GradleException(errorMessage) + } + } + if (project == rootProject) { + tasks.named("ideaProject") { + doFirst { + throw GradleException(errorMessage) + } + } + } +} + +plugins.withType(MavenPublishPlugin::class).configureEach { + configure { + // CI builds pick up artifacts from this repo. + // It's important that this repo is only declared once per project. + repositories { + maven { + name = "projectLocal" // affects task names + url = uri("file:///$rootDir/build/m2") + } + } + // use resolved/locked (e.g., `1.15`) + // instead of declared (e.g., `1.+`) + // dependency versions in generated POMs + publications { + withType(MavenPublication::class.java) { + versionMapping { + allVariants { + fromResolutionResult() + } + } + } + } + } +} + +// settings.gradle.kts sets `--write-locks` +// if Gradle command line contains this task name +val updateDependencyLocks by tasks.registering { + doLast { + configurations + .filter { it.isCanBeResolved } + .forEach { it.resolve() } + } +} + +val allDependencies by tasks.registering(DependencyReportTask::class) diff --git a/buildSrc/src/main/kotlin/pklFatJar.gradle.kts b/buildSrc/src/main/kotlin/pklFatJar.gradle.kts new file mode 100644 index 00000000..fef4f611 --- /dev/null +++ b/buildSrc/src/main/kotlin/pklFatJar.gradle.kts @@ -0,0 +1,183 @@ +import org.gradle.api.GradleException +import org.gradle.api.artifacts.Configuration +import org.gradle.api.component.AdhocComponentWithVariants +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.bundling.Jar +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.* + +plugins { + `java-library` + `maven-publish` + id("com.github.johnrengelman.shadow") +} + +// make fat Jar available to other subprojects +val fatJarConfiguration: Configuration = configurations.create("fatJar") + +val fatJarPublication: MavenPublication = publishing.publications.create("fatJar") + +// ideally we'd configure this automatically based on project dependencies +val firstPartySourcesJarsConfiguration: Configuration = configurations.create("firstPartySourcesJars") + +val relocations = mapOf( + // pkl-core dependencies + "org.antlr.v4." to "org.pkl.thirdparty.antlr.v4.", + // https://github.com/oracle/graal/issues/1644 has been fixed, + // but native-image still fails when shading com.oracle.truffle + //"com.oracle.truffle" to "org.pkl.thirdparty.truffle", + "org.graalvm." to "org.pkl.thirdparty.graalvm.", + "org.organicdesign.fp." to "org.pkl.thirdparty.paguro.", + "org.snakeyaml.engine." to "org.pkl.thirdparty.snakeyaml.engine.", + "org.msgpack." to "org.pkl.thirdparty.msgpack.", + "org.w3c.dom." to "org.pkl.thirdparty.w3c.dom", + "com.oracle.svm.core." to "org.pkl.thirdparty.svm.", + + // pkl-cli dependencies + "org.jline." to "org.pkl.thirdparty.jline.", + "com.github.ajalt.clikt." to "org.pkl.thirdparty.clikt.", + "kotlin." to "org.pkl.thirdparty.kotlin.", + "kotlinx." to "org.pkl.thirdparty.kotlinx.", + "org.intellij." to "org.pkl.thirdparty.intellij.", + "org.fusesource.jansi." to "org.pkl.thirdparty.jansi", + "org.fusesource.hawtjni." to "org.pkl.thirdparty.hawtjni", + + // pkl-doc dependencies + "org.commonmark." to "org.pkl.thirdparty.commonmark.", + "org.jetbrains." to "org.pkl.thirdparty.jetbrains.", + + // pkl-config-java dependencies + "io.leangen.geantyref." to "org.pkl.thirdparty.geantyref.", + + // pkl-codegen-java dependencies + "com.squareup.javapoet." to "org.pkl.thirdparty.javapoet.", + + // pkl-codegen-kotlin dependencies + "com.squareup.kotlinpoet." to "org.pkl.thirdparty.kotlinpoet.", +) + +val nonRelocations = listOf("com/oracle/truffle/") + +tasks.shadowJar { + inputs.property("relocations", relocations) + + archiveClassifier.set(null as String?) + + configurations = listOf(project.configurations.runtimeClasspath.get()) + + exclude("META-INF/maven/**") + exclude("META-INF/upgrade/**") + exclude("META-INF/versions/19/**") + + // org.antlr.v4.runtime.misc.RuleDependencyProcessor + exclude("META-INF/services/javax.annotation.processing.Processor") + + exclude("module-info.*") + + for ((from, to) in relocations) { + relocate(from, to) + } + + // necessary for service files to be adapted to relocation + mergeServiceFiles() +} + +// workaround for https://github.com/johnrengelman/shadow/issues/651 +components.withType(AdhocComponentWithVariants::class.java).forEach { c -> + c.withVariantsFromConfiguration(project.configurations.shadowRuntimeElements.get()) { + skip() + } +} + +val testFatJar by tasks.registering(Test::class) { + testClassesDirs = files(tasks.test.get().testClassesDirs) + classpath = + // compiled test classes + sourceSets.test.get().output + + // fat Jar + tasks.shadowJar.get().outputs.files + + // test-only dependencies + // (test dependencies that are also main dependencies must already be contained in fat Jar; + // to verify that, we don't want to include them here) + (configurations.testRuntimeClasspath.get() - configurations.runtimeClasspath.get()) +} + +tasks.check { + dependsOn(testFatJar) +} + +val validateFatJar by tasks.registering { + val outputFile = file("$buildDir/validateFatJar/result.txt") + inputs.files(tasks.shadowJar) + inputs.property("nonRelocations", nonRelocations) + outputs.file(outputFile) + + doLast { + val unshadowedFiles = mutableListOf() + zipTree(tasks.shadowJar.get().outputs.files.singleFile).visit { + val fileDetails = this + val path = fileDetails.relativePath.pathString + if (!(fileDetails.isDirectory || + path.startsWith("org/pkl/") || + path.startsWith("META-INF/") || + nonRelocations.any { path.startsWith(it) })) { + // don't throw exception inside `visit` + // as this gives a misleading "Could not expand ZIP" error message + unshadowedFiles.add(path) + } + } + if (unshadowedFiles.isEmpty()) { + outputFile.writeText("SUCCESS") + } else { + outputFile.writeText("FAILURE") + throw GradleException("Found unshadowed files:\n" + unshadowedFiles.joinToString("\n")) + } + } +} +tasks.check { + dependsOn(validateFatJar) +} + +val resolveSourcesJars by tasks.registering(ResolveSourcesJars::class) { + configuration.set(configurations.runtimeClasspath) + outputDir.set(project.file("$buildDir/resolveSourcesJars")) +} + +val fatSourcesJar by tasks.registering(MergeSourcesJars::class) { + plugins.withId("pklJavaLibrary") { + inputJars.from(tasks.named("sourcesJar")) + } + inputJars.from(firstPartySourcesJarsConfiguration) + inputJars.from(resolveSourcesJars.map { fileTree(it.outputDir) }) + + mergedBinaryJars.from(tasks.shadowJar) + relocatedPackages.set(relocations) + outputJar.fileProvider(provider { + file(tasks.shadowJar.get().archiveFile.get().asFile.path.replace(".jar", "-sources.jar")) + }) +} + +artifacts { + add("fatJar", tasks.shadowJar) +} + +publishing { + publications { + named("fatJar") { + project.shadow.component(this) + + // sources Jar is fat + artifact(fatSourcesJar.flatMap { it.outputJar.asFile }) { + classifier = "sources" + } + + plugins.withId("pklJavaLibrary") { + val javadocJar by tasks.existing(Jar::class) + // Javadoc Jar is not fat (didn't invest effort) + artifact(javadocJar.flatMap { it.archiveFile }) { + classifier = "javadoc" + } + } + } + } +} diff --git a/buildSrc/src/main/kotlin/pklGraalVm.gradle.kts b/buildSrc/src/main/kotlin/pklGraalVm.gradle.kts new file mode 100644 index 00000000..e59e1eb5 --- /dev/null +++ b/buildSrc/src/main/kotlin/pklGraalVm.gradle.kts @@ -0,0 +1,85 @@ +import java.nio.file.* +import java.util.UUID +import de.undercouch.gradle.tasks.download.Download +import de.undercouch.gradle.tasks.download.Verify + +plugins { + id("de.undercouch.download") +} + +val buildInfo = project.extensions.getByType() + +val homeDir = buildInfo.graalVm.homeDir +val baseName = buildInfo.graalVm.baseName +val installDir = buildInfo.graalVm.installDir +val downloadUrl = buildInfo.graalVm.downloadUrl +val downloadFile = file(homeDir).resolve("$baseName.tar.gz") + +// tries to minimize chance of corruption by download-to-temp-file-and-move +val downloadGraalVm by tasks.registering(Download::class) { + onlyIf { + !installDir.exists() + } + + src(downloadUrl) + dest(downloadFile) + overwrite(false) + tempAndMove(true) +} + +val verifyGraalVm by tasks.registering(Verify::class) { + onlyIf { + !installDir.exists() + } + + dependsOn(downloadGraalVm) + src(downloadFile) + checksum(buildInfo.libs.findVersion("graalVmSha256-${buildInfo.graalVm.osName}-${buildInfo.graalVm.arch}").get().toString()) + algorithm("SHA-256") +} + +// minimize chance of corruption by extract-to-random-dir-and-flip-symlink +val installGraalVm by tasks.registering { + dependsOn(verifyGraalVm) + + onlyIf { + !installDir.exists() + } + + doLast { + val distroDir = "$homeDir/${UUID.randomUUID()}" + + try { + mkdir(distroDir) + + println("Extracting $downloadFile into $distroDir") + // faster and more reliable than Gradle's `copy { from tarTree() }` + exec { + workingDir = file(distroDir) + executable = "tar" + args("--strip-components=1", "-xzf", downloadFile) + } + + val distroBinDir = if (buildInfo.os.isMacOsX) "$distroDir/Contents/Home/bin" else "$distroDir/bin" + + println("Installing native-image into $distroDir") + exec { + executable = "$distroBinDir/gu" + args("install", "--no-progress", "native-image") + } + + println("Creating symlink $installDir for $distroDir") + val tempLink = Paths.get("$homeDir/${UUID.randomUUID()}") + Files.createSymbolicLink(tempLink, Paths.get(distroDir)) + try { + Files.move(tempLink, installDir.toPath(), StandardCopyOption.ATOMIC_MOVE) + } catch (e: Exception) { + try { delete(tempLink.toFile()) } catch (ignored: Exception) {} + throw e + } + } catch (e: Exception) { + try { delete(distroDir) } catch (ignored: Exception) {} + throw e + } + } +} diff --git a/buildSrc/src/main/kotlin/pklGradlePluginTest.gradle.kts b/buildSrc/src/main/kotlin/pklGradlePluginTest.gradle.kts new file mode 100644 index 00000000..189b19ed --- /dev/null +++ b/buildSrc/src/main/kotlin/pklGradlePluginTest.gradle.kts @@ -0,0 +1,103 @@ +/** + * Allows to run Gradle plugin tests against different Gradle versions. + * + * Adds a `compatibilityTestX` task for every Gradle version X + * between `ext.minSupportedGradleVersion` and `ext.maxSupportedGradleVersion` + * that is not in `ext.gradleVersionsExcludedFromTesting`. + * The list of available Gradle versions is obtained from services.gradle.org. + * Adds lifecycle tasks to test against multiple Gradle versions at once, for example all Gradle release versions. + * Compatibility test tasks run the same tests and use the same task configuration as the project's `test` task. + * They set system properties for the Gradle version and distribution URL to be used. + * These properties are consumed by the `AbstractTest` class. + */ + +plugins { + java +} + +val gradlePluginTests = extensions.create("gradlePluginTests") + +tasks.addRule("Pattern: compatibilityTest[All|Releases|Latest|Candidate|Nightly|]") { + val taskName = this + val matchResult = Regex("compatibilityTest(.+)").matchEntire(taskName) ?: return@addRule + + when (val taskNameSuffix = matchResult.groupValues[1]) { + "All" -> + task("compatibilityTestAll") { + dependsOn("compatibilityTestReleases", "compatibilityTestCandidate", "compatibilityTestNightly") + } + // releases in configured range + "Releases" -> + task("compatibilityTestReleases") { + val versionInfos = GradleVersionInfo.fetchReleases() + val versionsToTestAgainst = versionInfos.filter { versionInfo -> + val v = versionInfo.gradleVersion + !versionInfo.broken && + v in gradlePluginTests.minGradleVersion..gradlePluginTests.maxGradleVersion && + v !in gradlePluginTests.skippedGradleVersions + } + + dependsOn(versionsToTestAgainst.map { createCompatibilityTestTask(it) }) + } + // latest release (if not developing against latest) + "Latest" -> + task("compatibilityTestLatest") { + val versionInfo = GradleVersionInfo.fetchCurrent() + if (versionInfo.version == gradle.gradleVersion) { + doLast { + println("No new Gradle release available. " + + "(Run `gradlew test` to test against ${versionInfo.version}.)") + } + } else { + dependsOn(createCompatibilityTestTask(versionInfo)) + } + } + // active release candidate (if any) + "Candidate" -> + task("compatibilityTestCandidate") { + val versionInfo = GradleVersionInfo.fetchRc() + if (versionInfo?.activeRc == true) { + dependsOn(createCompatibilityTestTask(versionInfo)) + } else { + doLast { + println("No active Gradle release candidate available.") + } + } + } + // latest nightly + "Nightly" -> + task("compatibilityTestNightly") { + val versionInfo = GradleVersionInfo.fetchNightly() + dependsOn(createCompatibilityTestTask(versionInfo)) + } + // explicit version + else -> + createCompatibilityTestTask( + taskNameSuffix, + "https://services.gradle.org/distributions-snapshots/gradle-$taskNameSuffix-bin.zip" + ) + } +} + +fun createCompatibilityTestTask(versionInfo: GradleVersionInfo): Task = + createCompatibilityTestTask(versionInfo.version, versionInfo.downloadUrl) + +fun createCompatibilityTestTask(version: String, downloadUrl: String): Task { + return tasks.create("compatibilityTest$version", Test::class.java) { + mustRunAfter(tasks.test) + + maxHeapSize = tasks.test.get().maxHeapSize + jvmArgs = tasks.test.get().jvmArgs + classpath = tasks.test.get().classpath + systemProperty("testGradleVersion", version) + systemProperty("testGradleDistributionUrl", downloadUrl) + + doFirst { + if (version == gradle.gradleVersion && gradle.taskGraph.hasTask(tasks.test.get())) { + // don't test same version twice + println("This version has already been tested by the `test` task.") + throw StopExecutionException() + } + } + } +} diff --git a/buildSrc/src/main/kotlin/pklHtmlValidator.gradle.kts b/buildSrc/src/main/kotlin/pklHtmlValidator.gradle.kts new file mode 100644 index 00000000..6d09f01b --- /dev/null +++ b/buildSrc/src/main/kotlin/pklHtmlValidator.gradle.kts @@ -0,0 +1,58 @@ +plugins { + base +} + +val htmlValidator = extensions.create("htmlValidator", project) + +val buildInfo = project.extensions.getByType() + +val validatorConfiguration: Configuration = configurations.create("validator") { + resolutionStrategy.eachDependency { + if (requested.group == "log4j" && requested.name == "log4j") { + @Suppress("UnstableApiUsage") + useTarget(buildInfo.libs.findLibrary("log4j12Api").get()) + because("mitigate critical security vulnerabilities") + } + } +} + +dependencies { + @Suppress("UnstableApiUsage") + validatorConfiguration(buildInfo.libs.findLibrary("nuValidator").get()) { + // we only want jetty-util and jetty-util-ajax (with the right version) + // couldn't find a more robust way to express this + exclude(group = "org.eclipse.jetty", module = "jetty-continuation") + exclude(group = "org.eclipse.jetty", module = "jetty-http") + exclude(group = "org.eclipse.jetty", module = "jetty-io") + exclude(group = "org.eclipse.jetty", module = "jetty-security") + exclude(group = "org.eclipse.jetty", module = "jetty-server") + exclude(group = "org.eclipse.jetty", module = "jetty-servlets") + exclude(group = "javax.servlet") + exclude(group = "commons-fileupload") + } +} + +val validateHtml by tasks.registering(JavaExec::class) { + val resultFile = file("$buildDir/validateHtml/result.txt") + inputs.files(htmlValidator.sources) + outputs.file(resultFile) + + classpath = validatorConfiguration + mainClass.set("nu.validator.client.SimpleCommandLineValidator") + args("--skip-non-html") // --also-check-css doesn't work (still checks css as html), so limit to html files + args("--filterpattern", "(.*)Consider adding “lang=(.*)") + args("--filterpattern", "(.*)Consider adding a “lang” attribute(.*)") + args("--filterpattern", "(.*)unrecognized media “amzn-kf8”(.*)") // kindle + // for debugging + // args "--verbose" + args(htmlValidator.sources) + + // write a basic result file s.t. gradle can consider task up-to-date + // writing a result file in case validation fails is not easily possible with JavaExec, but also not strictly necessary + doFirst { project.delete(resultFile) } + doLast { resultFile.writeText("Success.") } +} + +tasks.check { + dependsOn(validateHtml) +} diff --git a/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts new file mode 100644 index 00000000..a33ec933 --- /dev/null +++ b/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts @@ -0,0 +1,59 @@ +@file:Suppress("HttpUrlsUsage") + +plugins { + `java-library` + id("pklKotlinTest") + id("com.diffplug.spotless") +} + +// make sources Jar available to other subprojects +val sourcesJarConfiguration = configurations.register("sourcesJar") + +java { + withSourcesJar() // creates `sourcesJar` task + withJavadocJar() +} + +artifacts { + // make sources Jar available to other subprojects + add("sourcesJar", tasks["sourcesJar"]) +} + +spotless { + java { + googleJavaFormat("1.15.0") + targetExclude("**/generated/**", "**/build/**") + licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt")) + } +} + +tasks.compileKotlin { + enabled = false +} + +tasks.jar { + manifest { + attributes += mapOf("Automatic-Module-Name" to "org.${project.name.replace("-", ".")}") + } +} + +tasks.javadoc { + classpath = sourceSets.main.get().output + sourceSets.main.get().compileClasspath + source = sourceSets.main.get().allJava + title = "${project.name} ${project.version} API" + (options as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet") +} + +val workAroundKotlinGradlePluginBug by tasks.registering { + doLast { + // Works around this problem, which sporadically appears and disappears in different subprojects: + // A problem was found with the configuration of task ':pkl-executor:compileJava' (type 'JavaCompile'). + // > Directory '[...]/pkl/pkl-executor/build/classes/kotlin/main' + // specified for property 'compileKotlinOutputClasses' does not exist. + file("$buildDir/classes/kotlin/main").mkdirs() + } +} + +tasks.compileJava { + dependsOn(workAroundKotlinGradlePluginBug) +} diff --git a/buildSrc/src/main/kotlin/pklKotlinLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklKotlinLibrary.gradle.kts new file mode 100644 index 00000000..72a829cf --- /dev/null +++ b/buildSrc/src/main/kotlin/pklKotlinLibrary.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("pklJavaLibrary") + + kotlin("jvm") +} + +val buildInfo = project.extensions.getByType() + +dependencies { + // At least some of our kotlin APIs contain Kotlin stdlib types + // that aren't compiled away by kotlinc (e.g., `kotlin.Function`). + // So let's be conservative and default to `api` for now. + // For Kotlin APIs that only target Kotlin users (e.g., pkl-config-kotlin), + // it won't make a difference. + api(buildInfo.libs.findLibrary("kotlinStdLib").get()) +} + +tasks.compileKotlin { + enabled = true // disabled by pklJavaLibrary +} + +spotless { + kotlin { + ktfmt("0.44").googleStyle() + targetExclude("**/generated/**", "**/build/**") + licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt")) + } +} diff --git a/buildSrc/src/main/kotlin/pklKotlinTest.gradle.kts b/buildSrc/src/main/kotlin/pklKotlinTest.gradle.kts new file mode 100644 index 00000000..0b40c956 --- /dev/null +++ b/buildSrc/src/main/kotlin/pklKotlinTest.gradle.kts @@ -0,0 +1,57 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import java.net.URI + +plugins { + kotlin("jvm") +} + +val buildInfo = project.extensions.getByType() + +dependencies { + testImplementation(buildInfo.libs.findLibrary("assertj").get()) + testImplementation(buildInfo.libs.findLibrary("junitApi").get()) + testImplementation(buildInfo.libs.findLibrary("junitParams").get()) + testImplementation(buildInfo.libs.findLibrary("kotlinStdLib").get()) + + testRuntimeOnly(buildInfo.libs.findLibrary("junitEngine").get()) +} + +tasks.withType().configureEach { + val testTask = this + + useJUnitPlatform() + + // enable checking of stdlib return types + systemProperty("org.pkl.testMode", "true") + + reports.named("html") { + enabled = true + } + + testLogging { + exceptionFormat = TestExceptionFormat.FULL + } + + addTestListener(object : TestListener { + override fun beforeSuite(suite: TestDescriptor) {} + override fun beforeTest(testDescriptor: TestDescriptor) {} + override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {} + + // print report link at end of task, not just at end of build + override fun afterSuite(descriptor: TestDescriptor, result: TestResult) { + if (descriptor.parent != null) return // only interested in overall result + + if (result.resultType == TestResult.ResultType.FAILURE) { + println("\nThere were failing tests. See the report at: ${fixFileUri(testTask.reports.html.entryPoint.toURI())}") + } + } + + // makes links clickable on macOS + private fun fixFileUri(uri: URI): URI { + if ("file" == uri.scheme && !uri.schemeSpecificPart.startsWith("//")) { + return URI.create("file://" + uri.schemeSpecificPart) + } + return uri + } + }) +} diff --git a/buildSrc/src/main/kotlin/pklNativeBuild.gradle.kts b/buildSrc/src/main/kotlin/pklNativeBuild.gradle.kts new file mode 100644 index 00000000..78c2a2ff --- /dev/null +++ b/buildSrc/src/main/kotlin/pklNativeBuild.gradle.kts @@ -0,0 +1,7 @@ +val assembleNative by tasks.registering {} + +val checkNative by tasks.registering {} + +val buildNative by tasks.registering { + dependsOn(assembleNative, checkNative) +} diff --git a/buildSrc/src/main/kotlin/pklPublishLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklPublishLibrary.gradle.kts new file mode 100644 index 00000000..c279df36 --- /dev/null +++ b/buildSrc/src/main/kotlin/pklPublishLibrary.gradle.kts @@ -0,0 +1,118 @@ +import org.gradle.api.publish.maven.tasks.GenerateMavenPom +import java.nio.charset.StandardCharsets +import java.util.Base64 + +plugins { + `maven-publish` + signing +} + +publishing { + publications { + components.findByName("java")?.let { javaComponent -> + create("library") { + from(javaComponent) + } + } + withType().configureEach { + pom { + name.set(artifactId) + licenses { + license { + name.set("The Apache Software License, Version 2.0") + url.set("https://github.com/apple/pkl/blob/main/LICENSE.txt") + } + } + developers { + developer { + id.set("pkl-authors") + name.set("The Pkl Authors") + email.set("pkl-oss@group.apple.com") + } + } + scm { + connection.set("scm:git:git://github.com/apple/pkl.git") + developerConnection.set("scm:git:ssh://github.com/apple/pkl.git") + val buildInfo = project.extensions.getByType() + url.set("https://github.com/apple/pkl/tree/${buildInfo.commitish}") + } + issueManagement { + system.set("GitHub Issues") + url.set("https://github.com/apple/pkl/issues") + } + ciManagement { + system.set("Circle CI") + url.set("https://app.circleci.com/pipelines/github/apple/pkl") + } + } + } + } +} + +val validatePom by tasks.registering { + val generatePomFileForLibraryPublication by tasks.existing(GenerateMavenPom::class) + val outputFile = file("$buildDir/validatePom") // dummy output to satisfy up-to-date check + + dependsOn(generatePomFileForLibraryPublication) + inputs.file(generatePomFileForLibraryPublication.get().destination) + outputs.file(outputFile) + + doLast { + outputFile.delete() + + val pomFile = generatePomFileForLibraryPublication.get().destination + assert(pomFile.exists()) + + val text = pomFile.readText() + + run { + val unresolvedVersion = Regex(".*[+,()\\[\\]].*") + val matches = unresolvedVersion.findAll(text).toList() + if (matches.isNotEmpty()) { + throw GradleException( + """ + Found unresolved version selector(s) in generated POM: + ${matches.joinToString("\n") { it.groupValues[0] }} + """.trimIndent() + ) + } + } + + val buildInfo = project.extensions.getByType() + if (buildInfo.isReleaseBuild) { + val snapshotVersion = Regex(".*-SNAPSHOT") + val matches = snapshotVersion.findAll(text).toList() + if (matches.isNotEmpty()) { + throw GradleException( + """ + Found snapshot version(s) in generated POM of Pkl release version: + ${matches.joinToString("\n") { it.groupValues[0] }} + """.trimIndent() + ) + } + } + + outputFile.writeText("OK") + } +} + +tasks.publish { + dependsOn(validatePom) +} + +signing { + // provided as env vars `ORG_GRADLE_PROJECT_signingKey` and `ORG_GRADLE_PROJECT_signingPassword` + // in CI. + val signingKey = (findProperty("signingKey") as String?) + ?.let { Base64.getDecoder().decode(it).toString(StandardCharsets.US_ASCII) } + val signingPassword = findProperty("signingPassword") as String? + if (signingKey != null && signingPassword != null) { + useInMemoryPgpKeys(signingKey, signingPassword) + } + publishing.publications.findByName("library")?.let { sign(it) } +} + +artifacts { + project.tasks.findByName("javadocJar")?.let { archives(it) } + project.tasks.findByName("sourcesJar")?.let { archives(it) } +} diff --git a/buildSrc/src/main/resources/license-header.line-comment.txt b/buildSrc/src/main/resources/license-header.line-comment.txt new file mode 100644 index 00000000..02c9978d --- /dev/null +++ b/buildSrc/src/main/resources/license-header.line-comment.txt @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + diff --git a/buildSrc/src/main/resources/license-header.star-block.txt b/buildSrc/src/main/resources/license-header.star-block.txt new file mode 100644 index 00000000..f83fa8dc --- /dev/null +++ b/buildSrc/src/main/resources/license-header.star-block.txt @@ -0,0 +1,15 @@ +/** + * 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. + */ diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 00000000..56898877 --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,6 @@ +name: main +title: Main Project +version: 0.25.0-dev +prerelease: true +nav: +- nav.adoc diff --git a/docs/docs.gradle.kts b/docs/docs.gradle.kts new file mode 100644 index 00000000..77d90f9f --- /dev/null +++ b/docs/docs.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension + +plugins { + pklAllProjects + pklKotlinTest +} + +sourceSets { + test { + java { + srcDir(file("modules/pkl-core/examples")) + srcDir(file("modules/pkl-config-java/examples")) + } + val kotlin = project.extensions + .getByType() + .sourceSets[name] + .kotlin + kotlin.srcDir(file("modules/pkl-config-kotlin/examples")) + } +} + +dependencies { + testImplementation(project(":pkl-core")) + testImplementation(project(":pkl-config-java")) + testImplementation(project(":pkl-config-kotlin")) + testImplementation(project(":pkl-commons-test")) + testImplementation(libs.junitEngine) + testImplementation(libs.antlrRuntime) +} + +tasks.test { + inputs.files(fileTree("modules").matching { + include("**/pages/*.adoc") + }) +} diff --git a/docs/gradle.lockfile b/docs/gradle.lockfile new file mode 100644 index 00000000..196e592f --- /dev/null +++ b/docs/gradle.lockfile @@ -0,0 +1,36 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.tunnelvisionlabs:antlr4-runtime:4.9.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.14=testRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:22.3.1=testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=testRuntimeClasspath +empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileClasspath,compileOnly,compileOnlyDependenciesMetadata,default,implementationDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeClasspath,runtimeOnlyDependenciesMetadata,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/docs/modules/ROOT/pages/community.adoc b/docs/modules/ROOT/pages/community.adoc new file mode 100644 index 00000000..121c6fd2 --- /dev/null +++ b/docs/modules/ROOT/pages/community.adoc @@ -0,0 +1,8 @@ += Community +:uri-github-issue: https://github.com/apple/pkl/issues +:uri-github-discussions: https://github.com/apple/pkl/discussions + +We'd love to hear from you! + +* Create an {uri-github-issue}[issue] +* Ask questions on {uri-github-discussions}[GitHub Discussions] diff --git a/docs/modules/ROOT/pages/examples.adoc b/docs/modules/ROOT/pages/examples.adoc new file mode 100644 index 00000000..45d9467e --- /dev/null +++ b/docs/modules/ROOT/pages/examples.adoc @@ -0,0 +1,14 @@ += Examples + +:uri-github-apple: https://github.com/apple +:uri-jvm-examples: {uri-github-apple}/pkl-jvm-examples +:uri-go-examples: {uri-github-apple}/pkl-go-examples +:uri-swift-examples: {uri-github-apple}/pkl-swift-examples +:uri-k8s-examples: {uri-github-apple}/pkl-k8s-examples + +For ready-to-go examples with full source code, see the various repositories that are available for you. + +* {uri-jvm-examples}[pkl-jvm-examples] -- for using Pkl within the JVM +* {uri-swift-examples}[pkl-swift-examples] -- for using Pkl with Swift +* {uri-go-examples}[pkl-go-examples] -- for using Pkl with Go +* {uri-k8s-examples}[pkl-k8s-examples] -- for using Pkl with Kubernetes diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 00000000..33dca19b --- /dev/null +++ b/docs/modules/ROOT/pages/index.adoc @@ -0,0 +1,15 @@ += User Manual +include::../partials/component-attributes.adoc[] + +Quick Links: xref:pkl-cli:index.adoc#installation[Installation] | xref:language-reference:index.adoc[Language Reference] + +Pkl -- pronounced _Pickle_ -- is an embeddable configuration language which provides rich support for data templating and validation. +It can be used from the command line, integrated in a build pipeline, or embedded in a program. +Pkl scales from small to large, simple to complex, ad-hoc to repetitive configuration tasks. + +xref:introduction:index.adoc[Introduction]:: Why we created Pkl and what it can do for you. +xref:language.adoc[Language]:: Get to know the language and standard library. +xref:language-bindings.adoc[Bindings]:: Libraries for embedding Pkl into general-purpose languages. +xref:tools.adoc[Tools]:: CLI, Gradle plugin, code generators, and other tools. +link:{uri-pkl-examples-repo}[Examples]:: Ready-to-go examples with full source code. +xref:release-notes:index.adoc[Release Notes]:: What's new in each release. diff --git a/docs/modules/ROOT/pages/integrations.adoc b/docs/modules/ROOT/pages/integrations.adoc new file mode 100644 index 00000000..9fb71f88 --- /dev/null +++ b/docs/modules/ROOT/pages/integrations.adoc @@ -0,0 +1,3 @@ += Framework Integrations + +* xref:spring:ROOT:index.adoc[Spring (Boot) Integration] diff --git a/docs/modules/ROOT/pages/language-bindings.adoc b/docs/modules/ROOT/pages/language-bindings.adoc new file mode 100644 index 00000000..fab388fe --- /dev/null +++ b/docs/modules/ROOT/pages/language-bindings.adoc @@ -0,0 +1,6 @@ += Language Bindings + +* xref:java-binding:index.adoc[Java] +* xref:kotlin-binding:index.adoc[Kotlin] +* xref:swift:ROOT:index.adoc[Swift] +* xref:go:ROOT:index.adoc[Go] diff --git a/docs/modules/ROOT/pages/language.adoc b/docs/modules/ROOT/pages/language.adoc new file mode 100644 index 00000000..0754bdc3 --- /dev/null +++ b/docs/modules/ROOT/pages/language.adoc @@ -0,0 +1,5 @@ += Language + +* xref:language-tutorial:index.adoc[Tutorial] +* xref:language-reference:index.adoc[Language Reference] +* xref:standard-library.adoc[Standard Library] diff --git a/docs/modules/ROOT/pages/resources.adoc b/docs/modules/ROOT/pages/resources.adoc new file mode 100644 index 00000000..242428b5 --- /dev/null +++ b/docs/modules/ROOT/pages/resources.adoc @@ -0,0 +1,7 @@ += Resources +include::../partials/component-attributes.adoc[] + +There's more to explore! + +* Visit Pkl's repositories on https://github.com/apple?q=pkl[GitHub] +* Browse the standard library's https://pkl-lang.org/package-docs/pkl/{pkl-version}/[API Docs] diff --git a/docs/modules/ROOT/pages/standard-library.adoc b/docs/modules/ROOT/pages/standard-library.adoc new file mode 100644 index 00000000..bc649946 --- /dev/null +++ b/docs/modules/ROOT/pages/standard-library.adoc @@ -0,0 +1,13 @@ += Standard Library +include::../partials/component-attributes.adoc[] + +The standard library is a set of Pkl modules, versioned and distributed together with the language. +It is documented in the link:{uri-pkl-stdlib-docs-index}[API Docs]. + +To import a standard library module, use `import "pkl:"`. +For example, `import "pkl:json"` imports the `pkl.json` module. + +The `pkl.base` module defines the most fundamental properties, methods, and classes for using Pkl. +Its members are automatically available in every module and hence, it does not need to be imported. + +The default module allowlist (`--allowed-modules`) grants access to all standard library modules. diff --git a/docs/modules/ROOT/pages/tools.adoc b/docs/modules/ROOT/pages/tools.adoc new file mode 100644 index 00000000..1da3fc0c --- /dev/null +++ b/docs/modules/ROOT/pages/tools.adoc @@ -0,0 +1,10 @@ += Tools +include::ROOT:partial$component-attributes.adoc[] + +* xref:pkl-cli:index.adoc[CLI] +* xref:pkl-doc:index.adoc[Pkldoc] +* xref:pkl-gradle:index.adoc[Gradle Plugin] +* Editor support +** xref:intellij:ROOT:index.adoc[IntelliJ] +** xref:vscode:ROOT:index.adoc[VSCode] +** xref:neovim:ROOT:index.adoc[Neovim] diff --git a/docs/modules/ROOT/partials/component-attributes.adoc b/docs/modules/ROOT/partials/component-attributes.adoc new file mode 100644 index 00000000..9e00c322 --- /dev/null +++ b/docs/modules/ROOT/partials/component-attributes.adoc @@ -0,0 +1,64 @@ +// TODO: move to antora.yml once supported + +// the following attributes must be updated immediately before a release + +// pkl version corresponding to current git commit without -dev suffix or git hash +:pkl-version-no-suffix: 0.25.0 +// tells whether pkl version corresponding to current git commit +// is a release version (:is-release-version: '') or dev version (:!is-release-version:) +:!is-release-version: + +// the remaining attributes do not need to be updated regularly + +:pkl-version: {pkl-version-no-suffix}-dev +ifdef::is-release-version[] +:pkl-version: {pkl-version-no-suffix} +endif::[] + +// use non-unique snapshot version because we have no way to determine unique snapshot version here +:pkl-artifact-version: {pkl-version-no-suffix}-SNAPSHOT +ifdef::is-release-version[] +:pkl-artifact-version: {pkl-version} +endif::[] + +:uri-maven-docsite: https://central.sonatype.com/ + +:uri-sonatype: https://s01.oss.sonatype.org/service/local/repositories/snapshots/content/ + +:symbolic-version-name: latest +ifdef::is-release-version[] +:symbolic-version-name: current +endif::[] +:uri-pkl-docs-base: https://pkl-lang.org/package-docs +:uri-pkl-stdlib-docs-base: {uri-pkl-docs-base}/pkl +:uri-pkl-stdlib-docs: {uri-pkl-stdlib-docs-base}/{pkl-version} +:uri-pkl-stdlib-docs-index: {uri-pkl-stdlib-docs}/ + +// TODO(oss): check these links when we have tags +:github-branch: main +ifdef::is-release-version[] +:github-branch: v{pkl-version-no-suffix} +endif::[] +:uri-github-tree: https://github.com/apple/pkl/tree/{github-branch} +:uri-pkl-stdlib-sources: {uri-github-tree}/stdlib + +:github-releases-base: https://github.com/apple/pkl/releases +:github-releases: {github-releases-base}/download/{pkl-artifact-version} + +:uri-pkl-core-main-sources: {uri-github-tree}/pkl-core/src/main/java/org/pkl/core +:uri-pkl-cli-main-sources: {uri-github-tree}/pkl-cli/src/main/kotlin/org/pkl/cli +:uri-pkl-doc-main-sources: {uri-github-tree}/pkl-doc/src/main/kotlin/org/pkl/doc + +// This attribute is used as language for Pkl code blocks. +// It can then be mapped to different languages in different environments (e.g., IntelliJ vs. Antora). +:pkl: pkl +:pkl-expr: pkl expression + +:uri-pkl-examples-repo: https://github.com/apple/pkl-jvm-examples +:uri-pkl-examples-tree: {uri-pkl-examples-repo}/tree/main +:uri-build-eval-example: {uri-pkl-examples-tree}/build-eval +:uri-codegen-java-example: {uri-pkl-examples-tree}/codegen-java +:uri-codegen-kotlin-example: {uri-pkl-examples-tree}/codegen-kotlin +:uri-config-java-example: {uri-pkl-examples-tree}/config-java +:uri-config-kotlin-example: {uri-pkl-examples-tree}/config-kotlin +:uri-pkldoc-example: {uri-pkl-examples-tree}/pkldoc diff --git a/docs/modules/introduction/pages/comparison.adoc b/docs/modules/introduction/pages/comparison.adoc new file mode 100644 index 00000000..b498fe27 --- /dev/null +++ b/docs/modules/introduction/pages/comparison.adoc @@ -0,0 +1,93 @@ += Comparison +include::ROOT:partial$component-attributes.adoc[] +:uri-jsonnet: https://jsonnet.org +:uri-hcl: https://github.com/hashicorp/hcl +:uri-dhall: https://dhall-lang.org +:uri-pkl-spring: https://github.com/apple/pkl-spring +:uri-graalvm: https://www.graalvm.org + +Configuration is often described in a static configuration format or is generated with a general-purpose programming language. +This page lists shortcomings of these approaches and explains how Pkl addresses them. +Also, Pkl's strong and weak points in comparison to other configuration languages are discussed in this document. + +[[static-config-formats]] +== Pkl vs. Static Config Formats + +Static configuration formats such as JSON, YAML, and XML work reasonably well for simple configuration needs. +However, they do have some shortcomings, including: + +. They are not very human-friendly to read and write. (JSON, XML) +. They do not provide a way to split a large file into multiple smaller ones. (JSON, YAML) +. They offer no way or very limited ways to abstract over repetitive configuration. (JSON, YAML, XML) +. They do not offer standardized or widely available schema validators. (JSON, YAML) +. They offer little or no schema-aware tooling. (JSON, YAML) + +Pkl addresses these shortcomings as follows: + +. It has a clutter-free and familiar syntax with nestable comments. +. Modules can import other modules from local and remote locations. +. Every object can act as a template for other objects. + The standard library offers strong support for data manipulation. +. It has strong built-in support for describing and validating configuration schemas. +. It is designed to enable schema-aware tooling, such as REPLs and editors with code completion support. + +[[general-purpose-langs]] +== Pkl vs. General-purpose Languages + +When configuration needs outgrow the capabilities of static configuration formats, +projects often turn to generate configuration with a general-purpose programming language such as Python. +Given enough effort, this approach can satisfy complex configuration needs. +However, expressing configuration in a full-blown programming language does have some shortcomings, including: + +. Reading, writing, and debugging configuration can become as challenging as reading, writing, and debugging application code. +. The host language may not be a good fit for describing, manipulating, and abstracting over hierarchical configuration. +. Configuration code may not visually resemble the configuration it generates. +. The host language may not be a good fit for defining and validating configuration schemas. +. Development environments may offer little help for developing and validating configuration written in the host language. +. General-purpose languages are powerful and often difficult to sandbox. + Are you certain your configuration script isn't erasing your hard disk or launching a rocket? + +Pkl addresses these shortcomings as follows: + +. As an expression-oriented and side-effect free language, it eliminates many potential sources of errors. +. It is specifically designed for describing, manipulating, and abstracting over hierarchical configuration. +. Pkl code often resembles the configuration it generates. +. It has strong built-in support for defining and validating configuration schemas. +. It is designed to enable advanced and schema-aware tooling. +. It is comparatively powerless and strictly sandboxed, making fatal configuration mistakes and exploits less likely. + Till now, we haven't spotted any Pkl script capable of erasing your hard disk. + +[[other-config-langs]] +== Pkl vs. Other Config Languages + +Compared to open-source configuration languages such as link:{uri-jsonnet}[Jsonnet], +link:{uri-hcl}[HCL], and link:{uri-dhall}[Dhall], Pkl's strong points are: + +General:: ++ +* Pkl has a clean and familiar syntax, which makes it easier to read and learn. +* Pkl supports writing sophisticated schemas, which enables config validation, code and documentation generation, and advanced IDE support. + This is Pkl's most significant differentiator, and is the main reason why we created it. +* Pkl has stronger templating capabilities than other config languages, reducing user code to the absolute minimum. + +Embedding:: ++ +* Pkl is great for embedding into JVM applications. +* Pkl offers modern xref:java-binding:pkl-config-java.adoc[JVM libraries] for runtime application configuration. +* Pkl supports xref:java-binding:codegen.adoc[code generation] to enable statically typed access to configuration from programming languages. +* Pkl integrates with third-party (link:{uri-pkl-spring}[Spring Boot]) JVM libraries and frameworks. + +Tooling:: ++ +* Pkl has a polished xref:pkl-doc:index.adoc[documentation generator] that produces highly navigable and searchable documentation. +* Pkl offers a xref:pkl-gradle:index.adoc[Gradle plugin] to easily integrate code evaluation, documentation generation, and code generation into your builds. +* Pkl's native executables have a link:{uri-graalvm}[JIT compiler] that can speed up evaluation up to hundred times. + +On the other hand, we believe that Pkl's weak points are: + +* Pkl's native binaries are larger than those of other config languages. +* Pkl is less known and has a smaller community than some other config languages. + +We are working towards making Pkl overcome these weakness. Please support us in reaching this goal! + +We hope that you will enjoy Pkl, and that you trust us to gradually improve its weak points. diff --git a/docs/modules/introduction/pages/concepts.adoc b/docs/modules/introduction/pages/concepts.adoc new file mode 100644 index 00000000..7cb0883f --- /dev/null +++ b/docs/modules/introduction/pages/concepts.adoc @@ -0,0 +1,97 @@ += Concepts +include::ROOT:partial$component-attributes.adoc[] +:uri-property-list: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/UnderstandXMLPlist/UnderstandXMLPlist.html + +Let's get to know Pkl by discussing some of its concepts and features. + +[[abstraction]] +== Abstraction + +Configuration tends to grow larger and more complex over time, making it increasingly difficult to understand and maintain. +Pkl can reduce the size and complexity of configuration by + +* describing similar configuration elements in terms of their differences +* introducing abstractions for common configuration elements +* separating configuration structure from configuration data +* computing instead of enumerating configuration + +[[evaluation]] +== Evaluation + +Pkl code lives in _modules_, a more fancy and general term for _files_. +Evaluating a module produces an in-memory _data model_ that is roughly comparable to a JSON data model. +If evaluation completes successfully, the Pkl evaluator converts the data model to an external representation and terminates with the status code zero. +Otherwise, the evaluator prints an error message and terminates with a non-zero status code. + +[[immutability]] +== Immutability + +All Pkl data is immutable. +Manipulating a value always returns a new value, leaving the original value unchanged. +Immutability eliminates many potential sources of errors. + +[[isolation]] +== Isolation + +Evaluation of Pkl code is strictly sandboxed. +Except for a few well-defined and well-controlled exceptions, Pkl code cannot interact with the outside world. +Leaving aside bugs in the language implementation, the worst thing that buggy or malicious Pkl code can do is to consume CPU and memory resources until the evaluator gets killed. +Over time, sandboxing will be further strengthened to cover fine-grained CPU and memory boxing. + +[[rendering]] +== Rendering + +Converting a data model to an external representation is called _rendering_ the model. +Pkl ships with renderers for the following data formats: + +* JSON +* Jsonnet +* Pcf (a static subset of Pkl) +* (Java) Properties +* {uri-property-list}[Property List] +* XML +* YAML + +Support for other formats can be added by writing a custom renderer in Pkl or Java. +See xref:language-reference:index.adoc#module-output[Module Output] and xref:pkl-core:index.adoc#value-visitor[Value Visitor] for more information. + +[[resemblance]] +== Resemblance + +By design, Pkl code tends to structurally and visually resemble the configuration it generates. +This makes the code easier to read and write. + +[[reuse]] +== Reuse + +Modules can reuse other modules by xref:language-reference:index.adoc#import-module[importing] them from local or remote locations. +Imports can also be used to split up one large module into multiple smaller ones, increasing maintainability. +A configurable security policy helps to keep imports under control. + +[[schema]] +== Schema + +Configuration is structured data. +Pkl supports -- but does not require -- to express this structure as a _configuration schema_, a set of classes defining configuration properties, their defaults, types, and constraints. +Writing and maintaining a configuration schema takes some effort but, in return, provides these benefits: + +* Independent evolution of configuration schema and configuration data, often by different teams (for example service providers and service consumers). +* Automatic xref:pkl-doc:index.adoc[documentation generation]. +* Strong validation of configuration, both during development time and runtime. +* Statically typed access to configuration from xref:java-binding:codegen.adoc[Java] and other languages through code generation. +* Schema-aware development tools, for example REPLs and editors with code completion support. + +[[template]] +== Templating + +Pkl supports writing templates for objects and entire modules. +Templates can be repeatedly turned into concrete configuration by filling in the blanks, and -- when necessary -- overriding defaults. +Sharing template modules over the network can streamline complex configuration tasks for entire teams, organizations, and communities. + +[[usability]] +== Usability + +Everybody needs a configuration solution, but nobody wants to spend a lot of time learning it. +To reflect this reality, Pkl has a strong focus on usability. +For example, error messages explain causes and possible solutions and object properties maintain definition order to avoid surprises. +We hope that this focus on usability will make Pkl accessible to a wide audience of occasional users, while still leaving room for expert users and advanced use cases. diff --git a/docs/modules/introduction/pages/index.adoc b/docs/modules/introduction/pages/index.adoc new file mode 100644 index 00000000..fc17e6b4 --- /dev/null +++ b/docs/modules/introduction/pages/index.adoc @@ -0,0 +1,13 @@ += Introduction +include::ROOT:partial$component-attributes.adoc[] + +Pkl -- pronounced _Pickle_ -- is a configuration-as-code language with rich validation and tooling. +It can be used as a command line tool, software library, or build plugin. +Pkl scales from small to large, simple to complex, ad-hoc to recurring configuration tasks. + +We created Pkl because we believe that configuration is best expressed in a special-purpose configuration language; +a blend between a static configuration format, and a general-purpose programming language. + +* xref:use-cases.adoc[Use Cases] +* xref:concepts.adoc[Concepts] +* xref:comparison.adoc[Comparison] diff --git a/docs/modules/introduction/pages/use-cases.adoc b/docs/modules/introduction/pages/use-cases.adoc new file mode 100644 index 00000000..6ee00c99 --- /dev/null +++ b/docs/modules/introduction/pages/use-cases.adoc @@ -0,0 +1,35 @@ += Use Cases +include::ROOT:partial$component-attributes.adoc[] +:uri-kotlin-homepage: https://kotlinlang.org +:uri-xml-property-lists: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/UnderstandXMLPlist/UnderstandXMLPlist.html + +Pkl is a good fit for: + +Generating Static Configuration:: +Are you using a tool, service, or application that is configured with JSON, YAML, or any other static configuration format? ++ +By generating this configuration with Pkl, you can reduce verbosity and increase maintainability through xref:concepts.adoc#reuse[reuse], xref:concepts.adoc#template[templating], and xref:concepts.adoc#abstraction[abstraction]. +JSON, YAML, and {uri-xml-property-lists}[XML property lists] are supported out of the box; xref:concepts.adoc#rendering[renderers] for other configuration formats can be developed and shared by anyone. +Automatic defaults, strong validation, and sensible error messages come in reach with configuration xref:concepts.adoc#schema[schemas]. ++ +Generation can be triggered manually, by an automation pipeline, or by the target application. + +Application Runtime Configuration:: +Are you the author of a tool, service, or application that consumes configuration? ++ +By adopting Pkl as your "native" configuration solution (rather than, say, using it to generate JSON files), you benefit from a modern xref:java-binding:pkl-config-java.adoc[configuration library] that is safe, easy, and enjoyable to use. +At the same time, anyone configuring your application -- whether that's your users, site reliability engineers (SREs), or yourself -- benefit from a well-defined, well-documented, and scalable configuration language. ++ +At the time of writing, Pkl offers configuration libraries for the JVM runtime, Swift, and also for Golang. ++ +We maintian the following libraries: ++ +* xref:java-binding:pkl-config-java.adoc[pkl-config-java] for Java compatible languages +* xref:kotlin-binding:pkl-config-kotlin.adoc[pkl-config-kotlin] for the {uri-kotlin-homepage}[Kotlin] language. +* xref:swift:ROOT:index.adoc[pkl-swift] for the Swift language. +* xref:go:ROOT:index.adoc[pkl-go] for the Go language. + +In the future, we hope to add support for other popular languages and platforms, realizing our vision of a polyglot config solution based on a single config language. + +:: +We are just getting started. Tell us about _your_ Pkl success story! diff --git a/docs/modules/java-binding/examples/JavaConfigExample.java b/docs/modules/java-binding/examples/JavaConfigExample.java new file mode 100644 index 00000000..7f0e4b01 --- /dev/null +++ b/docs/modules/java-binding/examples/JavaConfigExample.java @@ -0,0 +1,22 @@ +import org.pkl.config.java.Config; +import org.pkl.config.java.ConfigEvaluator; +import org.pkl.config.java.JavaType; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("unused") +// the pkl/pkl-examples repo has a similar example +public class JavaConfigExample { + @Test + public void usage() { + // tag::usage[] + Config config; + try (var evaluator = ConfigEvaluator.preconfigured()) { // <1> + config = evaluator.evaluateText( + "pigeon { age = 5; diet = \"Seeds\" }"); // <2> + } + var pigeon = config.get("pigeon"); // <3> + var age = pigeon.get("age").as(int.class); // <4> + var diet = pigeon.get("diet").as(JavaType.listOf(String.class)); // <5> + // end::usage[] + } +} diff --git a/docs/modules/java-binding/pages/codegen.adoc b/docs/modules/java-binding/pages/codegen.adoc new file mode 100644 index 00000000..d69ed2e4 --- /dev/null +++ b/docs/modules/java-binding/pages/codegen.adoc @@ -0,0 +1,180 @@ += Java Code Generator +include::ROOT:partial$component-attributes.adoc[] +:uri-pkl-codgen-java-maven-module: {uri-maven-docsite}/artifact/org.pkl-lang/pkl-codegen-java + +The Java source code generator takes Pkl class definitions as an input, and generates corresponding Java classes with equally named properties. + +The benefits of code generation are: + +* Configuration can be conveniently consumed as statically typed Java objects. +* The entire configuration tree can be code-completed in Java IDEs. +* Any drift between Java code and Pkl configuration structure is caught at compile time. + +The generated classes are immutable and have component-wise implementations of `equals()`, `hashCode()`, and `toString()`. + +== Installation + +The code generator is offered as Gradle plugin, Java library, and CLI. + +=== Gradle Plugin + +See xref:pkl-gradle:index.adoc#installation[Installation] in the Gradle plugin chapter. + +[[install-library]] +=== Java Library + +The `pkl-codegen-java` library is available {uri-pkl-codgen-java-maven-module}[from Maven Central]. +It requires Java 11 or higher. + +ifndef::is-release-version[] +NOTE: Snapshots are published to repository `{uri-sonatype}`. +endif::[] + +==== Gradle + +To use the library in a Gradle project, declare the following dependency: + +[tabs] +==== +Groovy:: ++ +.build.gradle +[source,groovy,subs="+attributes"] +---- +dependencies { + compile "org.pkl-lang:pkl-codegen-java:{pkl-artifact-version}" +} + +ifndef::is-release-build[] +repositories { + maven { url "{uri-sonatype}" } +} +endif::[] +---- + +Kotlin:: ++ +.build.gradle.kts +[source,kotlin,subs="+attributes"] +---- +dependencies { + compile("org.pkl-lang:pkl-codegen-java:{pkl-artifact-version}") +} + +ifndef::is-release-build[] +repositories { + maven { url = uri("{uri-sonatype}") } +} +endif::[] +---- +==== + +==== Maven + +To use the library in a Maven project, declare the following dependency: + +.pom.xml +[source,xml,subs="+attributes"] +---- + + + org.pkl-lang + pkl-codegen-java + {pkl-artifact-version} + +ifndef::is-release-build[] + + + sonatype-s01 + Sonatype S01 + {uri-sonatype} + + +endif::[] + +---- + +[[install-cli]] +=== CLI + +The CLI is bundled with the Java library. +As we do not currently ship the CLI as a self-contained Jar, we recommend to provision it with a Maven compatible build tool as shown in <>. + +[[codegen-java-usage]] +== Usage + +The code generator is offered as Gradle plugin, Java library, and CLI. + +=== Gradle Plugin + +See xref:pkl-gradle:index.adoc#java-code-gen[Java Code Generation] in the Gradle plugin chapter. + +=== Java Library + +The Java library offers two APIs: a high-level API that corresponds to the CLI, and a lower-level API that provides additional features and control. +The entry points for these APIs are `org.pkl.codegen.java.CliJavaCodeGenerator` and `org.pkl.codegen.java.JavaCodeGenerator`, respectively. +For more information, refer to the Javadoc documentation. + +=== CLI + +As explained in <>, the CLI is bundled with the Java library. +To run the CLI, execute the library Jar or its `org.pkl.codegen.java.Main` main class. + +*Synopsis:* `java -cp -jar pkl-codegen-java.jar [] ` + +``:: +The absolute or relative URIs of the modules to generate classes for. +Relative URIs are resolved against the working directory. + +==== Options + +.--generate-getters +[%collapsible] +==== +Default: (flag not set) + +Flag that indicates to generate private final fields and public getter methods instead of public final fields. +==== + +.--generate-javadoc +[%collapsible] +==== +Default: (flag not set) + +Flag that indicates to generate Javadoc based on doc comments for Pkl modules, classes, and properties. +==== + +.--params-annotation +[%collapsible] +==== +Default: `org.pkl.config.java.mapper.Named` + +Fully qualified name of the annotation to use on constructor parameters. +==== + +.--non-null-annotation +[%collapsible] +==== +Default: `org.pkl.config.java.mapper.NonNull` + +Fully qualified named of the annotation class to use for non-null types. + +This annotation is required to have `java.lang.annotation.ElementType.TYPE_USE` as a `@Target` +or it may generate code that does not compile. +==== + +.--implement-serializable +[%collapsible] +==== +Default: (flag not set) + +Whether to make generated classes implement `java.io.Serializable`. +==== + +Common code generator options: + +include::{partialsdir}/cli-codegen-options.adoc[] + +Common CLI options: + +include::../../pkl-cli/partials/cli-common-options.adoc[] + +[[full-example]] +== Full Example + +For a ready-to-go example with full source code, +see link:{uri-codegen-java-example}[codegen-java] in the _pkl/pkl-examples_ repository. diff --git a/docs/modules/java-binding/pages/index.adoc b/docs/modules/java-binding/pages/index.adoc new file mode 100644 index 00000000..12525b65 --- /dev/null +++ b/docs/modules/java-binding/pages/index.adoc @@ -0,0 +1,4 @@ += Integration with Java + +Pkl provides rich integration with Java. Our integration allows you to embed the Pkl runtime into your Java program, and also provides code generation from Pkl source files. + diff --git a/docs/modules/java-binding/pages/pkl-config-java.adoc b/docs/modules/java-binding/pages/pkl-config-java.adoc new file mode 100644 index 00000000..8db3f11a --- /dev/null +++ b/docs/modules/java-binding/pages/pkl-config-java.adoc @@ -0,0 +1,212 @@ += pkl-config-java Library +include::ROOT:partial$component-attributes.adoc[] +:uri-pkl-core-EvalException: {uri-pkl-core-main-sources}/EvalException.java + +:uri-pkl-config-java-maven-module: {uri-maven-docsite}/artifact/org.pkl-lang/pkl-config-java-all +:uri-pkl-config-java-main-sources: {uri-github-tree}/pkl-config-java/src/main/java/org/pkl/config/java +:uri-pkl-config-java-test-sources: {uri-github-tree}/pkl-config-java/src/test/java/org/pkl/config/java +:uri-pkl-config-java-test-resources: {uri-github-tree}/pkl-config-java/src/test/resources/org/pkl/config/java +:uri-pkl-config-java-ConfigEvaluator: {uri-pkl-config-java-main-sources}/ConfigEvaluator.java +:uri-pkl-config-java-Config: {uri-pkl-config-java-main-sources}/Config.java +:uri-pkl-config-java-ValueMapper: {uri-pkl-config-java-main-sources}/mapper/ValueMapper.java +:uri-pkl-config-java-Named: {uri-pkl-config-java-main-sources}/mapper/Named.java +:uri-pkl-config-java-Conversion: {uri-pkl-config-java-main-sources}/mapper/Conversion.java +:uri-pkl-config-java-Conversions: {uri-pkl-config-java-main-sources}/mapper/Conversions.java +:uri-pkl-config-java-ConverterFactories: {uri-pkl-config-java-main-sources}/mapper/ConverterFactories.java +:uri-pkl-config-java-Converter: {uri-pkl-config-java-main-sources}/mapper/Converter.java +:uri-pkl-config-java-ConverterFactory: {uri-pkl-config-java-main-sources}/mapper/ConverterFactory.java +:uri-pkl-config-java-PObjectToObjectByCtorTestJava: {uri-pkl-config-java-test-sources}/mapper/PObjectToObjectByCtorTest.java +:uri-pkl-config-java-PObjectToObjectByCtorTestPkl: {uri-pkl-config-java-test-resources}/mapper/PObjectToObjectByCtorTest.pkl + +The _pkl-config-java_ library builds upon xref:pkl-core:index.adoc[pkl-core]. +It offers a higher-level API specifically designed for consuming application runtime configuration. + +== Installation + +The _pkl-config-java_ library is available {uri-pkl-config-java-maven-module}[from Maven Central]. +It requires Java 11 or higher. + +=== Gradle + +To use the library in a Gradle project, declare the following dependency: + +[tabs] +==== +Groovy:: ++ +.build.gradle +[source,groovy,subs="+attributes"] +---- +dependencies { + compile "org.pkl-lang:pkl-config-java:{pkl-artifact-version}" +} + +ifndef::is-release-build[] +repositories { + maven { url "{uri-sonatype}" } +} +endif::[] +---- + +Kotlin:: ++ +.build.gradle.kts +[source,kotlin,subs="+attributes"] +---- +dependencies { + compile("org.pkl-lang:pkl-config-java:{pkl-artifact-version}") +} + +ifndef::is-release-build[] +repositories { + maven { url = uri("{uri-sonatype}") } +} +endif::[] +---- +==== + +Unlike `pkl-config-java`, `pkl-config-java__-all__` is a fat Jar with renamed third-party packages to avoid version conflicts. + +=== Maven + +To use the library in a Maven project, declare the following dependency: + +.pom.xml +[source,xml,subs="+attributes"] +---- + + + org.pkl-lang + pkl-config-java + {pkl-artifact-version} + +ifndef::is-release-build[] + + + sonatype-s01 + Sonatype S01 + {uri-sonatype} + + +endif::[] + +---- + +Unlike `pkl-config-java`, `pkl-config-java__-all__` is a fat Jar with renamed third-party packages to avoid version conflicts. + +== Usage + +=== Consuming Configuration + +The {uri-pkl-config-java-ConfigEvaluator}[`ConfigEvaluator`] class loads and evaluates Pkl modules. +If evaluation succeeds, a {uri-pkl-config-java-Config}[`Config`] object is returned. +Otherwise, an {uri-pkl-core-EvalException}[`EvalException`] with error details is thrown. + +The returned `Config` object represents the root of the Pkl configuration tree. +Intermediate and leaf nodes are also represented as `Config` objects. + +`Config` objects offer methods to + +* convert their Pkl value to a Java value of the specified type. +* navigate to child nodes. + +Let's see this in action: + +[[config-evaluator-java-example]] +[source,java,indent=0] +---- +include::{examplesdir}/JavaConfigExample.java[tags=usage] +---- +<1> Create a preconfigured `ConfigEvaluator`. +To create a customized evaluator, start from `ConfigEvaluatorBuilder.preconfigured()` or `ConfigEvaluatorBuilder.unconfigured()`. +The evaluator should be closed once it is no longer needed. +In this example, this is done with a try-with-resources statement. +Note that objects returned by the evaluator remain valid after calling `close()`. +<2> Evaluate the given text. +Other `evaluate` methods read from files, URLs, and other sources. +If evaluation fails, an {uri-pkl-core-EvalException}[`EvalException`] is thrown. +<3> Navigate from the config root to its `"pigeon"` child. +<4> Navigate from `"pigeon"` to `"age"` and get the latter's value as an `int`. +If conversion to the requested type fails, a `ConversionException` is thrown. +<5> Navigate from `"pigeon"` to `"diet"` and get the latter's value as a `List`. +Note the use of `JavaType.listOf()` for creating a parameterized type literal. +Similar methods exist for sets, maps, and other generic types. + +A `ConfigEvaluator` caches module sources and evaluation results. +To clear the cache, for example to evaluate the same module again, close the evaluator and create a new one. + +For a ready-to-go example with full source code, +see link:{uri-config-java-example}[config-java] in the _pkl/pkl-examples_ repository. + +[[object-mapping]] +=== Object Mapping + +When a `Config` object needs to convert its Pkl value to a Java value, it delegates the conversion to {uri-pkl-config-java-ValueMapper}[`ValueMapper`]. +`ValueMapper` can convert an entire `PModule` or any part thereof. + +A `ValueMapper` instance can be configured with many different Pkl-to-Java value conversions. +`ValueMapper.preconfigured()` creates an instance configured with conversions from Pkl values to: + +* Number types +* Strings +* Enums +* Collections +* Arrays +* `java.util.Optional` +* `java.time.Duration` +* `java.net.URI/URL` +* etc. + +Additionally, a preconfigured `ValueMapper` instance can convert Pkl objects to Java objects with equally named properties that are settable through a constructor. +This conversion works as follows: + +. Find the Java class constructor with the highest number of parameters. +. Match constructor parameters with Pkl object properties by name. ++ +Unmatched constructor parameters result in a conversion error. +Unmatched Pkl object properties are ignored. ++ +. Convert each Pkl property value to the corresponding constructor parameter's type. +. Invoke the constructor. + +The Pkl object's runtime type is irrelevant to this conversion. +Hence, typed and dynamic Pkl objects are equally supported. + +To perform this conversion, `ValueMapper` needs a way to obtain the Java constructor's parameter names. +They need to be provided in one of the following ways: + +* Annotate constructor with `java.beans.ConstructorProperties`. +* Annotate parameters with {uri-pkl-config-java-Named}[`Named`]. +* Annotate parameters with `javax.inject.Named`. +* Set the Java compiler flag `-parameters`. + +For a complete object mapping example, see: + +* {uri-pkl-config-java-PObjectToObjectByCtorTestJava}[`PObjectToObjectByCtorTest.java`] + +TIP: Together with xref:java-binding:codegen.adoc[code generation], object mapping provides a complete solution for consuming Pkl configuration as statically typed Java objects. +Java code never drifts from the configuration structure defined in Pkl, and the entire configuration tree can be code-completed in Java IDEs. + +==== Value Conversions + +The Pkl-to-Java value conversions that ship with the library are defined in {uri-pkl-config-java-Conversions}[`Conversions`] (for individual conversions) and {uri-pkl-config-java-ConverterFactories}[`ConverterFactories`] (for families of conversions). +To implement and register your own conversions, follow these steps: + +. For conversions from a single source type to a single target type, implement a {uri-pkl-config-java-Conversion}[`Conversion`]. ++ +Example: `Conversions.pStringToCharacter` converts a single-character `pkl.base#String` to `java.lang.Character`. + +. For conversions from one or multiple source types to one or multiple target types, implement a {uri-pkl-config-java-ConverterFactory}[`ConverterFactory`]. ++ +Example: `ConverterFactories.pCollectionToCollection` converts any `pkl.base#Collection` to any implementation of `java.util.Collection`, for any `E`. ++ +Converter factories are called once per combination of source type and (possibly parameterized) target type. +The returned `Converter`s are cached. + +. Create a `ValueMapperBuilder`, add all desired conversions, and build a `ValueMapper`. + +. Either use the `ValueMapper` directly, or connect it to a `ConfigEvaluator` through `ConfigEvaluatorBuilder`. + +== Further Information + +Refer to the Javadoc and sources published with the library, or browse the library's {uri-pkl-config-java-main-sources}[main] and {uri-pkl-config-java-test-sources}[test] sources. diff --git a/docs/modules/java-binding/partials/cli-codegen-options.adoc b/docs/modules/java-binding/partials/cli-codegen-options.adoc new file mode 100644 index 00000000..42f16e4d --- /dev/null +++ b/docs/modules/java-binding/partials/cli-codegen-options.adoc @@ -0,0 +1,23 @@ +.--indent +[%collapsible] +==== +Default: `" "` (two spaces) + +Example: `"\t"` (one tab) + +The characters to use for indenting generated source code. +==== + +.-o, --output-dir +[%collapsible] +==== +Default: (not set) + +Example: `generated/` + +The directory where generated source code is placed. +Relative paths are resolved against the working directory. +==== + +.--generate-spring-boot +[%collapsible] +==== +Default: (not set) + +Flag that indicates to generate config classes for use with Spring Boot. +==== diff --git a/docs/modules/kotlin-binding/examples/KotlinConfigExample.kt b/docs/modules/kotlin-binding/examples/KotlinConfigExample.kt new file mode 100644 index 00000000..997c3ff4 --- /dev/null +++ b/docs/modules/kotlin-binding/examples/KotlinConfigExample.kt @@ -0,0 +1,33 @@ +@file:Suppress("UNUSED_VARIABLE") + +import org.pkl.config.java.ConfigEvaluator +import org.pkl.config.kotlin.forKotlin +import org.pkl.config.kotlin.to +import org.junit.jupiter.api.Test + +// the pkl/pkl-examples repo has a similar example +class KotlinConfigExample { + @Test + fun usage() { + // tag::usage[] + val evaluator = ConfigEvaluator.preconfigured().forKotlin() // <1> + val config = evaluator.use { // <2> + it.evaluateText("""pigeon { age = 5; diet = "Seeds" }""") + } + val pigeon = config["pigeon"] // <3> + val age = pigeon["age"].to() // <4> + val hobbies = pigeon["diet"].to>() // <5> + // end::usage[] + } + + @Test + fun nullable() { + // tag::nullable[] + val evaluator = ConfigEvaluator.preconfigured().forKotlin() + val config = evaluator.use { + it.evaluateText("name = null") // <1> + } + val name = config["name"].to() // <2> + // end::nullable[] + } +} diff --git a/docs/modules/kotlin-binding/pages/codegen.adoc b/docs/modules/kotlin-binding/pages/codegen.adoc new file mode 100644 index 00000000..7e23cdee --- /dev/null +++ b/docs/modules/kotlin-binding/pages/codegen.adoc @@ -0,0 +1,130 @@ += Kotlin Code Generator +include::ROOT:partial$component-attributes.adoc[] +:uri-pkl-codegen-kotlin-maven-module: {uri-maven-docsite}/artifact/org.pkl-lang/pkl-codegen-kotlin + +The Kotlin source code generator reads Pkl classes and generates corresponding Kotlin classes with equally named properties. + +Together with xref:java-binding:pkl-config-java.adoc#object-mapping[Object Mapping], code generation provides a complete solution for consuming Pkl configuration as statically typed Kotlin objects. +Kotlin code never drifts from the configuration structure defined in Pkl, and the entire configuration tree can be code-completed in Kotlin IDEs. + +== Installation + +The code generator is offered as Gradle plugin, Java library, and CLI. + +=== Gradle Plugin + +See xref:pkl-gradle:index.adoc#installation[Installation] in the Gradle plugin chapter. + +[[install-library]] +=== Java Library + +The `pkl-codegen-kotlin` library is available {uri-pkl-codegen-kotlin-maven-module}[from Maven Central]. +It requires Java 8 or higher and Kotlin 1.3 or higher. + +==== Gradle + +To use the library in a Gradle project, declare the following dependency: + +[tabs] +==== +Groovy:: ++ +.build.gradle +[source,groovy,subs="+attributes"] +---- +dependencies { + compile "org.pkl-lang:pkl-config-kotlin:{pkl-artifact-version}" +} + +ifndef::is-release-build[] +repositories { + maven { url "{uri-sonatype}" } +} +endif::[] +---- + +Kotlin:: ++ +.build.gradle.kts +[source,kotlin,subs="+attributes"] +---- +dependencies { + compile("org.pkl-lang:pkl-config-kotlin:{pkl-artifact-version}") +} + +ifndef::is-release-build[] +repositories { + maven { url = uri("{uri-sonatype}") } +} +endif::[] +---- +==== + +==== Maven + +To use the library in a Maven project, declare the following dependency: + +.pom.xml +[source,xml,subs="+attributes"] +---- + + org.pkl-lang + pkl-codegen-kotlin + {pkl-artifact-version} + +---- + +[[install-cli]] +=== CLI + +The CLI is bundled with the library. +As we do not currently ship the CLI as a self-contained Jar, we recommend to provision it with a Maven compatible build tool as shown in <>. + +[[usage]] +== Usage + +The code generator is offered as Gradle plugin, Java library, and CLI. + +=== Gradle Plugin + +See xref:pkl-gradle:index.adoc#kotlin-code-gen[Kotlin Code Generation] in the Gradle plugin chapter. + +=== Java Library + +The library offers two APIs: a high-level API that corresponds to the CLI, and a lower-level API that provides additional features and control. +The entry points for these APIs are `org.pkl.codegen.kotlin.CliKotlinCodeGenerator` and `org.pkl.codegen.kotlin.KotlinCodeGenerator`, respectively. +For more information, refer to the KDoc documentation. + +=== CLI + +As mentioned in <>, the CLI is bundled with the library. +To run the CLI, execute the library Jar or its `org.pkl.codegen.kotlin.Main` main class. + +*Synopsis:* `java -cp -jar pkl-codegen-kotlin.jar [] ` + +``:: +The absolute or relative URIs of the modules to generate classe for. +Relative URIs are resolved against the working directory. + +==== Options + +.--generate-kdoc +[%collapsible] +==== +Default: (flag not set) + +Flag that indicates to generate Kdoc based on doc comments for Pkl modules, classes, and properties. +==== + +Common code generator options: + +include::../../java-binding/partials/cli-codegen-options.adoc[] + +Common CLI options: + +include::../../pkl-cli/partials/cli-common-options.adoc[] + +[[full-example]] +== Full Example + +For a ready-to-go example with full source code, +see link:{uri-codegen-kotlin-example}[codegen-kotlin] in the _pkl/pkl-examples_ repository. diff --git a/docs/modules/kotlin-binding/pages/index.adoc b/docs/modules/kotlin-binding/pages/index.adoc new file mode 100644 index 00000000..4a902f58 --- /dev/null +++ b/docs/modules/kotlin-binding/pages/index.adoc @@ -0,0 +1,3 @@ += Integration with Kotlin + +Pkl provides rich integration with Kotlin. Our integration allows you to embed the Pkl runtime into your Kotlin application, and also provides code generation for from Pkl source code. diff --git a/docs/modules/kotlin-binding/pages/pkl-config-kotlin.adoc b/docs/modules/kotlin-binding/pages/pkl-config-kotlin.adoc new file mode 100644 index 00000000..29d404f6 --- /dev/null +++ b/docs/modules/kotlin-binding/pages/pkl-config-kotlin.adoc @@ -0,0 +1,117 @@ += pkl-config-kotlin Library +include::ROOT:partial$component-attributes.adoc[] +:uri-pkl-config-kotlin-maven-module: {uri-maven-docsite}/artifact/org.pkl-lang/pkl-config-kotlin +:uri-pkl-config-kotlin-main-sources: {uri-github-tree}/pkl-config-kotlin/src/main/kotlin/org/pkl/kotlin +:uri-pkl-config-kotlin-test-sources: {uri-github-tree}/pkl-config-kotlin/src/test/kotlin/org/pkl/kotlin +:uri-pkl-config-kotlin-ConverterFactories: {uri-pkl-config-kotlin-main-sources}/ConverterFactories.kt +:uri-pkl-config-kotlin-ConfigExtensions: {uri-pkl-config-kotlin-main-sources}/ConfigExtensions.kt + +The _pkl-config-kotlin_ library extends xref:java-binding:pkl-config-java.adoc[pkl-config-java] with Kotlin specific extension methods and object converters. +We recommend that Kotlin projects depend on this library instead of _pkl-config-java_. + +== Installation + +The _pkl-config-kotlin_ library is available {uri-pkl-config-kotlin-maven-module}[from Maven Central]. +It requires Java 11 or higher and Kotlin 1.5 or higher. + +=== Gradle + +To use the library in a Gradle project, declare the following dependency: + +[tabs] +==== +Groovy:: ++ +.build.gradle +[source,groovy,subs="+attributes"] +---- +dependencies { + compile "org.pkl-lang:pkl-config-kotlin:{pkl-artifact-version}" +} + +ifndef::is-release-build[] +repositories { + maven { url "{uri-sonatype}" } +} +endif::[] +---- + +Kotlin:: ++ +.build.gradle.kts +[source,kotlin,subs="+attributes"] +---- +dependencies { + compile("org.pkl-lang:pkl-config-kotlin:{pkl-artifact-version}") +} + +ifndef::is-release-build[] +repositories { + maven { url = uri("{uri-sonatype}") } +} +endif::[] +---- +==== + +=== Maven + +To use the library in a Maven project, declare the following dependency: + +.pom.xml +[source,xml,subs="+attributes"] +---- + + + org.pkl-lang + pkl-config-kotlin + {pkl-artifact-version} + +ifndef::is-release-build[] + + + sonatype-s01 + Sonatype S01 + {uri-sonatype} + + +endif::[] + +---- + +== Usage + +Below is the Kotlin version of the Java xref:java-binding:pkl-config-java.adoc#config-evaluator-java-example[ConfigEvaluator] example. +Differences to the Java version are called out. + +[source,kotlin,indent=0] +---- +include::{examplesdir}/KotlinConfigExample.kt[tags=usage] +---- +<1> Use the `forKotlin()` method to preconfigure the builder with Kotlin specific conversions. +In particular, `forKotlin()` eliminates the need to annotate constructor parameters of Kotlin classes and Kotlin data classes with `@Named`. +<2> The evaluator should be closed once it is no longer needed. +Here this is done with a Kotlin `use {}` expression. +Any data returned by the evaluator before calling `close()` remains valid. +<3> Navigate to the `"pigeon"` child. +The subscript notation is shorthand for `config.get("pigeon")`. +<4> Convert `"age"` to `Int` with the `Config.to()` extension method. +The target type is provided as a type argument. +Always use `Config.to()` instead of `Config.as()` in Kotlin. +<5> `Config.to()` makes conversions to parameterized types straightforward: +`to>()` instead of `as(JavaType.listOf(String::class.java))`. + +For properties that are allowed to be `null`, convert to a nullable type: + +[source,kotlin,indent=0] +---- +include::{examplesdir}/KotlinConfigExample.kt[tags=nullable] +---- +<1> To indicate that `null` is an allowed value, convert to the nullable type `String?`. +Converting to `String` would result in a `ConversionException`. + +For a ready-to-go example with full source code, +see link:{uri-config-kotlin-example}[config-kotlin] in the _pkl/pkl-examples_ repository. + +== Further Information + +Refer to the Javadoc and sources published with the library, or browse the library's {uri-pkl-config-kotlin-main-sources}[main] and {uri-pkl-config-kotlin-test-sources}[test] sources. diff --git a/docs/modules/language-reference/pages/index.adoc b/docs/modules/language-reference/pages/index.adoc new file mode 100644 index 00000000..2c3542a9 --- /dev/null +++ b/docs/modules/language-reference/pages/index.adoc @@ -0,0 +1,5325 @@ += Language Reference +include::ROOT:partial$component-attributes.adoc[] +:uri-common-mark: https://commonmark.org/ +:uri-newspeak: https://newspeaklanguage.org +:uri-antlr4: https://www.antlr.org +:uri-prototypical-inheritance: https://en.wikipedia.org/wiki/Prototype-based_programming +:uri-double-precision: https://en.wikipedia.org/wiki/Double-precision_floating-point_format +:uri-progressive-disclosure: https://en.wikipedia.org/wiki/Progressive_disclosure +:uri-javadoc-Pattern: https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html +:uri-github-PklLexer: {uri-github-tree}/pkl-core/src/main/antlr/PklLexer.g4 +:uri-github-PklParser: {uri-github-tree}/pkl-core/src/main/antlr/PklParser.g4 +:uri-stdlib-baseModule: {uri-pkl-stdlib-docs}/base +:uri-stdlib-jsonnetModule: {uri-pkl-stdlib-docs}/jsonnet +:uri-stdlib-reflectModule: {uri-pkl-stdlib-docs}/reflect +:uri-stdlib-xmlModule: {uri-pkl-stdlib-docs}/xml +:uri-stdlib-protobufModule: {uri-pkl-stdlib-docs}/protobuf +:uri-stdlib-Boolean: {uri-stdlib-baseModule}/Boolean +:uri-stdlib-xor: {uri-stdlib-baseModule}/Boolean#xor() +:uri-stdlib-implies: {uri-stdlib-baseModule}/Boolean#implies() +:uri-stdlib-Any: {uri-stdlib-baseModule}/Any +:uri-stdlib-String: {uri-stdlib-baseModule}/String +:uri-stdlib-Int: {uri-stdlib-baseModule}/Int +:uri-stdlib-Float: {uri-stdlib-baseModule}/Float +:uri-stdlib-Number: {uri-stdlib-baseModule}/Number +:uri-stdlib-NaN: {uri-stdlib-baseModule}/#NaN +:uri-stdlib-Infinity: {uri-stdlib-baseModule}/#Infinity +:uri-stdlib-isBetween: {uri-stdlib-baseModule}/Number#isBetween +:uri-stdlib-isFinite: {uri-stdlib-baseModule}/Number#isFinite +:uri-stdlib-Int8: {uri-stdlib-baseModule}/#Int8 +:uri-stdlib-Int16: {uri-stdlib-baseModule}/#Int16 +:uri-stdlib-Int32: {uri-stdlib-baseModule}/#Int32 +:uri-stdlib-UInt8: {uri-stdlib-baseModule}/#UInt8 +:uri-stdlib-UInt16: {uri-stdlib-baseModule}/#UInt16 +:uri-stdlib-UInt32: {uri-stdlib-baseModule}/#UInt32 +:uri-stdlib-UInt: {uri-stdlib-baseModule}/#UInt +:uri-stdlib-Uri: {uri-stdlib-baseModule}/#Uri +:uri-stdlib-matches: {uri-stdlib-baseModule}/String#matches() +:uri-stdlib-Null: {uri-stdlib-baseModule}/Null +:uri-stdlib-ifNonNull: {uri-stdlib-baseModule}/Null#ifNonNull() +:uri-stdlib-List: {uri-stdlib-baseModule}/List +:uri-stdlib-Set: {uri-stdlib-baseModule}/Set +:uri-stdlib-Map: {uri-stdlib-baseModule}/Map +:uri-stdlib-Listing: {uri-stdlib-baseModule}/Listing +:uri-stdlib-Listing-default: {uri-stdlib-baseModule}/Listing#default +:uri-stdlib-Listing-isDistinct: {uri-stdlib-baseModule}/Listing#isDistinct +:uri-stdlib-Listing-isDistinctBy: {uri-stdlib-baseModule}/Listing#isDistinctBy() +:uri-stdlib-Mapping: {uri-stdlib-baseModule}/Mapping +:uri-stdlib-Mapping-default: {uri-stdlib-baseModule}/Mapping#default +:uri-stdlib-Duration: {uri-stdlib-baseModule}/Duration +:uri-stdlib-Duration-value: {uri-stdlib-baseModule}/Duration#value +:uri-stdlib-Duration-unit: {uri-stdlib-baseModule}/Duration#unit +:uri-stdlib-DurationUnit: {uri-stdlib-baseModule}/#DurationUnit +:uri-stdlib-DataSize: {uri-stdlib-baseModule}/DataSize +:uri-stdlib-DataSize-value: {uri-stdlib-baseModule}/DataSize#value +:uri-stdlib-DataSize-unit: {uri-stdlib-baseModule}/DataSize#unit +:uri-stdlib-DataSizeUnit: {uri-stdlib-baseModule}/#DataSizeUnit +:uri-stdlib-Dynamic: {uri-stdlib-baseModule}/Dynamic +:uri-stdlib-Dynamic-toTyped: {uri-stdlib-baseModule}/Dynamic#toTyped() +:uri-stdlib-Typed: {uri-stdlib-baseModule}/Typed +:uri-stdlib-Regex: {uri-stdlib-baseModule}/Regex +:uri-stdlib-Regex-method: {uri-stdlib-baseModule}/#Regex() +:uri-stdlib-Regex-match: {uri-stdlib-baseModule}/Regex#match +:uri-stdlib-RegexMatch: {uri-stdlib-baseModule}/RegexMatch +:uri-stdlib-ValueRenderer: {uri-stdlib-baseModule}/ValueRenderer +:uri-stdlib-PcfRenderer-converters: {uri-stdlib-baseModule}/PcfRenderer#converters +:uri-stdlib-Function: {uri-stdlib-baseModule}/Function +:uri-stdlib-Function0: {uri-stdlib-baseModule}/Function0 +:uri-stdlib-Function1: {uri-stdlib-baseModule}/Function1 +:uri-stdlib-Function1-apply: {uri-stdlib-baseModule}/Function1#apply() +:uri-stdlib-Function2: {uri-stdlib-baseModule}/Function2 +:uri-stdlib-Function3: {uri-stdlib-baseModule}/Function3 +:uri-stdlib-Function4: {uri-stdlib-baseModule}/Function4 +:uri-stdlib-Function5: {uri-stdlib-baseModule}/Function5 +:uri-stdlib-Resource: {uri-stdlib-baseModule}/Resource +:uri-stdlib-outputFiles: {uri-stdlib-baseModule}/ModuleOutput#files +:uri-pkl-core-ModuleSchema: {uri-pkl-core-main-sources}/ModuleSchema.java +:uri-pkl-core-SecurityManager: {uri-pkl-core-main-sources}/SecurityManager.java +:uri-pkl-core-ResourceReader: {uri-pkl-core-main-sources}/resource/ResourceReader.java +:uri-pkl-core-ModuleKey: {uri-pkl-core-main-sources}/module/ModuleKey.java +:uri-pkl-core-PklException: {uri-pkl-core-main-sources}/PklException.java +:uri-value-converters: {uri-pkl-stdlib-docs}/base/PcfRenderer#converters +// TODO: double check this +:uri-pkl-go-resource-reader-docs: https://github.com/apple/pkl-go/blob/main/pkl/reader.go +:uri-pkl-swift-resource-reader-docs: https://github.com/apple/pkl-swift/blob/main/Sources/PklSwift/Reader.swift +:uri-glob-7: https://man7.org/linux/man-pages/man7/glob.7.html +:uri-unicode-identifier: https://unicode.org/reports/tr31/#R1-1 +:uri-semver: https://semver.org +:uri-mvs-build-list: https://research.swtch.com/vgo-mvs#algorithm_1 + +The language reference provides a comprehensive description of every Pkl language feature. + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +For a hands-on introduction, see xref:language-tutorial:index.adoc[Tutorial]. +For ready-to-go examples with full source code, see xref:ROOT:examples.adoc[]. +For API documentation, see xref:ROOT:standard-library.adoc[Standard Library]. + +[[comments]] +== Comments + +Pkl has three forms of comments: + +Line comment:: +A code comment that starts with a double-slash (`//`) and runs until the end of the line. ++ +[source%parsed,{pkl}] +---- +// Single-line comment +---- + +Block comment:: +A nestable multiline comment which is typically used to comment out code. +Starts with `+/*+` and ends with `+*/+`. ++ +[source%parsed,{pkl}] +---- +/* + Multiline + comment +*/ +---- + +Doc comment:: +A user-facing comment attached to a program member. +It starts with a triple-slash (`///`) and runs until the end of the line. +Doc comments on consecutive lines are merged. ++ +[source%parsed,{pkl}] +---- +/// A *bird* superstar. +/// Unfortunately, extinct. +dodo: Bird +---- + +Doc comments are processed by xref:pkl-doc:index.adoc[Pkldoc], Pkl's documentation generator. +For details on their syntax, see <>. + +[[numbers]] +== Numbers + +Pkl has two numeric types, link:{uri-stdlib-Int}[Int] and link:{uri-stdlib-Float}[Float]. +Their common supertype is link:{uri-stdlib-Number}[Number]. + +=== Integers + +A value of type link:{uri-stdlib-Int}[Int] is a 64-bit signed integer. + +Integer literals can be written in decimal, hexadecimal, binary, or octal notation: + +[source%tested,{pkl}] +---- +num1 = 123 +num2 = 0x012AFF // <1> +num3 = 0b00010111 // <2> +num4 = 0o755 // <3> +---- +<1> decimal: 76543 +<2> decimal: 23 +<3> decimal: 493 + +Integers can optionally include an underscore as a separator to improve readability. +An underscore does not affect the integer's value. + +[source%tested,{pkl}] +---- +num1 = 1_000_000 // <1> +num2 = 0x0134_64DE // <2> +num3 = 0b0001_0111 // <3> +num4 = 0o0134_6475 // <4> +---- +<1> Equivalent to `1000000` +<2> Equivalent to `0x013464DE` +<3> Equivalent to `0b00010111` +<4> Equivalent to `0o01346475` + +Negative integer literals start with a minus sign, as in `-123`. + +Integers support the standard comparison operators: + +[source%tested,{pkl}] +---- +comparison1 = 5 == 2 +comparison2 = 5 < 2 +comparison3 = 5 > 2 +comparison4 = 5 <= 2 +comparison5 = 5 >= 2 +---- + +Integers support the following arithmetic operators: + +[source%tested,{pkl}] +---- +num1 = 5 + 2 // <1> +num2 = 5 - 2 // <2> +num3 = 5 * 2 // <3> +num4 = 5 / 2 // <4> +num5 = 5 ~/ 2 // <5> +num6 = 5 % 2 // <6> +num7 = 5 ** 2 // <7> +---- +<1> addition (result: `7`) +<2> subtraction (result: `3`) +<3> multiplication (result: `10`) +<4> division (result: `2.5`, always `Float`) +<5> integer division (result: `2`, always `Int`) +<6> remainder (result: `1`) +<7> exponentiation (result: `25`) + +Arithmetic overflows are caught and result in an error. + +To restrict an integer's range, use one of the predefined <> +or an link:{uri-stdlib-isBetween}[isBetween] <>: + +[source,{pkl}] +---- +clientPort: UInt16 +serverPort: Int(isBetween(0, 1023)) +---- + +=== Floats + +A value of type link:{uri-stdlib-Float}[Float] is a 64-bit link:{uri-double-precision}[double-precision] floating point number. + +Float literals use decimal notation. +They consist of an integer part, decimal point, fractional part, and exponent part. +The integer and exponent part are optional. + +[source%tested,{pkl}] +---- +num1 = .23 +num2 = 1.23 +num3 = 1.23e2 // <1> +num4 = 1.23e-2 // <2> +---- +<1> result: 1.23 * 10^2^ +<2> result: 1.23 * 10^-2^ + +Negative float literals start with a minus sign, as in `-1.23`. + +The special float values _not a number_, _positive infinity_, and _negative infinity_ are written as: + +[source%tested,{pkl}] +---- +notANumber = NaN +positiveInfinity = Infinity +negativeInfinity = -Infinity +---- + +The link:{uri-stdlib-NaN}[NaN] and link:{uri-stdlib-Infinity}[Infinity] properties are defined in the standard library. + +Floats support the same comparison and arithmetic operators as integers. +Float literals with zero fractional part can be safely replaced with integer literals. +For example, it is safe to write `1.3 * 42` instead of `1.3 * 42.0`. + +Floats can also include the same underscore separater as integers. For example, `1_000.4_400` is a float whose value is equivalent to `1000.4400`. + +TIP: As integers are more convenient to use than floats with zero fractional part, we recommend to require `x: Number` instead of `x: Float` in type annotations. + +To restrict a float to a finite value, use the link:{uri-stdlib-isFinite}[isFinite] <>: + +[source,{pkl}] +---- +x: Float(isFinite) +---- + +To restrict a float's range, use the link:{uri-stdlib-isBetween}[isBetween] type constraint: + +[source,{pkl}] +---- +x: Float(isBetween(0, 10e6)) +---- + +[[booleans]] +== Booleans + +A value of type link:{uri-stdlib-Boolean}[Boolean] is either `true` or `false`. + +Apart from the standard logical operators, `Boolean` has +link:{uri-stdlib-xor}[xor] and link:{uri-stdlib-implies}[implies] methods. + +[source%tested,{pkl}] +---- +res1 = true && false // <1> +res2 = true || false // <2> +res3 = !false // <3> +res4 = true.xor(false) // <4> +res5 = true.implies(false) // <5> +---- +<1> logical conjunction (result: `false`) +<2> logical disjunction (result: `true`) +<3> logical negation (result: `true`) +<4> exclusive disjunction (result: `true`) +<5> logical implication (result: `false`) + +[[strings]] +== Strings + +A value of type link:{uri-stdlib-String}[String] is a sequence of Unicode code points. + +String literals are enclosed in double quotes: + +[source%tested,{pkl-expr}] +---- +"Hello, World!" +---- + +TIP: Except for a few minor differences footnote:[Pkl's string literals have fewer character escape sequences, +have stricter rules for line indentation in multiline strings, and do not have a line continuation character.], +String literals have the same syntax and semantics as in Swift 5. Learn one of them, know both of them! + +Inside a string literal, the following character escape sequences have special meaning: + +* `\t` - tab +* `\n` - line feed +* `\r` - carriage return +* `\"` - verbatim quote +* `\\` - verbatim backslash + +Unicode escape sequences have the form `\u{}`, where `` is a hexadecimal number between 0 and 10FFFF: + +[source%tested,{pkl-expr}] +---- +"\u{26} \u{E9} \u{1F600}" // <1> +---- +<1> result: `"& é 😀"` + +To concatenate strings, use the `+` (plus) operator, as in `"abc" + "def" + "ghi"`. + +=== String Interpolation + +To embed the result of expression `` in a string, use `\()`: + +[source%tested,{pkl}] +---- +name = "Dodo" +greeting = "Hi, \(name)!" // <1> +---- +<1> result: `"Hi, Dodo!"` + +Before a result is inserted, it is converted to a string: + +[source%tested,{pkl}] +---- +x = 42 +str = "\(x + 2) plus \(x * 2) is \(0x80)" // <1> +---- +<1> result: `"44 plus 84 is 128"` + +=== Multiline Strings + +To write a string that spans multiple lines, use a multiline string literal: + +[source%tested,{pkl-expr}] +---- +""" +Although the Dodo is extinct, +the species will be remembered. +""" +---- + +Multiline string literals are delimited by three double quotes (`"""`). +String content and closing delimiter must each start on a new line. + +The content of a multiline string starts on the first line after the opening quotes and ends on the last line before the closing quotes. +Line breaks are included in the string and normalized to `\n`. + +The previous multiline string is equivalent to this single line string. +Notice that there is no leading or trailing whitespace. + +[source%tested,{pkl-expr}] +---- +"Although the Dodo is extinct,\nthe species will be remembered." +---- + +String interpolation, character escape sequences, and Unicode escape sequences work the same as for single line strings: + +[source%tested,{pkl}] +---- +bird = "Dodo" +message = """ +Although the \(bird) is extinct, +the species will be remembered. +""" +---- + +Each content line must begin with the same whitespace characters as the line containing the closing delimiter, +which are not included in the string. Any further leading whitespace characters are preserved. +In other words, line indentation is controlled by indenting lines relative to the closing delimiter. + +In the following string, lines have no leading whitespace: + +[source%tested,{pkl}] +---- +str = """ + Although the Dodo + is extinct, + the species + will be remembered. + """ +---- + +In the following string, lines are indented between three and five spaces: + +[source%tested,{pkl}] +---- +str = """ + Although the Dodo + is extinct, + the species + will be remembered. + """ +---- + +[[custom-string-delimiters]] +=== Custom String Delimiters + +Some strings contain many verbatim backslash (`\`) or quote (`"`) characters. +A good example is regular expressions, which make frequent use of backslash characters for their own escaping. +In such cases, using the escape sequences `\\` and `\"` quickly becomes tedious and hampers readability. + +Instead, leading/closing string delimiters can be customized to start/end with a pound sign (`\#`). +This also affects the escape character, which changes from `\` to `\#`. + +All backslash and quote characters in the following string are interpreted verbatim: + +[source%tested,{pkl-expr}] +---- +#" \\\\\ """"" "# +---- + +Escape sequences and string interpolation still work, and now start with `\#`: + +[source%tested,{pkl}] +---- +bird = "Dodo" +str = #" \\\\\ \#n \#u{12AF} \#(bird) """"" "# +---- + +More generally, string delimiters and escape character can be customized to contain _n_ pound signs each, for n >= 1. + +In the following string, _n_ is 2. As a result, the string content is interpreted verbatim: + +[source%tested,{pkl-expr}] +---- +##" \\\\\ \#\#\# """"" "## +---- + +=== String API + +The `String` class offers a link:{uri-stdlib-String}[rich API]. +Here are just a few examples: + +[source%tested,{pkl}] +---- +strLength = "dodo".length // <1> +reversedStr = "dodo".reverse() // <2> +hasAx = "dodo".contains("alive") // <3> +trimmed = " dodo ".trim() // <4> +---- +<1> result: `4` +<2> result: `"odod"` +<3> result: `false` +<4> result: `"dodo"` + +[[durations]] +== Durations + +A value of type link:{uri-stdlib-Duration}[Duration] has a _value_ component of type `Number` and a _unit_ component of type `String`. +The unit component is constrained to the units defined in link:{uri-stdlib-DurationUnit}[DurationUnit]. + +Durations are constructed with the following `Number` properties: + +[source%tested,{pkl}] +---- +duration1 = 5.ns // nanoseconds (smallest unit) +duration2 = 5.us // microseconds +duration3 = 5.ms // milliseconds +duration4 = 5.s // seconds +duration5 = 5.min // minutes +duration6 = 5.h // hours +duration7 = 3.d // days (largest unit) +---- + +A duration can be negative, as in `-5.min`. +It can have a floating point value, as in `5.13.min`. +The link:{uri-stdlib-Duration-value}[value] and link:{uri-stdlib-Duration-unit}[unit] properties provide access to the duration's components. + +Durations support the standard comparison operators: + +[source%tested,{pkl}] +---- +comparison1 = 5.min == 3.s +comparison2 = 5.min < 3.s +comparison3 = 5.min > 3.s +comparison4 = 5.min <= 3.s +comparison5 = 5.min >= 3.s +---- + +Durations support the same arithmetic operators as numbers: + +[source%tested,{pkl}] +---- +res1 = 5.min + 3.s // <1> +res2 = 5.min - 3.s // <2> +res3 = 5.min * 3 // <3> +res4 = 5.min / 3 // <4> +res5 = 5.min / 3.min // <5> +res6 = 5.min ~/ 3 // <6> +res7 = 5.min ~/ 3.min // <7> +res8 = 5.min % 3 // <8> +res9 = 5.min ** 3 // <9> +---- +<1> result: `5.05.min` +<2> result: `4.95.min` +<3> result: `15.min` +<4> result: `1.6666666666666667.min` +<5> result: `1.6666666666666667` +<6> result: `1.min` +<7> result: `1` +<8> result: `2.min` +<9> result: `125.min` + +The value component can be an expression: + +[source%tested,{pkl}] +---- +x = 5 +xMinutes = x.min // <1> +y = 3 +xySeconds = (x + y).s // <2> +---- +<1> result: `5.min` +<2> result: `8.s` + +[[data-sizes]] +== Data Sizes + +A value of type link:{uri-stdlib-DataSize}[DataSize] has a _value_ component of type `Number` and a _unit_ component of type `String`. +The unit component is constrained to the units defined in link:{uri-stdlib-DataSizeUnit}[DataSizeUnit]. + +Data sizes with decimal unit (factor 1000) are constructed with the following `Number` properties: + +[source%tested,{pkl}] +---- +datasize1 = 5.b // bytes (smallest unit) +datasize2 = 5.kb // kilobytes +datasize3 = 5.mb // megabytes +datasize4 = 5.gb // gigabytes +datasize5 = 5.tb // terabytes +datasize6 = 5.pb // petabytes (largest unit) +---- + +Data sizes with binary unit (factor 1024) are constructed with the following `Number` properties: + +[source%tested,{pkl}] +---- +datasize1 = 5.b // bytes (smallest unit) +datasize2 = 5.kib // kibibytes +datasize3 = 5.mib // mebibytes +datasize4 = 5.gib // gibibytes +datasize5 = 5.tib // tebibytes +datasize6 = 5.pib // pebibytes (largest unit) +---- + +A data size can be negative, as in `-5.mb`. +It can have a floating point value, as in `5.13.mb`. +The link:{uri-stdlib-DataSize-value}[value] and link:{uri-stdlib-DataSize-unit}[unit] properties provide access to the data size's components. + +Data sizes support the standard comparison operators: + +[source%tested,{pkl}] +---- +comparison1 = 5.mb == 3.kib +comparison2 = 5.mb < 3.kib +comparison3 = 5.mb > 3.kib +comparison4 = 5.mb <= 3.kib +comparison5 = 5.mb >= 3.kib +---- + +Data sizes support the same arithmetic operators as numbers: + +[source%tested,{pkl}] +---- +res1 = 5.mb + 3.kib // <1> +res2 = 5.mb - 3.kib // <2> +res3 = 5.mb * 3 // <3> +res4 = 5.mb / 3 // <4> +res5 = 5.mb / 3.mb // <5> +res6 = 5.mb ~/ 3 // <6> +res7 = 5.mb ~/ 3.mb // <7> +res8 = 5.mb % 3 // <8> +res9 = 5.mb ** 3 // <9> +---- +<1> result: `5.003072.mb` +<2> result: `4.996928.mb` +<3> result: `15.mb` +<4> result: `1.6666666666666667.mb` +<5> result: `1.6666666666666667` +<6> result: `1.mb` +<7> result: `1` +<8> result: `2.mb` +<9> result: `125.mb` + +The value component can be an expression: + +[source%tested,{pkl}] +---- +x = 5 +xMegabytes = x.mb // <1> +y = 3 +xyKibibytes = (x + y).kib // <2> +---- +<1> result: `5.mb` +<2> result: `8.kib` + +[[objects]] +== Objects + +An object is an ordered collection of _values_ indexed by _name_. + +An object's key–value pairs are called its _properties_. +Property values are lazily evaluated on the first read. + +Because Pkl's objects differ in important ways from objects in general-purpose programming languages, +and because they are the backbone of most data models, understanding objects is the key to understanding Pkl. + +[[defining-objects]] +=== Defining Objects + +Let's define an object with properties `name` and `extinct`: + +[source%tested,{pkl}] +---- +dodo { // <1> + name = "Dodo" // <2> + extinct = true // <3> +} // <4> +---- +<1> Defines a module property named `dodo`. + The open curly brace (`{`) indicates that the value of this property is an object. +<2> Defines an object property named `name` with string value `"Dodo"`. +<3> Defines an object property named `extinct` with boolean value `true`. +<4> The closing curly brace indicates the end of the object definition. + +To access an object property by name, use dot (`.`) notation: + +[source%tested,{pkl}] +---- +dodoName = dodo.name +dodoIsExtinct = dodo.extinct +---- + +Objects can be nested: + +[source%tested,{pkl}] +---- +dodo { + name = "Dodo" + taxonomy { // <1> + `class` = "Aves" // <2> + } +} +---- +<1> Defines an object property named `taxonomy`. + The open curly brace indicates that its value is another object. +<2> The word `class` is a keyword of Pkl, and needs to be wrapped in backticks (```) to be used as a property. + +As you probably guessed, the nested property `class` can be accessed with `dodo.taxonomy.class`. + +Like all values, objects are _immutable_, which is just a fancy (and short!) way to say that their properties never change. +So what happens when Pigeon moves to a different street? Do we have to construct a new object from scratch? + +[[amending-objects]] +=== Amending Objects + +Fortunately we don't have to. +An object can be _amended_ to form a new object that only differs in selected properties. +Here is how this looks: + +[source%parsed,{pkl}] +---- +tortoise = (dodo) { // <1> + name = "Galápagos tortoise" + taxonomy { // <2> + `class` = "Reptilia" // <3> + } +} +---- +<1> Defines a module property named `tortoise`. + Its value is an object that _amends_ `dodo`. + Note that the amended object must be enclosed in parentheses. +<2> Object property `tortoise.taxonomy` _amends_ `dodo.taxonomy`. +<3> Object property `tortoise.taxonomy.class` _overrides_ `dodo.taxonomy.class`. + +As you can see, it is easy to construct a new object that overrides selected properties of an existing object, +even if, as in our example, the overridden property is nested inside another object. + +NOTE: If this way of constructing new objects from existing objects reminds you of prototypical inheritance, you are spot-on: +Pkl objects use protoypical inheritance as known from languages such as JavaScript. +But unlike in JavaScript, their prototype chain cannot be directly accessed or even be modified. +Another difference is that in Pkl, object properties are late-bound. Read on to see what this means. + +[[amends-declaration]] +[NOTE] +.Amends expressions vs. amends declarations +==== + +The <> and <> sections cover two notations that are both a form of amending; called an _amends declaration_ and an _amends expression_, respectively. + +[source%tested,{pkl}] +---- +pigeon { // <1> + name = "Turtle dove" + extinct = false +} + +parrot = (pigeon) { // <2> + name = "Parrot" +} +---- +<1> Amends expression. +<2> Amends declaration. + +An amends declaration amends the property of the same name in a module's parent module, if the parent property exists. +Otherwise, an amends declaration implicitly amends {uri-stdlib-Dynamic}[Dynamic]. + +Another way to think about an amends declaration is that it is shorthand for assignment. +In practical terms, `pigeon {}` is the same as `pigeon = super.pigeon {}`. + +Amending object bodies can be chained for both an amends expression and an amends declaration. + +[source%tested,{pkl}] +---- +pigeon { + name = "Common wood pigeon" +} { + extinct = false +} // <1> + +dodo = (pigeon) { + name = "Dodo" +} { + extinct = true +} // <2> +---- +<1> Chained amends expression (`pigeon { ... } { ... }` is the amends expression). +<2> Chained amends declaration. +==== + +[[late-binding]] +=== Late Binding + +Let's move on to Pkl's secret sauce: +the ability to define an object property's value in terms of another property's value, and the resulting _late binding_ effect. +Here is an example: + +[source%tested,{pkl}] +---- +penguin { + eggIncubation = 40.d + adultWeightInGrams = eggIncubation.value * 100 // <1> +} +adultWeightInGrams = penguin.adultWeightInGrams +---- +<1> result: `4000` + +We have defined a hypothetical `penguin` object whose `adultWeightInGrams` property is defined in terms of the `eggIncubation` duration. +Can you guess what happens when `penguin` is amended and its `eggIncubation` overridden? + +[source%tested,{pkl}] +---- +madeUpBird = (penguin) { + eggIncubation = 11.d +} +adultWeightInGrams = madeUpBird.adultWeightInGrams // <1> +---- +<1> result: `1100` + +As you can see, ``madeUpBird``'s `adultWeightInGrams` changed along with its `eggIncubation`. +This is what we mean when we say that object properties are _late-bound_. + +[NOTE] +.Spreadsheet Programming +==== +A good analogy is that object properties behave like spreadsheet cells. +When they are linked, changes to "downstream" properties automatically propagate to "upstream" properties. +The main difference is, editing a spreadsheet cell changes the state of the spreadsheet, +whereas "editing" a property results in a new object, leaving the original object untouched. +It is as if you made a copy of the entire spreadsheet whenever you edited a cell! +==== + +Late binding of properties is an incredibly useful feature for a configuration language. +It is used extensively in Pkl code (especially in templates), and is the key to understanding how Pkl works. + +=== Transforming Objects + +Say we have the following object: + +[source%tested,{pkl}] +---- +dodo { + name = "Dodo" + extinct = true +} +---- + +How can property `name` be removed? + +The recipe for transforming an object is: + +. Convert the object to a map. +. Transform the map using ``Map``'s link:{uri-stdlib-Map}[rich API]. +. If necessary, convert the map back to an object. + +Equipped with this knowledge, let's try to accomplish our objective: + +[source%tested,{pkl-expr}] +---- +dodo + .toMap() + .remove("name") + .toDynamic() +---- + +The resulting dynamic object is equivalent to `dodo`, except that it no longer has a `name` property. + +[IMPORTANT] +.Lazy vs. Eager Data Types +==== +Converting an object to a map is a transitition from a _lazy_ to an _eager_ data type. +All of the object's properties are evaluated and all references between them are resolved. + +If the map is later converted back to an object, subsequent changes to the object's properties no longer propagate to (previously) dependent properties. +To make these boundaries clear, transitioning between _lazy_ and _eager_ data types always requires an explicit method call, such as `toMap()` or `toDynamic()`. +==== + +[[typed-objects]] +=== Typed Objects + +:fn-typed-objects: footnote:[By "structure" we mean a list of property names and (optionally) property types.] + +Pkl has two kinds of objects: + +* A link:{uri-stdlib-Dynamic}[Dynamic] object has no predefined structure.{fn-typed-objects} + When a dynamic object is amended, not only can existing properties be overridden or amended, but new properties can also be added. + So far, we have only used dynamic objects in this chapter. +* A link:{uri-stdlib-Typed}[Typed] object has a fixed structure described by a class definition. + When a typed object is amended, its properties can be overridden or amended, but new properties cannot be added. + In other words, the new object has the same class as the original object. + +[TIP] +.When to Use Typed vs. Dynamic Objects +==== +* Use typed objects to build schema-backed data models that are validatedfootnote:[By "Use typed objects" we mean to define classes and build data models out of instances of these classes.]. + This is what most templates do. +* Use dynamic objects to build schema-less data models that are not validated. + Dynamic objects are useful for ad-hoc tasks, tasks that do not justify the effort of writing and maintaining a schema, and for representing data whose structure is unknown. +==== + +Note that every <> is a typed object. Its properties implicitly define a class, +and new properties cannot be added when amending the module. + +A typed object is backed by a _class_. +Let's look at an example: + +[source%tested,{pkl}] +---- +class Bird { // <1> + name: String + lifespan: Int + migratory: Boolean +} + +pigeon = new Bird { // <2> + name = "Pigeon" + lifespan = 8 + migratory = false +} +---- +<1> Defines a class named `Bird` with properties `name`, `lifespan` and `migratory`. +<2> Defines a module property named `pigeon`. + Its value is a typed object constructed by instantiating class `Bird`. + A type only needs to be stated when the property does not have or inherit a <>. + Otherwise, amend syntax (`pigeon { ... }`) or shorthand instantiation syntax (`pigeon = new { ... }`) should be used. + +Congratulations, you have constructed your first typed objectfootnote:[Not counting that every module is a typed object.]! +How does it differ from a dynamic object? + +The answer is that a typed object has a fixed structure prescribed by its class, which cannot be changed when amending the object: + +[source%tested%error,{pkl}] +---- +class Bird { // <1> + name: String + lifespan: Int +} + +faultyPigeon = new Bird { + name = "Pigeon" + lifespan = 8 + hobby = "singing" +} +---- + +Evaluating this, gives: + +[source,shell,subs="quotes"] +---- +Cannot find property *hobby* in object of type *repl#Bird*. + +Available properties: +lifespan +name +---- + +Class structure is also enforced when instantiating a class. +Let's try to override property `name` with a value of the wrong type: + +[source%tested%error,{pkl}] +---- +faultyPigeon2 = new Bird { + name = 3.min + lifespan = 8 +} +---- + +Evaluating this, also fails: + +[source,shell,subs="quotes"] +---- +Expected value of type *String*, but got type *Duration*. +Value: 3.min +---- + +Typed objects are the fundamental building block for constructing validated data models in Pkl. +To dive deeper into this topic, continue with <>. + +[NOTE] +.Converting untyped objects to typed objects +==== +When you have a `Dynamic` that has all the properties (with the right types and meeting all constraints), you can convert it to a `Typed` by using link:{uri-stdlib-Dynamic-toTyped}[`toTyped()`]: + +[source,{pkl}] +---- +class Bird { + name: String + lifespan: Int +} + +pigeon = new Dynamic { // <1> + name = "Pigeon" + lifespan = 8 +}.toTyped(Bird) // <2> +---- +<1> Instead of a `new Bird`, `pigeon` can be defined with a `new Dynamic`. +<2> That `Dynamic` is then converted to a `Bird`. +==== + +[[properties]] +=== Property Modifiers + +==== Hidden Properties + +A property with modifier `hidden` is omitted from rendered output and from object conversions. + +[source,{pkl}] +---- +class Bird { + name: String + lifespan: Int + hidden nameAndLifespanInIndex = "\(name), \(lifespan)" // <1> + nameSignWidth: UInt = nameAndLifespanInIndex.length // <2> +} + +pigeon = new Bird { // <3> + name = "Pigeon" +} + +pigeonInIndex = pigeon.nameAndLifespanInIndex // <4> + +pigeonDynamic = pigeon.toDynamic() // <5> +---- +<1> Properties defined as `hidden` are accessible on any `Bird` instance, but not output by default. +<2> Non-hidden properties can refer to hidden properties as usual. +<3> `pigeon` is an object with _four_ properties, but is rendered with _three_ properties. +<4> Accessing a `hidden` property from outside the class and object is like any other property. +<5> Object conversions omit hidden properties, so the resulting `Dynamic` has three properties. + +Invoking Pkl on this file produces the following result. + +[source,{pkl}] +---- +pigeon { + name = "Pigeon" + lifespan = 8 + nameSignWidth = 9 +} +pigeonInIndex = "Pigeon, 5" +pigeonDynamic { + name = "Pigeon" + lifespan = 8 + nameSignWidth = 9 +} +---- + +==== Local properties +A property with modifier `local` can only be referenced in the lexical scope of its definition. + +[source,{pkl}] +---- +class Bird { + name: String + lifespan: Int + local separator = "," // <1> + hidden nameAndLifespanInIndex = "\(name)\(separator) \(lifespan)" // <2> +} + +pigeon = new Bird { + name = "Pigeon" + lifespan = 8 +} + +pigeonInIndex = pigeon.nameAndLifespanInIndex // <3> +pigeonSeparator = pigeon.separator // Error <4> +---- +<1> This property can only be accessed from inside this _class definition_. +<2> Non-local properties can refer to the local property as usual. +<3> The _value_ of `separator` occurs in `nameAndLifespanInIndex`. +<4> Pkl does not accept this, as there is no property `separator` on a `Bird` instance. + +Because a `local` property is added to the lexical scope, but not (observably) to the object, you can add `local` properties to ``Listing``s and ``Mapping``s. + +[NOTE] +.Import clauses define local properties +==== + +An _import clause_ defines a local property in the containing module. +This means `import "someModule.pkl"` is equivalent to `local someModule = import("someModule.pkl")`. +Also, `import "someModule.pkl" as otherName` is equivalent to `local otherName = import("someModule.pkl")`. +==== + +[[fixed-properties]] +==== Fixed properties + +A property with the `fixed` modifier cannot be assigned to or amended when defining an object of its class. + +.Bird.pkl +[source%tested,{pkl}] +---- +fixed laysEggs: Boolean = true + +fixed birds: Listing = new { + "Pigeon" + "Hawk" + "Penguin" +} +---- + +When amending, assigning to a `fixed` property is an error. +Similarly, it is an error to use an <> on a fixed property: + +.invalid.pkl +[source%tested,{pkl}] +---- +amends "Bird.pkl" + +laysEggs = false // <1> + +birds { // <2> + "Giraffe" +} +---- +<1> Error: cannot assign to fixed property `laysEggs` +<2> Error: cannot amend fixed property `birds` + +When extending a class and overriding an existing property definition, the fixed-ness of the overridden property must be preserved. +If the property in the parent class is declared `fixed`, the child property must also be declared `fixed`. +If the property in the parent class is not declared `fixed`, the child property may not add the `fixed` modifier. + +[source%parsed,{pkl}] +---- +abstract class Bird { + fixed canFly: Boolean + name: String +} + +class Penguin extends Bird { + canFly = false // <1> + fixed name = "Penguin" // <2> +} +---- +<1> Error: missing modifier `fixed`. +<2> Error: modifier `fixed` cannot be applied to property `name`. + +The `fixed` modifier is useful for defining properties that are meant to be derived from other properties. +In the following snippet, property `result` is not meant to be assigned to, because it is derived +from other properties. + +[source%parsed,{pkl}] +---- +class Bird { + wingspan: Int + weight: Int + fixed wingspanWeightRatio: Int = wingspan / weight +} +---- + +Another use-case for `fixed` is to define properties that are meant to be fixed to a class definition. +In the example below, the `species` of a bird is tied to the class, and therefore is declared `fixed`. + +Note that it is possible to define a `fixed` property without a value, for one of two reasons: + +1. The type has a default value that makes an explicit default redundant. +2. The property is meant to be overridden by a child class. + +[source%tested,{pkl}] +---- +abstract class Bird { + fixed species: String // <1> +} + +class Osprey extends Bird { + fixed species: "Pandion haliaetus" // <2> +} +---- +<1> No explicit default because the property is overridden by a child class. +<2> Overrides the type from `String` to the <> `"Pandion haliaetus"`. + +Assigning an explicit default would be redundant, therefore it is omitted. + +==== Const properties + +A property with the `const` modifier behaves like the <> modifier, +with the additional rule that it cannot reference non-const properties or methods. + +.Bird.pkl +[source%tested,{pkl}] +---- +const laysEggs: Boolean = true + +const examples: Listing = new { + "Pigeon" + "Hawk" + "Penguin" +} +---- + +Referencing any non-const property or method is an error. + +.invalid.pkl +[source%parsed,{pkl}] +---- +pigeonName: String = "Pigeon" + +const function birdLifespan(i: Int): Int = (i / 4).toInt() + +class Bird { + name: String + lifespan: Int +} + +const bird: Bird = new { + name = pigeonName // <1> + lifespan = birdLifespan(24) // <2> +} +---- +<1> Error: cannot reference non-const property `pigeonName` from a const property. +<2> Allowed: `birdLifespan` is const. + +It is okay to reference another value _within_ the same const property. + +.valid.pkl +[source%tested,{pkl}] +---- +class Bird { + lifespan: Int + description: String + speciesName: "Bird" +} + +const bird: Bird = new { + lifespan = 8 + description = "species: \(speciesName), lifespan: \(lifespan)" // <1> +} +---- +<1> `lifespan` is declared within property `bird`. `speciesName` resolves to `this.speciesName`, where `this` is a value within property `bird`. + +NOTE: Because `const` members can only reference its own values, or other `const` members, they are not <>. + +The `const` modifier implies that it is also <>. +Therefore, the same rules that apply to `fixed` also apply to `const`: + +* A `const` property cannot be assigned to or amended when defining an object of its class. +* The const-ness of a property or method must be preserved when it is overridden by a child class. + +[[class-and-annotation-const]] +*Class and Annotation Scoping* + +Within a class or annotation body, any reference to a property or method of its enclosing module requires that the referenced member is `const`. + +.invalid2.pkl +[source%parsed,{pkl}] +---- +pigeonName: String = "Pigeon" + +class Bird { + name: String = pigeonName // <1> +} + +@Deprecated { message = "Replace with \(pigeonName)" } // <2> +oldPigeonName: String +---- +<1> Error: cannot reference non-const property `pigeonName` from a class. +<2> Error: cannot reference non-const property `pigeonName` from an annotation. + +This rule exists because classes and annotations are not <>; +it is not possible to change the definition of a class nor annotation by amending the module +where it is defined. + +Generally, there are two strategies for referencing a property from a class or annotation: + +*Add the `const` modifier to the referenced property* + +One solution is to add the `const` modifier to the property being referenced. + +.Birds.pkl +[source,diff] +---- +-pigeonName: String = "Pigeon" ++const pigeonName: String = "Pigeon" + + class Bird { + name: String = pigeonName + } +---- + +This solution makes sense if `pigeonName` does get assigned/amended when amending module `Birds.pkl` (modules are regular objects that can be amended). + +*Self-import the module* + +.Birds.pkl +[source,diff] +---- ++import "Birds.pkl" // <1> ++ + pigeonName: String = "Pigeon" + + class Bird { +- name: String = pigeonName ++ name: String = Birds.pigeonName + } +---- +<1> module `Birds` imports itself + +This solution works because an import clause implicitly defines a `const local` property and amending this module does not affect a self-import. + +This makes sense if property `pigeonName` *does* get assigned/amended when amending module `Birds.pkl`. + +[[listings]] +== Listings + +A value of type link:{uri-stdlib-Listing}[Listing] is an ordered, indexed collection of _elements_. + +A listing's elements have zero-based indexes and are lazily evaluated on the first read. + +Listings combine qualities of lists and objects: + +* Like lists, listings can contain arbitrary elements. +* Like objects, listings excel at defining and amending nested literal data structures. +* Like objects, listings can only be directly manipulated through amendment, + but converting them to a list (and, if necessary, back to a listing) opens the door to arbitrary transformations. +* Like object properties, listing elements are evaluated lazily, can be defined in terms of each other, and are late-bound. + +[TIP] +.When to use Listing vs. <> +==== +* When a collection of elements needs to be specified literally, use a listing. +* When a collection of elements needs to be transformed in a way that cannot be achieved by <> a listing, use a list. +* If in doubt, use a listing. + +Templates and schemas should almost always use listings instead of lists. +Note that listings can be converted to lists when the need arises. +==== + +=== Defining Listings + +Listings have a literal syntax that is similar to that of objects. +Here is a listing with two elements: + +[source%tested,{pkl}] +---- +birds = new Listing { // <1> + new { // <2> + name = "Pigeon" + diet = "Seed" + } + new { // <3> + name = "Parrot" + diet = "Berries" + } +} +---- +<1> Defines a module property named `birds` with a value of type `Listing`. + A type only needs to be stated when the property does not have or inherit a <>. + Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used. +<2> Defines a listing element of type `Dynamic`. +<3> Defines another listing element of type `Dynamic`. + The order of definitions is relevant. + +To access an element by index, use the `[]` (subscript) operator: + +[source%tested,{pkl}] +---- +firstBirdName = birds[0].name // <1> +secondBirdDiet = birds[1].diet // <2> +---- +<1> result: `"Pigeon"` +<2> result: `"Berries"` + +Listings can contain arbitrary types of elements: + +[source%tested,{pkl}] +---- +listing = new Listing { + "Pigeon" // <1> + 3.min // <2> + new Listing { // <3> + "Barn owl" + } +} +---- +<1> Defines a listing element of type `String`. +<2> Defines a listing element of type `Duration`. +<3> Defines a listing element of type `Listing`. + +Listings can have `local` properties: + +[source%tested,{pkl}] +---- +listing = new Listing { + local pigeon = "Pigeon" // <1> + pigeon // <2> + "A " + pigeon + " is a bird" // <3> +} +---- +<1> Defines a local property with value `"Pigeon"`. + Local properties can have a type annotation, as in `pigeon: String = "Pigeon"`. +<2> Defines a listing element that references the local property. +<3> Defines another listing element that references the local property. + +[[amending-listings]] +=== Amending Listings + +Let's say we have the following listing: + +[source%tested,{pkl}] +---- +birds = new Listing { + new { + name = "Pigeon" + diet = "Seeds" + } + new { + name = "Parrot" + diet = "Berries" + } +} +---- + +To add, override, or amend elements of this listing, amend the listing itself: + +[source%tested,{pkl}] +---- +birds2 = (birds) { // <1> + new { // <2> + name = "Barn owl" + diet = "Mice" + } + [0] { // <3> + diet = "Worms" + } + [1] = new { // <4> + name = "Albatross" + diet = "Fish" + } +} +---- +<1> Defines a module property named `birds2`. Its value is a listing that amends `birds`. +<2> Defines a listing element of type `Dynamic`. +<3> Amends the listing element at index 0 (whose name is `"Pigeon"`) and overrides property `diet`. +<4> Overrides the listing element at index 1 (whose name is `"Parrot"`) with an entirely new dynamic object. + +=== Late Binding + +A listing element can be defined in terms of another element. +To reference the element at index ``, use `this[]`: + +[source%tested,{pkl}] +---- +birds = new Listing { + new { // <1> + name = "Pigeon" + diet = "Seeds" + } + (this[0]) { // <2> + name = "Parrot" + } +} +---- +<1> Defines a listing element of type `Dynamic`. +<2> Defines a listing element that amends the element at index 0 and overrides `name`. + +Listing elements are late-bound: + +[source%tested,{pkl}] +---- +newBirds = (birds) { // <1> + [0] { + diet = "Worms" + } +} + +secondBirdDiet = newBirds[1].diet // <2> +---- +<1> Amends listing `birds` and overrides property `diet` of element 0 (whose name is "Pigeon"`) to have value `"Worms"`. +<2> Because element 1 is defined in terms of element 0, its `diet` property also changes to `"Worms"`. + +=== Transforming Listings + +Say we have the following listing: + +[source%tested,{pkl}] +---- +birds = new Listing { + new { + name = "Pigeon" + diet = "Seeds" + } + new { + name = "Parrot" + diet = "Berries" + } +} +---- + +How can the order of elements be reversed programmatically? + +The recipe for transforming a listing is: + +. Convert the listing to a list. +. Transform the list using ``List``'s link:{uri-stdlib-List}[rich API]. +. If necessary, convert the list back to a listing. + +TIP: Often, transformations happen in a link:{uri-stdlib-PcfRenderer-converters}[converter] of a link:{uri-stdlib-ValueRenderer}[value renderer]. +Because most value renderers treat lists the same as listings, it is often not necessary to convert back to a listing. + +Equipped with this knowledge, let's try to accomplish our objective: + +[source%tested,{pkl}] +---- +reversedbirds = birds + .toList() + .reverse() + .toListing() +---- + +`result` now contains the same elements as `birds`, but in reverse order. + +[IMPORTANT] +.Lazy vs. Eager Data Types +==== +Converting a listing to a list is a transitition from a _lazy_ to an _eager_ data type. +All of the listing's elements are evaluated and all references between them are resolved. + +If the list is later converted back to a listing, subsequent changes to the listing's elements no longer propagate to (previously) dependent elements. +To make these boundaries clear, transitioning between _lazy_ and _eager_ data types always requires an explicit method call, such as `toList()` or `toListing()`. +==== + +=== Default Element + +Listings can have a _default element_: + +[source%tested,{pkl}] +---- +birds = new Listing { + default { // <1> + lifespan = 8 + } + new { // <2> + name = "Pigeon" // <3> + } + new { // <4> + name = "Parrot" + lifespan = 20 // <5> + } +} +---- +<1> Amends the `default` element and sets property `lifespan`. +<2> Defines a new listing element that implicitly amends the default element. +<3> Defines a new property called `name`. Property `lifespan` is inherited from the default element. +<4> Defines a new listing element that implicitly amends the default element. +<5> Overrides the default for property `lifespan`. + +`default` is a hidden (that is, not rendered) link:{uri-stdlib-Listing-default}[property] defined in class `Listing`. +If `birds` had a <>, a suitable default element would be inferred from its type parameter. +If, as in our example, no type annotation is provided or inherited, the default element is the empty `Dynamic` object. + +Like regular listing elements, the default element is late-bound. +As a result, defaults can be changed retroactively: + +[source%tested,{pkl}] +---- +birds2 = (birds) { + default { + lifespan = 8 + diet = "Seeds" + } +} +---- + +Because both of ``birds``'s elements amend the default element, changing the default element also changes them. +An equivalent literal definition of `birds2` would look as follows: + +[source%tested,{pkl}] +---- +birds2 = new Listing { + new { + name = "Pigeon" + lifespan = 8 + diet = "Seeds" + } + new { + name = "Parrot" + lifespan = 20 + diet = "Berries" + } +} +---- + +Note that Parrot kept its diet because its prior self defined it explicitly, overriding any default. + +If you are interested in the technical underpinnings of default elements (and not afraid of dragons!), continue with <>. + +[[listing-type-annotations]] +=== Type Annotations + +To declare the type of a property that is intended to hold a listing, use: + +[source,{pkl}] +---- +x: Listing +---- + +This declaration has the following effects: + +* `x` is initialized with an empty listing. +* If `ElementType` has a <>, that value becomes the listing's default element. +* The first time `x` is read, + ** its value is checked to have type `Listing`. + ** the listing's elements are checked to have type `ElementType`. + +Here is an example: + +[source%tested,{pkl}] +---- +class Bird { + name: String + lifespan: Int +} + +birds: Listing +---- + +Because the default value for type `Bird` is `new Bird {}`, that value becomes the listing's default element. + +Let's go ahead and populate `birds`: + +[source%tested,{pkl}] +---- +birds { + new { + name = "Pigeon" + lifespan = 8 + } + new { + name = "Parrot" + lifespan = 20 + } +} +---- + +Thanks to ``birds``'s default element, which was inferred from its type, +it is not necessary to state the type of each list element +(`new Bird { ... }`, `new Bird { ... }`, etc.). + +==== Distinct Elements + +To constrain a listing to distinct elements, use ``Listing``'s link:{uri-stdlib-Listing-isDistinct}[isDistinct] property: + +[source%tested,{pkl}] +---- +class Bird { + name: String + lifespan: Int +} + +birds: Listing(isDistinct) +---- + +This is as close as Pkl's late-bound data types (objects, listings, and mappings) get to a <>. + +To demand distinct names instead of distinct `Bird` objects, use link:{uri-stdlib-Listing-isDistinctBy}[isDistinctBy()]: + +[source%tested,{pkl}] +---- +birds: Listing(isDistinctBy((it) -> it.name)) +---- + +[[mappings]] +== Mappings + +A value of type link:{uri-stdlib-Mapping}[Mapping] is an ordered collection of _values_ indexed by _key_. + +NOTE: Most of what has been said about <> also applies to mappings. +Nevertheless, this section is written to stand on its own. + +A mapping's key–value pairs are called its _entries_. +Keys are eagerly evaluated; values are lazily evaluated on the first read. + +Mappings combine qualities of maps and objects: + +* Like maps, mappings can contain arbitrary key–value pairs. +* Like objects, mappings excel at defining and amending nested literal data structures. +* Like objects, mappings can only be directly manipulated through amendment, + but converting them to a map (and, if necessary, back to a mapping) opens the door to arbitrary transformations. +* Like object properties, a mapping's values (but not its keys) are evaluated lazily, can be defined in terms of each other, and are late-bound. + +[TIP] +.When to use Mapping vs. <> +==== +* When key–value style data needs to be specified literally, use a mapping. +* When key–value style data needs to be transformed in a way that cannot be achieved by <> a mapping, use a map. +* If in doubt, use a mapping. + +Templates and schemas should almost always use mappings instead of maps. +Note that mappings can be converted to maps when the need arises. +==== + +=== Defining Mappings + +Mappings have the same literal syntax as objects, except that keys enclosed in `[]` take the place of property names. +Here is a mapping with two entries: + +[source%tested,{pkl}] +---- +birds = new Mapping { // <1> + ["Pigeon"] { // <2> + lifespan = 8 + diet = "Seeds" + } + ["Parrot"] { // <3> + lifespan = 20 + diet = "Berries" + } +} +---- +<1> Defines a module property named `birds` with a value of type `Mapping`. + A type only needs to be stated when the property does not have or inherit a <>. + Otherwise, amend syntax (`birds { ... }`) or shorthand instantiation syntax (`birds = new { ... }`) should be used. +<2> Defines a mapping entry with key `"Pigeon"` and a value of type `Dynamic`. +<3> Defines a mapping entry with key `"Parrot"` and a value of type `Dynamic`. + +To access a value by key, use the `[]` (subscript) operator: + +[source%tested,{pkl}] +---- +pigeon = birds["Pigeon"] +parrot = birds["Parrot"] +---- + +Mappings can contain arbitrary types of values: + +[source%tested,{pkl}] +---- +mapping = new Mapping { + ["number"] = 42 + ["list"] = List("Pigeon", "Parrot") + ["nested mapping"] { + ["Pigeon"] { + lifespan = 20 + diet = "Seeds" + } + } +} +---- + +Although string keys are most common, mappings can contain arbitrary types of keys: + +[source%tested,{pkl}] +---- +mapping = new Mapping { + [3.min] = 42 + [new Dynamic { name = "Pigeon" }] = "abc" +} +---- + +Keys can be computed: + +[source%tested,{pkl}] +---- +mapping = new Mapping { + ["Pigeon".reverse()] = 42 +} +---- + +Mappings can have `local` properties: + +[source%tested,{pkl}] +---- +mapping = new Mapping { + local parrot = "Parrot" // <1> + ["Pigeon"] { // <2> + friend = parrot + } +} +---- +<1> Defines a local property name `parrot` with value `"Parrot"`. + Local properties can have a type annotation, as in `parrot: String = "Parrot"`. +<2> Defines a mapping entry whose value references `parrot`. + The local property is visible to values but not keys. + +[[amending-mappings]] +=== Amending Mappings + +Let's say we have the following mapping: + +[source%tested,{pkl}] +---- +birds = new Mapping { + ["Pigeon"] { + lifespan = 8 + diet = "Seeds" + } + ["Parrot"] { + lifespan = 20 + diet = "Berries" + } +} +---- + +To add, override, or amend entries of this mapping, amend the mapping: + +[source%tested,{pkl}] +---- +birds2 = (birds) { // <1> + ["Barn owl"] { // <2> + lifespan = 15 + diet = "Mice" + } + ["Pigeon"] { // <3> + diet = "Seeds" + } + ["Parrot"] = new { // <4> + lifespan = 20 + diet = "Berries" + } +} +---- +<1> Defines a module property named `birds2`. Its value is a mapping that amends `birds`. +<2> Defines a mapping entry with key `"Barn owl"` and a value of type `Dynamic`. +<3> Amends mapping entry `"Pigeon"` and overrides property `diet`. +<4> Overrides mapping entry `"Parrot"` with an entirely new value of type `Dynamic`. + +=== Late Binding + +A mapping entry's value can be defined in terms of another entry's value. +To reference the value with key ``, use `this[]`: + +[source%tested,{pkl}] +---- +birds = new Mapping { + ["Pigeon"] { // <1> + lifespan = 8 + diet = "Seeds" + } + ["Parrot"] = (this["Pigeon"]) { // <2> + lifespan = 20 + } +} +---- +<1> Defines a mapping entry with key `"Pigeon"` and a value of type `Dynamic`. +<2> Defines a mapping entry with key `"Parrot"` and a value that amends `"Pigeon"`. + +Mapping values are late-bound: + +[source%tested,{pkl}] +---- +birds2 = (birds) { // <1> + ["Pigeon"] { + diet = "Seeds" + } +} + +parrotDiet = birds2["Parrot"].diet // <2> +---- +<1> Amends mapping `birds` and overrides ``"Pigeon"``'s `diet` property to have value `"Seeds"`. +<2> Because `"Parrot"` is defined in terms of `"Pigeon"`, its `diet` property also changes to `"Seeds"`. + +=== Transforming Mappings + +Say we have the following mapping: + +[source%tested,{pkl}] +---- +birds = new Mapping { + ["Pigeon"] { + lifespan = 8 + diet = "Seeds" + } + ["Parrot"] = (this["Pigeon"]) { + lifespan = 20 + } +} +---- + +How can ``birds``'s keys be reversed programmatically? + +The recipe for transforming a mapping is: + +. Convert the mapping to a map. +. Transform the map using ``Map``'s link:{uri-stdlib-Map}[rich API]. +. If necessary, convert the map back to a mapping. + +TIP: Often, transformations happen in a link:{uri-stdlib-PcfRenderer-converters}[converter] of a link:{uri-stdlib-ValueRenderer}[value renderer]. +As most value renderers treat maps the same as mappings, it is often not necessary to convert back to a mapping. + +Equipped with this knowledge, let's try to accomplish our objective: + +[source%tested,{pkl}] +---- +result = birds + .toMap() + .mapKeys((key, value) -> key.reverse()) + .toMapping() +---- + +`result` contains the same values as `birds`, but its keys have changed to `"noegiP"` and `"torraP"`. + +[IMPORTANT] +.Lazy vs. Eager Data Types +==== +Converting a mapping to a map is a transitition from a _lazy_ to an _eager_ data type. +All of the mapping's values are evaluated and all references between them are resolved. +(Mapping keys are eagerly evaluated.) + +If the map is later converted back to a mapping, changes to the mapping's values no longer propagate to (previously) dependent values. +To make these boundaries clear, transitioning between _lazy_ and _eager_ data types always requires an explicit method call, such as `toMap()` or `toMapping()`. +==== + +=== Default Value + +Mappings can have a _default value_: + +[source%tested,{pkl}] +---- +birds = new Mapping { + default { // <1> + lifespan = 8 + } + ["Pigeon"] { // <2> + diet = "Seeds" // <3> + } + ["Parrot"] { // <4> + lifespan = 20 // <5> + } +} +---- +<1> Amends the `default` value and sets property `lifespan`. +<2> Defines a mapping entry with key `"Pigeon"` that implicitly amends the default value. +<3> Defines a new property called `diet`. Property `lifespan` is inherited from the default value. +<4> Defines a mapping entry with key `"Parrot"` that implicitly amends the default value. +<5> Overrides the default for property `lifespan`. + +`default` is a hidden (that is, not rendered) link:{uri-stdlib-Mapping-default}[property] defined in class `Mapping`. +If `birds` had a <>, a suitable default value would be inferred from its second type parameter. +If, as in our example, no type annotation is provided or inherited, the default value is the empty `Dynamic` object. + +Like regular mapping values, the default value is late-bound. +As a result, defaults can be changed retroactively: + +[source%tested,{pkl}] +---- +birds2 = (birds) { + default { + lifespan = 8 + diet = "Seeds" + } +} +---- + +Because both of ``birds``'s mapping values amend the default value, changing the default value also changes them. +An equivalent literal definition of `birds2` would look as follows: + +[source%tested,{pkl}] +---- +birds2 = new Mapping { + ["Pigeon"] { + lifespan = 8 + diet = "Seeds" + } + ["Parrot"] { + lifespan = 20 + diet = "Berries" + } +} +---- + +Note that Parrot kept its lifespan because its prior self defined it explicitly, overriding any default. + +If you are interested in the technical underpinnings of default values, continue with <>. + +[[mapping-type-annotations]] +=== Type Annotations + +To declare the type of a property that is intended to hold a mapping, use: + +[source,{pkl}] +---- +x: Mapping +---- + +This declaration has the following effects: + +* `x` is initialized with an empty mapping. +* If `ValueType` has a <>, that value becomes the mapping's default value. +* The first time `x` is read, + ** its value is checked to have type `Mapping`. + ** the mapping's keys are checked to have type `KeyType`. + ** the mapping's values are checked to have type `ValueType`. + +Here is an example: + +[source%tested,{pkl}] +---- +class Bird { + lifespan: Int +} + +birds: Mapping +---- + +Because the default value for type `Bird` is `new Bird {}`, that value becomes the mapping's default value. + +Let's go ahead and populate `birds`: + +[source%tested,{pkl}] +---- +birds { + ["Pigeon"] { + lifespan = 8 + } + ["Parrot"] { + lifespan = 20 + } +} +---- + +Thanks to ``birds``'s default value, which was inferred from its type, +it is not necessary to state the type of each mapping value +(`["Pigeon"] = new Bird { ... }`, `["Parrot"] = new Bird { ... }`, etc.). + +[[classes]] +== Classes + +Classes are arranged in a single inheritance hierarchy. +At the top of the hierarchy sits class link:{uri-stdlib-Any}[Any]; at the bottom, type <>. + +Classes contain properties and methods, which can be `local` to their declaring scope. +Properties can also be `hidden` from rendering. + +[source%tested,{pkl}] +---- +class Bird { + name: String + hidden taxonomy: Taxonomy +} + +class Taxonomy { + `species`: String +} + +pigeon: Bird = new { + name = "Common wood pigeon" + taxonomy { + species = "Columba palumbus" + } +} + +pigeonClass = pigeon.getClass() +---- + +Declaration of new class instances will fail when property names are misspelled: + +[source%tested%error,{pkl}] +---- +// Detects the spelling mistake +parrot = new Bird { + namw = "Parrot" +} +---- + +=== Class Inheritance + +Pkl supports single inheritance with a Java(Script) like syntax. + +[source%tested,{pkl}] +---- +abstract class Bird { + name: String +} + +class ParentBird extends Bird { + kids: List +} + +pigeon: ParentBird = new { + name = "Old Pigeon" + kids = List("Pigeon Jr.", "Teen Pigeon") +} +---- + +[[methods]] +== Methods + +Modules and classes can define methods. +Submodules and subclasses can override them. + +Like Java and most other object-oriented languages, Pkl uses _single dispatch_ -- methods are dynamically dispatched based on the receiver's runtime type. + +[source%tested,{pkl}] +---- +class Bird { + name: String + function greet(bird: Bird): String = "Hello, \(bird.name)!" // <1> +} + +function greetPigeon(bird: Bird): String = bird.greet(pigeon) // <2> + +pigeon: Bird = new { + name = "Pigeon" +} +parrot: Bird = new { + name = "Parrot" +} + +greeting1 = pigeon.greet(parrot) // <3> +greeting2 = greetPigeon(parrot) // <4> +---- +<1> Instance method of class `Bird`. +<2> Module method. +<3> Call instance method on `pigeon`. +<4> Call module method (on `this`). + +[[modules]] +== Modules + +=== Introduction + +Modules are the unit of loading, executing, and sharing Pkl code. +Every file containing Pkl code is a module. +By convention, module files have a `.pkl` extension. + +Modules have a <> and are loaded from a <>. + +At runtime, modules are represented as objects of type link:{uri-stdlib-baseModule}/Module[Module]. +The precise runtime type of a module is a subclass of `Module` containing the module's property and method definitions. + +Like class members, module members may have type annotations, which are validated at runtime: + +[source%tested,{pkl}] +---- +timeout: Duration(isPositive) = 5.ms + +function greet(name: String): String = "Hello, \(name)!" +---- + +Because modules are regular objects, they can be assigned to properties and passed to and returned from methods. + +Modules can be <> by other modules. +In analogy to objects, modules can serve as templates for other modules through <>. +In analogy to classes, modules can be <> to add additional module members. + +=== Module Names + +Modules may declare their name by way of a _module clause_, which consists of the keyword `module` followed by a qualified name: + +[source%tested,{pkl}] +---- +/// My bird module. +module com.animals.Birds +---- + +A module clause must come first in a module. +Its doc comment, if present, holds the module's overall documentation. + +In the absence of a module clause, a module's name is inferred from the module URI from which the module was first loaded. +For example, the inferred name for a module first loaded from `+https://example.com/pkl/bird.pkl+` is `bird`. + +Module names do not affect evaluation but are used in diagnostic messages and Pkldoc. +In particular, they are the first component (everything before the hash sign) of fully qualified member names such as `pkl.base#Int`. + +NOTE: Modules shared with other parties should declare a qualified module name, which is more unique and stable than an inferred name. + +=== Module URIs + +Modules are loaded from _module URIs_. + +By default, the following URI types are available for import: + +==== File URI: +Example: `+file:///path/to/my_module.pkl+` + +Represents a module located on a file system. + +==== HTTP(S) URI: +Example: `+https://example.com/my_module.pkl+` + +Represents a module imported via an HTTP(S) GET request. + +NOTE: Modules loaded from HTTP(S) URIs are only cached until the `pkl` command exits or the `Evaluator` object is closed. + +[[module-path-uri]] +==== Module path URI: +Example: `+modulepath:/path/to/my_module.pkl+` + +Module path URIs are resolved relative to the _module path_, a search path for modules similar to Java's class path (see the `--module-path` CLI option). +For example, given the module path `/dir1:/zip1.zip:/jar1.jar`, module `+modulepath:/path/to/my_module.pkl+` will be searched for in the following locations: + +. `/dir1/path/to/my_module.pkl` +. `/path/to/my_module.pkl` within `/zip1.zip` +. `/path/to/my_module.pkl` within `/jar1.jar` + +When evaluating Pkl code from Java, `+modulepath:/path/to/my_module.pkl+` corresponds to class path location `path/to/my_module.pkl`. +In a typical Java project, this corresponds to file path `src/main/resources/path/to/my_module.pkl` or `src/test/resources/path/to/my_module.pkl`. + +[[package-asset-uri]] +==== Package asset URI: +Example: `+package://example.com/mypackage@1.0.0#/my_module.pkl+` + +Represent a module within a _package_. +A package is a shareable archive of modules and resources that are published to the internet. + +To import `package://example.com/mypackage@1.0.0#/my_module.pkl`, Pkl follows these steps: + +1. Make an HTTPS GET request to `https://example.com/mypackage@1.0.0` to retrieve the package's metadata. +2. From the package metadata, download the referenced zip archive, and validate its checksum. +3. Resolve path `/my_module.pkl` within the package's zip archive. + +A package asset URI has the following form: + +---- +'package://' '@' '#' +---- + +Optionally, the SHA-256 checksum of the package can also be specified: + +[source] +---- +'package://' '@' '::sha256:' '#' +---- + +Packages can be managed as dependencies within a _project_. +For more details, consult the <> section of the language reference. + +==== Standard Library URI + +Example: `+pkl:math+` + +Standard library modules are named `pkl.` and have module URIs of the form `pkl:`. +For example, module `pkl.math` has module URI `pkl:math`. +See the link:{uri-pkl-stdlib-docs-index}[API Docs] for the complete list of standard library modules. + +==== Relative URIs + +Relative module URIs are interpreted relative to the URI of the enclosing module. +For example, a module with URI `modulepath:/animals/birds/pigeon.pkl` +can import `modulepath:/animals/birds/parrot.pkl` +with `import "parrot.pkl"` or `import "/animals/birds/parrot.pkl"`. + +NOTE: When importing a relative folder or file that starts with `@`, the import string must be prefixed with `./`. +Otherwise, this syntax will be interpreted as dependency notation. + +==== Dependency notation URIs + +Example: `+@birds/bird.pkl+` + +Dependency notation URIs represent a path within a <>. +For example, import `@birds/bird.pkl` represents path `/bird.pkl` in a dependency named "birds". + +A dependency is either a remote package, or a local project dependency. + +==== Extension points + +Pkl embedders can register additional module loaders that recognize other types of module URIs. + +==== Evaluation + +Module URIs can be evaluated directly: + +[source,shell] +---- +$ pkl eval path/to/mymodule.pkl +$ pkl eval file:///path/to/my_module.pkl +$ pkl eval https://apple.com/path/to/mymodule.pkl +$ pkl eval --module-path=/pkl-modules modulepath:/path/to/my_module.pkl +$ pkl eval pkl:math +---- + +[[triple-dot-module-uris]] +==== Triple-dot Module URIs + +To simplify referencing ancestor modules in a hierarchical module structure, +relative file and module path URIs may start with `++...++/`, a generalization of `../`. +Module URI `++...++/foo/bar/baz.pkl` resolves to the first existing module among +`../foo/bar/baz.pkl`, `../../foo/bar/baz.pkl`, `../../../foo/bar/baz.pkl`, and so on. +Furthermore, module URI `++...++` is equivalent to `++...++/`. + +Using triple-dot module URIs never resolve to the current module. +For example, a module at path `foo/bar.pkl` that references module URI `++...++/foo/bar.pkl` +does not resolve to itself. + +[[module-amend]] +=== Amending a Module + +Recall how an object is amended: + +[source%tested,{pkl}] +---- +pigeon { + name = "Pigeon" + diet = "Seeds" +} + +parrot = (pigeon) { // <1> + name = "Parrot" // <2> +} +---- +<1> Object `parrot` amends object `pigeon`, inheriting all of its members. +<2> `parrot` overrides `name`. + +Amending a module works in the same way, except that the syntax differs slightly: + +.pigeon.pkl +[source%tested,{pkl}] +---- +name = "Pigeon" +diet = "Seeds" +---- + +.parrot.pkl +[source%parsed,{pkl}] +---- +amends "pigeon.pkl" // <1> + +name = "Parrot" // <2> +---- +<1> Module `parrot` amends module `pigeon`, inheriting all of its members. +<2> `parrot` overrides `name`. + +A module is amended by way of an _amends clause_, which consists of the keyword `amends` followed by the <> of the module to amend. + +An amends clause comes after the module clause (if present) and before any import clauses: + +.parrot.pkl +[source%parsed,{pkl}] +---- +module parrot + +amends "pigeon.pkl" + +import "other.pkl" + +name = "Parrot" +---- + +At most one amends clause is permitted. +A module cannot have both an amends clause and an extends clause. + +An amending module has the same type (that is, module class) as the module it amends. +As a consequence, it cannot define new properties, methods, or classes, unless they are declared as `local`. +In our example, this means that module `parrot` can only define (and thus override) the property `name`. +Spelling mistakes such as `namw` are caught immediately, rather than accidentally defining a new property. + +Amending is used to fill in _template modules_: + +. The template module defines which properties exist, their types, and what module output is desired (for example JSON indented with two spaces). +. The amending module fills in property values as required, relying on the structure, defaults and validation provided by the template module. +. The amending module is evaluated to produce the final result. + +Template modules are often provided by third parties and served over HTTPS. + +[[module-extend]] +=== Extending a Module + +Recall how a class is extended: + +.PigeonAndParrot.pkl +[source%tested,{pkl}] +---- +open class Pigeon { // <1> + name = "Pigeon" + diet = "Seeds" +} + +class Parrot extends Pigeon { // <2> + name = "Parrot" // <3> + diet = "Berries" // <4> + extinct = false // <5> + + function say() = "Pkl is great!" // <6> +} +---- +<1> Class `Pigeon` is declared as `open` for extension. +<2> Class `Parrot` extends `Pigeon`, inheriting all of its members. +<3> `Parrot` overrides `name`. +<4> `Parrot` overrides `diet`. +<5> `Parrot` defines a new property named `extinct`. +<6> `Parrot` defines a new function named `say`. + +Extending a module works in the same way, except that the syntax differs slightly: + +.pigeon.pkl +[source%tested,{pkl}] +---- +open module pigeon // <1> + +name = "Pigeon" +diet = "Seeds" +---- +<1> Module `pigeon` is declared as `open` for extension. + +.parrot.pkl +[source%parsed,{pkl}] +---- +extends "pigeon.pkl" // <1> + +name = "Parrot" // <2> +diet = "Berries" // <3> +extinct = false // <4> + +function say() = "Pkl is great!" // <5> +---- +<1> Module `parrot` extends module `pigeon`, inheriting all of its members. +<2> `parrot` overrides `name`. +<3> `parrot` overrides `diet`. +<4> `parrot` defines a new property named `extinct`. +<5> `parrot` defines a new function named `say`. + +A module is extended by way of an _extends clause_, which consists of the keyword `extends` followed by the <> of the module to extend. +The extends clause comes after the module clause (if present) and before any import clauses. +Only modules declared as `open` can be extended. + +.parrot.pkl +[source%parsed,{pkl}] +---- +module parrot + +extends "pigeon.pkl" + +import "other.pkl" + +name = "Parrot" +diet = "Berries" +extinct = false + +function say() = "Pkl is great!" +---- + +At most, one extends clause is permitted. +A module cannot have both an amends clause and an extends clause. + +Extending a module implicitly defines a new module class that extends the original module's class. + +[[import-module]] +=== Importing a Module + +A module import makes the imported module accessible to the importing module. +A module is imported by way of either an <>, or an <>. + +[[import-clause]] +==== Import Clauses + +An import clause consists of the keyword `import` followed by the <> of the module to import. +An import clause comes after module, amends and extends clauses (if present), and before the module body: + +.parrot.pkl +[source%parsed,{pkl}] +---- +module parrot + +amends "pigeon.pkl" + +import "module1.pkl" +import "module2.pkl" + +name = "Parrot" +---- + +Multiple import clauses are permitted. + +A module import implicitly defines a new `const local` property through which the imported module can be accessed. +(Remember that modules are regular objects.) +The name of this property, called _import name_, is constructed from the module URI as follows: + +. Strip the URI scheme, including the colon (`:`). +. Strip everything up to and including the last forward slash (`/`). +. Strip any trailing `.pkl` file extension. + +Here are some examples: + +.Local file import +[source%parsed,{pkl}] +---- +import "modules/pigeon.pkl" // relative to current module + +name = pigeon.name +---- + +.HTTPS import +[source%parsed,{pkl}] +---- +import "https://mycompany.com/modules/pigeon.pkl" + +name = pigeon.name +---- + +.Standard library import +[source%parsed,{pkl}] +---- +import "pkl:math" + +pi = math.Pi +---- + +.Package import +[source%parsed,{pkl}] +---- +import "package://example.com/birds@1.0.0#/sparrow.pkl" + +name = sparrow.name +---- + +Because its members are automatically visible in every module, the `pkl:base` module is typically not imported. + +Occasionally, the default import name for a module may not be convenient or appropriate: + +* If not a valid identifier, the import name needs to be enclosed in backticks on each use, for example, ``my-module`.someMember`. +* The import name may clash with other names in the importing module. + +In such a case, a different import name can be chosen: + +.parrot.pkl +[source%parsed,{pkl}] +---- +import "pigeon.pkl" as piggy + +name = "Parrot" +diet = piggy.diet +---- + +[TIP] +.What makes a good module file name? +==== +When creating a new module, especially one intended for import into other modules, try to choose a module file name that makes a good import name: + +* short ++ +Less than six characters, not counting the `.pkl` file extension, is a good rule of thumb. +* valid identifier ++ +Stick to alphanumeric characters. Use underscore (`_`) instead of hyphen (`-`) as name separator. +* descriptive ++ +An import name should make sense on its own and when used in qualified member names. +==== + +[[import-expression]] +==== Import Expressions (`import()`) + +An import expression consists of the keyword `import`, following by a <> wrapped in parentheses: + +[source,{pkl}] +---- +module birds + +pigeon = import("pigeon.pkl") +parrot = import("parrot.pkl") +---- + +Unlike import clauses, import expressions only import a value, and do not import a type. +A type is a name that can be used in type positions, for example, as a type annotation. + +[[globbed-imports]] +=== Globbed Imports + +Multiple modules may be imported at once with `import*`. +When importing multiple modules, a glob pattern is used to match against existing resources. +A globbed import evaluates to a `Mapping`, where keys are the expanded form of the glob and values are import expressions on each individual module. + +Globbed imports can be expressed as either a clause, or as an expression. +When expressed as a clause, they follow the same naming rules as a normal <>: they introduce a local property equal to the last path segment without the `.pkl` extension. +A globbed import clause cannot be used as a type. + +[source,{pkl}] +---- +import* "birds/*.pkl" as allBirds // <1> +import* "reptiles/*.pkl" <2> + +birds = import*("birds/*.pkl") // <3> +---- +<1> Globbed import clause +<2> Globbed import clause without explicit name (will import the name `*`) +<3> Globbed import expression + +Assuming that a file system contains these files: + +[source,txt] +---- +. +├── birds/ +│ ├── pigeon.pkl +│ ├── parrot.pkl +│ └── falcon.pkl +└── index.pkl +---- + +The following two snippets are logically identical: + +.index.pkl +[source,{pkl}] +---- +birds = import*("birds/*.pkl") +---- + +.index.pkl +[source,{pkl}] +---- +birds = new Mapping { + ["birds/pigeon.pkl"] = import("birds/pigeon.pkl") + ["birds/parrot.pkl"] = import("birds/parrot.pkl") + ["birds/falcon.pkl"] = import("birds/falcon.pkl") +} +---- + +By default, only the `file` and `package` schemes are globbable. +Globbing another scheme will cause Pkl to throw. + +Pkl can be extended to provide custom globbable schemes through the link:{uri-pkl-core-ModuleKey}[ModuleKey] +SPI. + +When globbing within <>, only the asset path (the fragment section) is globbable. +Otherwise, characters are interpreted verbatim, and not treated as glob wildcards. + +For details on how glob patterns work, refer to <> in the Advanced Topics section. + +NOTE: When globbing files, symbolic links are not followed. Additionally, the `.` and `..` entries are skipped. + +This behavior is similar to the behavior of Bash with `shopt -s dotglob` enabled. + +=== Security Checks + +When attempting to directly evaluate a module, as in `pkl myModule.pkl`, +the following security checks are performed: + +* The module URI is checked against the module allowlist (`--allowed-modules`). + +The module allowlist is a comma-separated list of regular expressions. +For access to be granted, at least one regular expression must match a prefix of the module URI. +For example, the allowlist `file:,https:` grants access to any module whose URI starts with `file:` or `https:`. + +When a module attempts to load another module (via `amends`, `extends`, or `imports`), +the following security checks are performed: + +* The target module URI is checked against the module allowlist (`--allowed-modules`). +* The source and target modules' _trust levels_ are determined and compared. + +For access to be granted, the source module's trust level must be higher than or equal to the target module's trust level. +By default, there are five trust levels, listed from highest to lowest: + +. `repl:` modules (code evaluated in the REPL) +. `file:` modules +. `modulepath:` modules +. All other modules (for example `https:`) +. `pkl:` modules (standard library) + +For example, this means that `file:` modules can import `https:` modules, but not the other way around. + +If a module URI is resolved in multiple steps, all URIs are subject to the above security checks. +An example for this is an HTTPS URL that results in a redirect. + +Pkl embedders can further customize security checks. + +[[module-output]] +=== Module Output + +By default, the output of evaluating a module is the entire module rendered as Pcf. +There are two ways to change this behavior: + +1. _Outside_ the language, by using the `--format` CLI option or the `outputFormat` Gradle task property. +2. _Inside_ the language, by configuring a module's `output` property. + +==== CLI + +Given the following module: + +[source%tested,{pkl}] +.config.pkl +---- +a = 10 +b { + c = 20 +} +---- + +`pkl config.pkl`, which is shorthand for `pkl --format pcf config.pkl`, renders the module as Pcf: + +[source,{pkl}] +---- +a = 10 +b { + c = 20 +} +---- + +`pkl --format yaml config.pkl` renders the module as YAML: + +[source,yaml] +---- +a: 10 +b: + c: 20 +---- + +Likewise, `pkl --format json config.pkl` renders the module as JSON. + +[[in-language]] +==== In-language + +Now let's do the same -- and more -- inside the language. + +Modules have an link:{uri-stdlib-baseModule}/Module#output[output] property that controls what the module's output is and how that output is rendered. + +To control *what* the output is, set the link:{uri-stdlib-baseModule}/ModuleOutput#value[output.value] property: + +[source%parsed,{pkl}] +---- +a = 10 +b { + c = 20 +} +output { + value = b // defaults to `outer`, which is the entire module +} +---- + +This produces: + +[source,{pkl}] +---- +c = 20 +---- + +To control _how_ the output is rendered, set the link:{uri-stdlib-baseModule}/ModuleOutput#renderer[output.renderer] property: + +[source%parsed,{pkl}] +---- +a = 10 +b { + c = 20 +} + +output { + renderer = new YamlRenderer {} +} +---- + +The standard library provides these renderers: + +* link:{uri-stdlib-baseModule}/JsonRenderer[JsonRenderer] +* link:{uri-stdlib-jsonnetModule}/Renderer[jsonnet.Renderer] +* link:{uri-stdlib-baseModule}/PcfRenderer[PcfRenderer] +* link:{uri-stdlib-baseModule}/PListRenderer[PListRenderer] +* link:{uri-stdlib-baseModule}/PropertiesRenderer[PropertiesRenderer] +* link:{uri-stdlib-protobufModule}/Renderer[protobuf.Renderer] +* link:{uri-stdlib-xmlModule}/Renderer[xml.Renderer] +* link:{uri-stdlib-baseModule}/YamlRenderer[YamlRenderer] + +To render a format that is not yet supported, implement your own renderer by extending class link:{uri-stdlib-baseModule}/ValueRenderer[ValueRenderer]. + +The standard library renderers can be configured with _value converters_, which influence how particular values are rendered. + +For example, since YAML does not have a standard way to represent data sizes, a plain `YamlRenderer` cannot render `DataSize` values. +However, we can teach it to: + +[source%parsed,{pkl}] +---- +quota { + memory = 100.mb + disk = 20.gb +} + +output { + renderer = new YamlRenderer { + converters { + [DataSize] = (size) -> "\(size.value) \(size.unit)" + } + } +} +---- + +This produces: + +[source,yaml] +---- +quota: + memory: 100 MB + disk: 20 GB +---- + +In addition to _type_ based converters, renderers also support _path_ based converters: + +[source%parsed,{pkl}] +---- +output { + renderer = new YamlRenderer { + converters { + ["quota.memory"] = (size) -> "\(size.value) \(size.unit)" + ["quota.disk"] = (size) -> "\(size.value) \(size.unit)" + } + } +} +---- + +For more on path based converters, see {uri-stdlib-PcfRenderer-converters}[PcfRenderer.converters]. + +Sometimes it is useful to directly compute the final module output, bypassing `output.value` and `output.converters`. +To do so, set the link:{uri-stdlib-baseModule}/ModuleOutput#text[output.text] property to a String value: + +[source%parsed,{pkl}] +---- +output { + // defaults to `renderer.render(value)` + text = "this is the final output".toUpperCase() +} +---- + +This produces: + +[source] +---- +THIS IS THE FINAL OUTPUT +---- + +[[multiple-file-output]] +==== Multiple File Output + +Sometimes, it is desirable for a single module to produce multiple output files. +This is possible by configuring a module's link:{uri-stdlib-outputFiles}[`output.files`] property +// suppress inspection "AsciiDocLinkResolve" +and specifying the xref:pkl-cli:index.adoc#multiple-file-output-path[`--multiple-file-output-path`] +(or `-m` for short) CLI option. + +Here is an example that produces a JSON and a YAML file: + +.birds.pkl +[source%parsed,{pkl}] +---- +pigeon { + name = "Pigeon" + diet = "Seeds" +} +parrot { + name = "Parrot" + diet = "Seeds" +} +output { + files { + ["birds/pigeon.json"] { + value = pigeon + renderer = new JsonRenderer {} + } + ["birds/parrot.yaml"] { + value = parrot + renderer = new YamlRenderer {} + } + } +} +---- + +Running `pkl -m output/ pigeon.pkl` produces the following output files: + +.output/birds/pigeon.json +[source,json] +---- +{ + "name": "Pigeon", + "diet": "Seeds" +} +---- + +.output/birds/parrot.yaml +[source,yaml] +---- +name: Parrot +diet: Berries +---- + +Within `output.files`, +a key determines a file's path relative to `--multiple-file-output-path`, +and a value determines the file's contents. +If a file's path resolves to a location outside `--multiple-file-output-path`, +evaluation fails with an error. +Non-existing parent directories are created. + +[[aggregating-module-outputs]] +===== Aggregating Module Outputs + +A value within `output.files` can be another module's `output`. +With this, a module can aggregate the outputs of multiple other modules. +Here is an example: + +.pigeon.pkl +[source%parsed,{pkl}] +---- +name = "Pigeon" +diet = "Seeds" +output { + renderer = new JsonRenderer {} +} +---- + +.parrot.pkl +[source%parsed,{pkl}] +---- +name = "Parrot" +diet = "Seeds" +output { + renderer = new YamlRenderer {} +} +---- + +.birds.pkl +[source%parsed,{pkl}] +---- +import "pigeon.pkl" +import "parrot.pkl" + +output { + files { + ["birds/pigeon.json"] = pigeon.output + ["birds/parrot.yaml"] = parrot.output + } +} +---- + +[TIP] +==== +When aggregating module outputs, +the appropriate file extensions can be obtained programmatically: + +.birds.pkl +[source%parsed,{pkl}] +---- +import "pigeon.pkl" +import "parrot.pkl" + +output { + files { + ["birds/pigeon.\(pigeon.output.renderer.extension)"] = pigeon.output + ["birds/parrot.\(parrot.output.renderer.extension)"] = parrot.output + } +} +---- +==== + +[[null-values]] +== Null Values + +The keyword `null` indicates the absence of a value. +`null` is an instance of link:{uri-stdlib-Null}[Null], a direct subclass of `Any`. + +=== Non-Null Operator + +The `!!` (non-null) operator asserts that its operand is non-null. +Here are some examples: + +[source%tested%error,{pkl}] +---- +name = "Pigeon" +nameNonNull = name!! // <1> + +name2 = null +name2NonNull = name2!! // <2> +---- +<1> result: `"Pigeon"` +<2> result: _Error: Expected a non-null value, but got `null`._ + +=== Null Coalescing + +The `??` (null coalescing) operator fills in a default for a `null` value. + +[source,{pkl-expr}] +---- +value ?? default +---- + +The above expression evaluates to `value` if `value` is non-null, and to `default` otherwise. +Here are some examples: + +[source%tested,{pkl}] +---- +name = "Pigeon" +nameOrParrot = name ?? "Parrot" // <1> + +name2 = null +name2OrParrot = name2 ?? "Parrot" // <2> +---- +<1> result: `"Pigeon"` +<2> result: `"Parrot"` + +[NOTE] +.Default non-null behavior +==== +There are many languages that allow `null` for (almost) every type, but Pkl does not. +Any type can be extended to include `null` by appending `?` to the type. + +For example, `parrot: Bird` will always be non-null, but `pigeon: Bird?` could be `null` - and _is_ by default, +if `pigeon` is never amended. This means if you try to coalesce a (non-nullable) typed variable, +the result is always that variable’s value. + +As per our example `parrot ?? pigeon == parrot` always holds, +but `pigeon ?? parrot` could either be `pigeon` or `parrot`, +depending on whether `pigeon` was ever amended with a non-null value. +==== + +[[null-propagation]] +=== Null Propagation + +The `?.` (null propagation) operator provides null-safe access to a member whose receiver may be `null`. + +[source,{pkl-expr}] +---- +value?.member +---- + +The above expression evaluates to `value.member` if `value` is non-null, and to `null` otherwise. +Here are some examples: + +[source%tested,{pkl}] +---- +name = "Pigeon" +nameLength = name?.length // <1> +nameUpper = name?.toUpperCase() // <2> + +name2 = null +name2Length = name2?.length // <3> +name2Upper = name2?.toUpperCase() // <4> +---- +<1> result: `6` +<2> result: `"PIGEON"` +<3> result: `null` +<4> result: `null` + +The `?.` operator is often combined with `??`: + +[source%tested,{pkl}] +---- +name = null +nameLength = name?.length ?? 0 // <1> +---- +<1> result: `0` + +=== ifNonNull Method + +The link:{uri-stdlib-ifNonNull}[ifNonNull()] method is a generalization of the <> operator. + +[source%parsed,{pkl-expr}] +---- +name.ifNonNull((it) -> doSomethingWith(it)) +---- + +The above expression evaluates to `doSomethingWith(name)` if `name` is non-null, and to `null` otherwise. +Here are some examples: + +[source%tested,{pkl}] +---- +name = "Pigeon" +nameWithTitle = name.ifNonNull((it) -> "Dr." + it) // <1> + +name2 = null +name2WithTitle = name2.ifNonNull((it) -> "Dr." + it) // <2> +---- +<1> result: `"Dr. Pigeon"` +<2> result: `null` + +=== NonNull Type Alias + +To express that a property can have any type except `Null`, use the `NonNull` <>: + +[source,{pkl}] +---- +x: NonNull +---- + +[[if-expressions]] +== If Expressions + +An `if` expression serves the same role as the ternary operator (`? :`) in other languages. +Every `if` expression must have an `else` branch. + +[source%tested,{pkl}] +---- +num = if (2 + 2 == 5) 1984 else 42 // <1> +---- +<1> result: `42` + +[[resources]] +== Resources + +Pkl programs can read external resources, such as environment variables or text files. + +To read a resource, use a `read` expression: + +[source%parsed,{pkl}] +---- +path = read("env:PATH") +---- + +By default, the following resource URI schemes are supported: + +env: :: Reads an environment variable. +Result type is `String`. +prop: :: Reads an external property set via the `-p name=value` CLI option. +Result type is `String`. +file: :: Reads a file from the file system. +Result type is link:{uri-stdlib-Resource}[Resource]. +http(s): :: Reads an HTTP(S) resource. +Result type is link:{uri-stdlib-Resource}[Resource]. +modulepath: :: Reads a resource from the module path (`--module-path`) or JVM class path. +Result type is link:{uri-stdlib-Resource}[Resource]. +See <> for further information. +package: :: Reads a resource from a _package_. Result type is link:{uri-stdlib-Resource}[Resource]. See <> for further information. + +Relative resource URIs are resolved against the enclosing module's URI. + +Resources are cached in memory on the first read. +Therefore, subsequent reads are guaranteed to return the same result. + +[[nullable-reads]] +=== Nullable Reads + +If a resource does not exist or cannot be read, `read()` fails with an error. +To recover from the absence of a resource, use `read?()` instead, +which returns `null` for absent resources: + +[source%parsed,{pkl}] +---- +port = read?("env:PORT")?.toInt() ?? 1234 +---- + +[[globbed-reads]] +=== Globbed Reads + +Multiple resources may be read at the same time with `read*()`. +When reading multiple resources, a glob pattern is used to match against existing resources. +A globbed read returns a `Mapping`, where the keys are the expanded form of the glob, and values are `read` expressions on each individual resource. + +Assuming that a file system contains these files: + +[source,txt] +---- +. +├── birds/ +│ ├── pigeon.pkl +│ ├── parrot.pkl +│ └── falcon.pkl +└── index.pkl +---- + +The following two snippets are logically identical: + +.index.pkl +[source%parsed,{pkl}] +---- +birdFiles = read*("birds/*.pkl") +---- + +.index.pkl +[source%parsed,{pkl}] +---- +birdFiles = new Mapping { + ["birds/pigeon.pkl"] = read("birds/pigeon.pkl") + ["birds/parrot.pkl"] = read("birds/parrot.pkl") + ["birds/falcon.pkl"] = read("birds/falcon.pkl") +} +---- + +By default, the following schemes support globbing: + +* `modulepath` +* `file` +* `env` +* `prop` + +Globbing other resources results in an error. + +For details on how glob patterns work, reference <> in the Advanced Topics section. + +NOTE: When globbing files, symbolic links are not followed. Additionally, the `.` and `..` entries are skipped. + +This behavior is similar to the behavior of Bash with `shopt -s dotglob` enabled. + +[NOTE] +==== +The `env` and `prop` schemes are considered opaque, as they do not have traditional hierarchical elements like a host, path, or query string. + +While globbing is traditionally viewed as a way to match elements in a file system, a glob pattern is simply a way to match strings. +Thus, environment variables and external properties can be globbed, where their names get matched according to the rules described by the glob pattern. + +To match all values within these schemes, use the `+++**+++` wildcard. +This has the effect of matching names that contain a forward slash too (`/`). +For example, the expression `read*("+++env:**+++")` will evaluate to a Mapping of all environment variables. +==== + +=== Extending resource readers + +When Pkl is embedded within another runtime, it can be extended to read other kinds of resources. + +When embedded into a JVM application, new resources may be read by implementing the link:{uri-pkl-core-ResourceReader}[ResourceReader] SPI. +When Pkl is embedded within Swift, new resources may be read by implementing the link:{uri-pkl-swift-resource-reader-docs}[ResourceReader] interface. +When Pkl is embedded within Go, new resources may be read by implementing the link:{uri-pkl-go-resource-reader-docs}[ResourceReader] interface. + +=== Resource Allowlist + +When attempting to read a resource, the resource URI is checked against the resource allowlist (`--allowed-resources`). +In embedded mode, the allowlist is configured via an evaluator's link:{uri-pkl-core-SecurityManager}[SecurityManager]. + +The resource allowlist is a comma-separated list of regular expressions. +For access to be granted, at least one regular expression must match a prefix of the resource URI. +For example, the allowlist `file:,https:` grants access to any resource whose URI starts with `file:` or `https:`. + +[[errors]] +== Errors + +By design, errors are fatal in Pkl -- there is no way to recover from them. +To raise an error, use a `throw` expression: + +[source%tested%error,{pkl}] +---- +myValue = throw("You won't be able to recover from this one!") // <1> +---- +<1> `myValue` never receives a value because the program exits. + +The error message is printed to the console, and the program exits. +In embedded mode, a link:{uri-pkl-core-PklException}[PklException] is thrown. + +[[debugging]] +== Debugging + +When debugging Pkl code, it can be useful to print the value of an expression. +To do so, use a `trace` expression: + +[source%tested,{pkl}] +---- +num1 = 42 +num2 = 16 +res = trace(num1 * num2) +---- + +Tracing an expression does not affect its result, but prints both its source code and result on standard error: + +[source,shell] +---- +pkl: TRACE: num1 * num2 = 672 (at file:///some/module.pkl, line 42) +---- + +[[advanced-topics]] +== Advanced Topics + +This section discusses language features that are generally more relevant to template and library authors than template consumers. + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +<> + +[[let-expressions]] +=== Let Expressions + +A `let` expression is Pkl's version of an (immutable) local variable. +Its syntax is: + +[source] +---- +let ( = ) +---- + +A `let` expression is evaluated as follows: + +. `` is bound to ``, itself an expression. +. `` is evaluated, which can refer to `` by its `` (this is the point). +. The result of becomes the result of the overall expression. + +Here is an example: + +[source%tested,{pkl}] +---- +birdDiets = let (diets = List("Seeds", "Berries", "Mice")) +List(diets[2], diets[0]) // <1> +---- +<1> result: `List("Mice", "Seeds")` + +`let` expressions serve two purposes: + +- They introduce a human-friendly name for a potentially complex expression. +- They evaluate a potentially expensive expression that is used in multiple places only once. + +`let` expressions can have type annotations: + +[source%tested,{pkl}] +---- +birdDiets = let (diets: List = List("Seeds", "Berries", "Mice")) +diets[2] + diets[0] // <1> +---- +<1> result: `List("Mice", "Seeds")` + +`let` expressions can be stacked: + +[source%tested,{pkl}] +---- +birdDiets = let (birds = List("Pigeon", "Barn owl", "Parrot")) +let (diet = List("Seeds", "Mice", "Berries")) +birds.zip(diet) // <1> +---- +<1> result: `List(Pair("Pigeon", "Seeds"), Pair("Barn owl", "Mice"), Pair("Parrot", "Berries"))` + +[[type-tests]] +=== Type Tests + +To test if a value conforms to a type, use the _is_ operator. + +All the following tests hold: + +[source%tested,{pkl}] +---- +test1 = 42 is Int +test2 = 42 is Number +test3 = 42 is Any +test4 = !(42 is String) + +open class Base +class Derived extends Base + +base = new Base {} + +test5 = base is Base +test6 = base is Any +test7 = !(base is Derived) + +derived = new Derived {} + +test8 = derived is Derived +test9 = derived is Base +test10 = derived is Any +---- + +A value can be tested against any type, not just a class: + +[source,{pkl}] +---- +test1 = email is String(contains("@")) // <1> +test2 = map is Map // <2> +test3 = name is "Pigeon"|"Barn owl"|"Parrot" // <3> +---- +<1> `email` is tested for being a string that contains a `@` sign +<2> `map` is tested for being a map from `Int` to `Base` values +<3> `name` is tested for being one of `"Pigeon"`, `"Barn owl"`, or `"Parrot"` + +[[type-casts]] +=== Type Casts + +The _as_ (type cast) operator performs a runtime type check on its operand. +If the type check succeeds, the operand is returned as-is; otherwise, an error is thrown. + +[source%tested,{pkl}] +---- +birds { + new { name = "Pigeon" } + new { name = "Barn owl" } +} +names = birds.toList().map((it) -> it.name) as List +---- + +Although type casts are never mandatory in Pkl, they occasionally help humans and tools better understand an expression's type. + +[[lists]] +=== Lists + +A value of type link:{uri-stdlib-List}[List] is an ordered, indexed collection of _elements_. + +A list's elements have zero-based indexes and are eagerly evaluated. + +[TIP] +.When to use List vs. <> +==== +* When a collection of elements needs to be specified literally, use a listing. +* When a collection of elements needs to be transformed in a way that cannot be achieved by <> a listing, use a list. +* If in doubt, use a listing. + +Templates and schemas should almost always use listings instead of lists. +Note that listings can be converted to lists when the need arises. +==== + +Lists are constructed with the `List()` methodfootnote:soft-keyword[Strictly speaking, `List`, `Set`, and `Map` are currently soft keywords. The goal is to eventually turn them into regular standard library methods.]: + +[source%tested,{pkl}] +---- +list1 = List() // <1> +list2 = List(1, 2, 3) // <2> +list3 = List(1, "x", 5.min, List(1, 2, 3)) // <3> +---- +<1> result: empty list +<2> result: list of length 3 +<3> result: heterogenous list whose last element is another list + +To concatenate lists, use the `+` operator: + +[source%tested,{pkl-expr}] +---- +List(1, 2) + List(3, 4) + List(5) +---- + +To access a list element by index, use the `[]` (subscript) operator: + +[source%tested,{pkl}] +---- +list = List(1, 2, 3, 4) +listElement = list[2] // <1> +---- +<1> result: `3` + +Class `List` offers a link:{uri-stdlib-List}[rich API]. +Here are just a few examples: + +[source%tested,{pkl}] +---- +list = List(1, 2, 3, 4) +res1 = list.contains(3) // <1> +res2 = list.first // <2> +res3 = list.rest // <3> +res4 = list.reverse() // <4> +res5 = list.drop(1).take(2) // <5> +res6 = list.map((n) -> n * 3) // <6> +---- +<1> result: `true` +<2> result: `1` +<3> result: `List(2, 3, 4)` +<4> result: `List(4, 3, 2, 1)` +<5> result: `List(2, 3)` +<6> result: `List(3, 6, 9, 12)` + +[[sets]] +=== Sets + +A value of type link:{uri-stdlib-Set}[Set] is an ordered collection of unique _elements_. + +A set's elements are eagerly evaluated. + +Sets are constructed with the `Set()` methodfootnote:soft-keyword[]: + +[source%tested,{pkl}] +---- +res1 = Set() // <1> +res2 = Set(1, 2, 3) // <2> +res3 = Set(1, 2, 3, 1) // <3> +res4 = Set(1, "x", 5.min, List(1, 2, 3)) // <4> +---- +<1> result: empty set +<2> result: set of length 3 +<3> result: same set of length 3 +<4> result: heterogenous set that contains a list as its last element + +To compute the union of sets, use the `+` operator: + +[source%tested,{pkl-expr}] +---- +Set(1, 2) + Set(2, 3) + Set(5, 3) // <1> +---- +<1> result: `Set(1, 2, 3, 5)` + +Class `Set` offers a link:{uri-stdlib-Set}[rich API]. +Here are just a few examples: + +[source%tested,{pkl}] +---- +set = Set(1, 2, 3, 4) +res1 = set.contains(3) // <1> +res2 = set.drop(1).take(2) // <2> +res3 = set.map((n) -> n * 3) // <3> +res4 = set.intersect(Set(3, 9, 2)) // <4> +---- +<1> result: `true` +<2> result: `Set(2, 3)` +<3> result: `Set(3, 6, 9, 12)` +<4> result: `Set(3, 2)` + +[[maps]] +=== Maps + +A value of type link:{uri-stdlib-Map}[Map] is an ordered collection of _values_ indexed by _key_. + +A map's key–value pairs are called its _entries_. +Keys and values are eagerly evaluated. + +[TIP] +.When to use Map vs. <> +==== +* When key–value style data needs to be specified literally, use a mapping. +* When key–value style data needs to be transformed in ways that cannot be achieved by <> a mapping, use a map. +* If in doubt, use a mapping. + +Templates and schemas should almost always use mappings instead of maps. +(Note that mappings can be converted to maps when the need arises.) +==== + +Maps are constructed by passing alternating keys and values to the `Map()` methodfootnote:soft-keyword[]: + +[source%tested,{pkl}] +---- +map1 = Map() // <1> +map2 = Map(1, "one", 2, "two", 3, "three") // <2> +map3 = Map(1, "x", 2, 5.min, 3, Map(1, 2)) // <3> +---- +<1> result: empty map +<2> result: set of length 3 +<3> result: heterogeneous map whose last value is another map + +Any Pkl value can be used as map key: + +[source%tested,{pkl-expr}] +---- +Map(new Dynamic { name = "Pigeon" }, 10.gb) +---- + +To merge maps, use the `+` operator: + +[source%tested,{pkl}] +---- +combinedMaps = Map(1, "one") + Map(2, "two", 1, "three") + Map(4, "four") // <1> +---- +<1> result: `Map(1, "three", 2, "two", 4, "four")` + +To access a value by key, use the `[]` (subscript) operator: + +[source%tested,{pkl}] +---- +map = Map("Pigeon", 5.gb, "Parrot", 10.gb) +parrotValue = map["Parrot"] // <1> +---- +<1> result: `10.gb` + +Class `Map` offers a link:{uri-stdlib-Map}[rich API]. +Here are just a few examples: + +[source%tested,{pkl}] +---- +map = Map("Pigeon", 5.gb, "Parrot", 10.gb) +res1 = map.containsKey("Parrot") // <1> +res2 = map.containsValue(8.gb) // <2> +res3 = map.isEmpty // <3> +res4 = map.length // <4> +res5 = map.getOrNull("Falcon") // <5> +---- +<1> result: `true` +<2> result: `false` +<3> result: `false` +<4> result: `2` +<5> result: `null` + +[[regular-expressions]] +=== Regular Expressions + +A value of type link:{uri-stdlib-Regex}[Regex] is a regular expression with the same syntax and semantics as a link:{uri-javadoc-Pattern}[Java regular expression]. + +Regular expressions are constructed with the link:{uri-stdlib-Regex-method}[Regex()] method: + +[source%tested,{pkl}] +---- +emailRegex = Regex(#"([\w\.]+)@([\w\.]+)"#) +---- + +// note: first \ on next line is asciidoc escape +Notice the use of custom string delimiters `\#"` and `"#`, which change the string's escape character from `\` to `\#`. +As a consequence, the regular expression's own backslash escape character no longer needs to be escaped. + +To test if a string fully matches a regular expression, use link:{uri-stdlib-matches}[String.matches()]: + +[source%tested,{pkl-expr}] +---- +"pigeon@example.com".matches(emailRegex) +---- + +Many `String` methods accept either a `String` or `Regex` argument. +Here is an example: + +[source%tested,{pkl}] +---- +res1 = "Pigeon".contains("pigeon@example.com") +res2 = "Pigeon".contains(emailRegex) +---- + +To find all matches of a regex in a string, use link:{uri-stdlib-Regex-match}[Regex.findMatchesIn()]. +The result is a list of link:{uri-stdlib-RegexMatch}[RegexMatch] objects containing details about each match: + +[source%tested,{pkl}] +---- +matches = emailRegex.findMatchesIn("pigeon@example.com / falcon@example.com / parrot@example.com") +list1 = matches.drop(1).map((it) -> it.start) // <1> +list2 = matches.drop(1).map((it) -> it.value) // <2> +list3 = matches.drop(1).map((it) -> it.groups[1].value) // <3> +---- +<1> result: `List(0, 19, 40)` (the entire match, `matches[0]`, was dropped) +<2> result: `List("pigeon@example.com", "falcon@example.com", "parrot@example.com")` +<3> result: `List("pigeon, falcon, parrot")` + +[[type-aliases]] +=== Type Aliases + +A _type alias_ introduces a new name for a (potentially complicated) type: + +[source%tested,{pkl}] +---- +typealias EmailAddress = String(matches(Regex(#".+@.+"#))) +---- + +Once a type alias has been defined, it can be used in type annotations: + +[source%tested,{pkl}] +---- +email: EmailAddress = "pigeon@example.com" + +emailList: List = List("pigeon@example.com", "parrot@example.com") +---- + +New type aliases can be defined in terms of existing ones: + +[source%tested,{pkl}] +---- +typealias EmailList = List + +emailList: EmailList = List("pigeon@example.com", "parrot@example.com") +---- + +Type aliases can have type parameters: + +[source%tested,{pkl}] +---- +typealias StringMap = Map + +map: StringMap = Map("Pigeon", 42, "Falcon", 21) +---- + +Code generators have different strategies for dealing with type aliases: + +* the xref:java-binding:codegen.adoc[Java] code generator inlines them +* the xref:kotlin-binding:codegen.adoc[Kotlin] code generator turns them into Kotlin type aliases. + +Type aliases for unions of <> are turned into enum classes by both code generators. + +[[predefined-type-aliases]] +==== Predefined Type Aliases + +The _pkl.base_ module defines the following type aliases: + +* link:{uri-stdlib-Int8}[Int8] (-128 to 127) +* link:{uri-stdlib-Int16}[Int16] (-32,768 to 32,767) +* link:{uri-stdlib-Int32}[Int32] (-2,147,483,648 to 2,147,483,647) + +//- +* link:{uri-stdlib-UInt8}[UInt8] (0 to 255) +* link:{uri-stdlib-UInt16}[UInt16] (0 to 65,535) +* link:{uri-stdlib-UInt32}[UInt32] (0 to 4,294,967,295) +* link:{uri-stdlib-UInt}[UInt] (0 to 9,223,372,036,854,775,807) + +//- +* link:{uri-stdlib-Uri}[Uri] (any String value) + +WARNING: Note that `UInt` has the same maximum value as `Int`, half of what would normally be expected. + +The main purpose of the provided integer aliases is to enforce the range of an integer: + +[source%tested%error,{pkl}] +---- +port: UInt16 = -1 +---- + +This gives: + +[source,shell,subs="quotes"] +---- +Type constraint *isBetween(0, 65535)* violated. +Value: -1 +---- + +To restrict a number to a custom range, use the link:{uri-stdlib-isBetween}[isBetween] method: + +[source%tested,{pkl}] +---- +port: Int(isBetween(0, 1023)) = 443 +---- + +NOTE: Remember that numbers are always instances of `Int` or `Float`. +Type aliases such as `UInt16` only check that numbers are within a certain range. + +The xref:java-binding:codegen.adoc[Java] and xref:kotlin-binding:codegen.adoc[Kotlin] code generators +map predefined type aliases to the most suitable Java and Kotlin types. +For example, `UInt8` is mapped to `java.lang.Byte` and `kotlin.Byte`, and `Uri` is mapped to `java.net.URI`. + +[[type-annotations]] +=== Type Annotations + +Property and method definitions may optionally contain type annotations. +Type annotations serve the following purposes: + +* Documentation ++ Type annotations help documenting data models. They are included in generated documentation. + +* Validation ++ Type annotations are validated at runtime. + +* Defaults ++ Type-annotated properties have <>. + +* Code Generation ++ Type annotations enable statically typed access to configuration data through code generation. + +* Tooling ++ Type annotations enable advanced tooling features such as code completion in editors. + +==== Class Types + +Any class can be used as a type: + +[source%parsed,{pkl}] +---- +class Bird { + name: String // <1> +} +bird: Bird // <2> +---- +<1> Declares an instance property of type `String`. +<2> Declares a module property of type `Bird`. + +==== Module Types + +Any module import can be used as type: + +[source%parsed,{pkl}] +.bird.pkl +---- +name: String +lifespan: Int +---- + +[source%parsed,{pkl}] +.birds.pkl +---- +import "bird.pkl" + +pigeon: bird // <1> +parrot: bird // <1> +---- +<1> Guaranteed to amend _bird.pkl_. + +As a special case, the `module` keyword denotes the _enclosing_ module's type: + +[source%parsed,{pkl}] +.bird.pkl +---- +name: String +lifespan: Int +friends: Listing +---- + +[source%parsed,{pkl}] +.pigeon.pkl +---- +amends "bird.pkl" + +name = "Pigeon" +lifespan = 8 +friends { + import("falcon.pkl") // <1> +} +---- +<1> _falcon.pkl_ (not shown here) is guaranteed to amend _bird.pkl_. + +==== Type Aliases + +Any <> can be used as a type: + +[source%parsed,{pkl}] +---- +typealias EmailAddress = String(contains("@")) + +email: EmailAddress // <1> + +emailList: List // <2> +---- +<1> equivalent to `email: String(contains("@"))` for type checking purposes +<2> equivalent to `emailList: List` for type checking purposes + +==== Nullable Types + +Class types such as `Bird` (see above) do not admit `null` values. +To turn them into _nullable types_, append a question mark (`?`): + +[source%parsed,{pkl}] +---- +bird: Bird = null // <1> +bird2: Bird? = null // <2> +---- +<1> throws `Type mismatch: Expected a value of type Bird, but got null` +<2> succeeds + +The only class types that admit `null` values despite not ending in `?` are `Any` and `Null`. +(`Null` is not very useful as a type because it _only_ admits `null` values.) +`Any?` and `Null?` are equivalent to `Any` and `Null`, respectively. + +[[generic-types]] +==== Generic Types + +The following class types are _generic types_: + +* `Pair` +* `Collection` +* `List` +* `Set` +* `Map` +* `Container` + +A generic type has constituent types written in angle brackets (`<>`): + +[source%parsed,{pkl}] +---- +pair: Pair // <1> +coll: Collection // <2> +list: List // <3> +set: Set // <4> +map: Map // <5> +cont: Mapping // <6> +---- + +<1> a pair with first element of type `String` and second element of type `Bird` +<2> a collection of `Bird` elements +<3> a list of `Bird` elements +<4> a set of `Bird` elements +<5> a map with `String` keys and `Bird` values +<6> a container of `Bird` elements + +Omitting the constituent types is equivalent to declaring them as `unknown`: + +[source%parsed,{pkl}] +---- +pair: Pair // equivalent to `Pair` +coll: Collection // equivalent to `Collection` +list: List // equivalent to `List` +set: Set // equivalent to `Set` +map: Map // equivalent to `Map` +cont: Mapping // equivalent to `Mapping` +---- + +The `unknown` type is both a top and a bottom type. +When a static type analyzer encounters an expression of `unknown` type, +it backs off and trusts the user that they know what they are doing. + +[[union-types]] +==== Union Types + +A value of type `A | B`, read "A or B", is either a value of type `A` or a value of type `B`. + +[source%tested,{pkl}] +---- +class Bird { name: String } + +bird1: String|Bird = "Pigeon" +bird2: String|Bird = new Bird { name = "Pigeon" } +---- + +More complex union types can be formed: + +[source%parsed,{pkl}] +---- +foo: List|Bird +---- + +Union types have no implicit default values, but an explicit type can be choosen using a `*` marker: +[source%parsed,{pkl}] +---- +foo: "a"|"b" // undefined. Will throw an error if not amended +bar: "a"|*"b" // default value will be taken from type "b" +baz: "a"|"b" = "a" // explicit value is given +qux: String|*Int // default taken from Int, but Int has no default. Will throw if not amended +---- + +Union types often come in handy when writing schemas for legacy JSON or YAML files. + +[[string-literal-types]] +==== String Literal Types + +A string literal type admits a single string value: + +[source%parsed,{pkl}] +---- +diet: "Seeds" +---- + +While occasionally useful on their own, +string literal types are often combined with <> to form enumerated types: + +[source%parsed,{pkl}] +---- +diet: "Seeds"|"Berries"|"Insects" +---- + +To reuse an enumerated type, introduce a type alias: + +[source%parsed,{pkl}] +---- +typealias Diet = "Seeds"|"Berries"|"Insects" +diet: Diet +---- + +The Java and Kotlin code generators turn type aliases for enumerated types into enum classes. + +[[nothing-type]] +==== Nothing Type + +The `nothing` type is the bottom type of Pkl's type system, the counterpart of top type `Any`. + +The bottom type is assignment-compatible with every other type, and no other type is assignment-compatible with it. + +Being assignment-compatible with every other type may sound too good to be true, but there is a catch -- the `nothing` type has no values! + +Despite being a lonely type, `nothing` has practical applications. +For example, it is used in the standard library's `TODO()` method: + +[source%tested,{pkl}] +---- +function TODO(): nothing = throw("TODO") +---- + +A `nothing` return type indicates that a method never returns normally but always throws an error. + +[[unknown-type]] +==== Unknown Type + +The `unknown` type footnote:[Also known as _dynamic type_. We do not use that term to avoid confusion with `Dynamic`, Pkl's dynamic object type.] is ``nothing``'s even stranger cousin: it is both a top and bottom type! +This makes `unknown` assignment-compatible with every other type, and every other type assignment-compatible with `unknown`. + +When a static type analyzer encounters a value of `unknown` type, +it backs off and trusts the code's author to know what they are doing -- for example, whether a method called on the value exists. + +==== Progressive Disclosure + +In the spirit of link:{uri-progressive-disclosure}[progressive disclosure], type annotations are optional in Pkl. +Omitting a type annotation is equivalent to specifying type `unknown`: + +[source%parsed,{pkl}] +---- +lifespan = 42 // <1> +map: Map // <2> +function say(name) = name // <3> +---- +<1> shorthand for `lifespan: unknown = 42` + (As a dynamically typed language, Pkl does not try to statically infer types.) +<2> shorthand for `map: Map = Map()` +<3> shorthand for `function say(name: unknown): unknown = name` + +[[default-values]] +==== Default Values + +Type-annotated properties have implicit "empty" default values depending on their type: + +[source%tested,{pkl}] +---- +class Bird + +coll: Collection // = List() <1> +list: List // = List() <2> +set: Set // = Set() <3> +map: Map // = Map() <4> +listing: Listing // = new Listing { default = (index) -> new Bird {} } <5> +mapping: Mapping // = new Mapping { default = (key) -> new Bird {} } <6> +obj: Bird // = new Bird {} <7> +nullable: Bird? // = Null(new Bird {}) <8> +union: *Bird|String // = new Bird {} <9> +stringLiteral: "Pigeon" // = "Pigeon" <10> +nullish: Null // = null <11> +---- + +<1> Properties of type `Collection` default to the empty list. +<2> Properties of type `List` default to the empty list. +<3> Properties of type `Set` default to the empty set. +<4> Properties of type `Map` default to the empty map. +<5> Properties of type `Listing` default to an empty listing whose default element is the default for `Y`. +<6> Properties of type `Mapping` default to an empty mapping whose default value is the default for `Y`. +<7> Properties of non-external class type `X` default to `new X {}`. +<8> Properties of type `X?` default to `Null(x)` where `x` is the default for `X`. +<9> Properties with a union type have no default value. By prefixing one of the types in a union with a `*`, the default of that type is chosen as the default for the union. +<10> Properties with a string literal type default to the type's only value. +<11> Properties of type `Null` default to `null`. + +See <> for further information. + +Properties of the following types do not have implicit default values: + +* `abstract` classes, including `Any` and `NotNull` +* Union types, unless an explicit default is given by prefixing one of the types with `*`. +* `external` (built-in) classes, including: +** `String` +** `Boolean` +** `Int` +** `Float` +** `Duration` +** `DataSize` +** `Pair` +** `Regex` + +Accessing a property that neither has an (implicit or explicit) default value nor has been overridden throws an error: + +[source%tested%error,{pkl}] +---- +name: String +---- + +[[type-constraints]] +==== Type Constraints + +A type may be followed by a comma-separated list of _type constraints_ enclosed in round brackets (`()`). +A type constraint is a boolean expression that must hold for the annotated element. +Type constraints enable advanced runtime validation that goes beyond the capabilities of static type checking. + +[source%tested,{pkl}] +---- +class Bird { + name: String(length >= 3) // <1> + parent: String(this != name) // <2> +} + +pigeon: Bird = new { + name = "Pigeon" + parent = "Pigeon Sr." // <3> +} +---- +<1> Restricts `name` to have at least three characters. +<2> The name of the bird (`this`) should not be the same as the name of the `parent`. +<3> Note how `parent` is different from `name`. If they were the same, we would be thrown a contraint error. + +In the following example, we define a `Bird` with a name of only two characters. + +[source%tested%error,{pkl}] +---- +pigeon: Bird = new { + // fails the constraint because [name] is less than 3 characters + name = "Pi" +} +---- + +Boolean expressions are convenient for ad-hoc type constraints. +Alternatively, type constraints can be given as lambda expressions accepting a single argument, namely the value to be validated. +This allows to abstract over and reuse type constraints. + +[source%tested,{pkl}] +---- +class Project { + local emailAddress = (str) -> str.matches(Regex(#".+@.+"#)) + email: String(emailAddress) +} + +project: Project = new { + email = "projectPigeon@example.com" +} +---- + +[source%tested%error,{pkl}] +---- +project: Project = new { + // fails the constraint because `"projectPigeon-example.com"` doesn't match the regular expression. + email = "projectPigeon-example.com" +} +---- + +===== Composite Type Constraints + +A composite type can have type constraints for the overall type, its constituent types, or both. + +[source%tested,{pkl}] +---- +class Project { + local emailAddress = (str) -> str.matches(Regex(#".+@.+"#)) + // constrain the nullable type's element type + type: String(contains("source"))? + // constrain the map type and its key/value types + contacts: Map(length <= 5) +} + +project: Project = new { + type = "open-source" + contacts = Map("Pigeon", "pigeon@example.com") +} +---- + +[[anonymous-functions]] +=== Anonymous Functions + +An _anonymous function_ is a function without a name. + +Most modern general-purpose programming languages support anonymous functions, +under names such as _lamba expressions_, _arrow functions_, _function literals_, _closures_, or _procs_. + +Anonymous functions have their own literal syntax: + +[source] +---- +() -> expr // <1> +(param) -> expr // <2> +(param1, param2, ..., paramN) -> expr // <3> +---- +<1> Zero-parameter lambda expression +<2> Single-parameter lambda expression +<3> Multi-parameter lambda expression + +Here is an example: + +[source%tested,{pkl-expr}] +---- +(n) -> n * 3 +---- + +This anonymous function accepts a parameter named `n`, multiplies it by 3, and returns the result. + +Anonymous functions are values of type link:{uri-stdlib-Function}[Function], more specifically +link:{uri-stdlib-Function0}[Function0], link:{uri-stdlib-Function1}[Function1], +link:{uri-stdlib-Function2}[Function2], link:{uri-stdlib-Function3}[Function3], +link:{uri-stdlib-Function4}[Function4], or link:{uri-stdlib-Function5}[Function5]. +They cannot have more than five parameters. + +To invoke an anonymous function, call its link:{uri-stdlib-Function1-apply}[apply] method: + +[source%tested,{pkl-expr}] +---- +((n) -> n * 3).apply(4) // 12 +---- + +Many standard library methods accept anonymous functions: + +[source%tested,{pkl-expr}] +---- +List(1, 2, 3).map((n) -> n * 3) // List(3, 6, 9) +---- + +Anonymous functions can be assigned to properties, thereby giving them a name: + +[source%tested,{pkl}] +---- +add = (a, b) -> a + b +added = add.apply(2, 3) +---- + +[TIP] +==== +If an anonymous function is not intended to be passed as value, it is customary to declare a method instead: + +[source%tested,{pkl}] +---- +function add(a, b) = a + b +added = add(2, 3) +---- +==== + +An anonymous function's parameters can have type annotations: + +[source%tested,{pkl-expr}] +---- +(a: Number, b: Number) -> a + b +---- + +Applying this function to arguments not of type `Number` results in an error. + +Anonymous functions are _closures_: They can access members defined in a lexically enclosing scope, even after leaving that scope: + +[source%tested,{pkl}] +---- +a = 42 +addToA = (b: Number) -> a + b +list = List(1, 2, 3).map(addToA) // List(43, 44, 45) +---- + +Single-parameter anonymous functions can also be applied with the `|>` (pipe) operator, +which expects a function argument to the left and an anonymous function to the right. +The pipe operator works especially well for chaining multiple functions: + +[source%tested,{pkl}] +---- +mul3 = (n) -> n * 3 +add2 = (n) -> n + 2 + +num = 4 + |> mul3 + |> add2 + |> mul3 // <1> +---- +<1> result: `42` + +Like methods, anonymous functions can be recursive: + +[source%tested,{pkl}] +---- +factor = (n: Number(isPositive)) -> if (n < 2) n else n * factor.apply(n - 1) +num = factor.apply(5) // <1> +---- +<1> result: `120` + +[[mixins]] +==== Mixins + +A mixin is an anonymous function used to apply the same modification to different objects. + +Even though mixins are regular functions, they are best created with object syntax: + +[source%tested,{pkl}] +---- +withDiet = new Mixin { + diet = "Seeds" +} +---- + +Mixins can optionally specify which type of object they apply to: + +[source%tested,{pkl}] +---- +class Bird { diet: String } + +withDietTyped = new Mixin { + diet = "Seeds" +} +---- + +For properties with type annotation, the shorthand `new { ... }` syntax can be used: + +[source%tested,{pkl}] +---- +withDietTyped: Mixin = new { + diet = "Seeds" +} +---- + +To apply a mixin, use the `|>` (pipe) operator: + +[source%tested,{pkl}] +---- +pigeon { + name = "Pigeon" +} +pigeonWithDiet = pigeon |> withDiet + +barnOwl { + name = "Barn owl" +} +barnOwlWithDiet = barnOwl |> withDiet +---- + +`withDiet` can be generalized by turning it into a factory method for mixins: +[source%tested,{pkl}] +---- +function withDiet(_diet: String) = new Mixin { + diet = _diet +} +seedPigeon = pigeon |> withDiet("Seeds") +MiceBarnOwl = barnOwl |> withDiet("Mice") +---- + +Mixins can themselves be modified with <>. + +[[function-amending]] +==== Function Amending + +An anonymous function that returns an object can be amended with the same syntax as that object. +The result is a new function that accepts the same number of parameters as the original function, +applies the original function to them, and amends the returned object. + +Function amending is a special form of function composition. +Thanks to function amending, link:{uri-stdlib-Listing-default}[Listing.default] +and link:{uri-stdlib-Mapping-default}[Mapping.default] can be treated as if they were objects, +only gradually revealing their true (single-parameter function) nature: + +[source%tested,{pkl}] +---- +birds = new Mapping { + default { // <1> + diet = "Seeds" + } + ["Pigeon"] { // <2> + lifespan = 8 + } +} +---- +<1> Amends the `default` function, which returns a default mapping value given a mapping key, and sets property `diet`. +<2> Implicitly applies the amended `default` function and amends the returned object with property `lifespan`. + +The result is a mapping whose entry `"Pigeon"` has both `diet` and `lifespan` set. + +When amending an anonymous function, it is possible to access its parameters +by declaring a comma-separated, arrow (`->`) terminated parameter list after the opening curly brace (`{`). + +Once again, this is especially useful to configure a listing's or mapping's `default` function: + +[source%tested,{pkl}] +---- +birds = new Mapping { + default { key -> // <1> + name = key + } + ["Pigeon"] {} // <2> + ["Barn owl"] {} // <3> +} +---- +<1> Amends the `default` function and sets the `name` property to the mapping entry's key. + To access the `default` function's key parameter, it is declared with `key ->`. + (Any other parameter name could be chosen, but `key` is customary for default functions.) +<2> Defines a mapping entry with key `"Pigeon"` +<3> Defines a mapping entry with key `"Barn owl"` + +The result is a mapping with two entries `"Pigeon"` and `"Barn owl"` whose `name` properties are set to their keys. + +Function amending can also be used to refine <>. + +[[amend-null]] +=== Amending Null Values + +It's time to lift a secret: The predefined `null` value is just one of potentially many values of type `Null`. + +First, here are the technical facts: + +* Null values are constructed with `pkl.base#Null()`. +* `Null(x)` constructs a null value that is equivalent to `x` when amended. +In other words, `Null(x) { ... }` is equivalent to `x { ... }`. +* All null values are equal according to `==`. + +We say that `Null(x)` is a "null value with default x". +But what is it useful for? + +==== +Null values with default are used to define properties that are null ("switched off") by default but have a default value once amended ("switched on"). +==== + +Here is an example: + +.template.pkl +[source%tested,{pkl}] +---- +// we don't have a pet yet, but already know that it is going to be a bird +pet = Null(new Dynamic { + animal = "bird" +}) +---- + +[source%parsed,{pkl}] +---- +amends "template.pkl" + +// We got a pet, let's fill in its name +pet { + name = "Parry the Parrot" +} +---- + +A null value can be switched on without adding or overriding a property: + +[source%parsed,{pkl}] +---- +amends "template.pkl" + +// We do not need to name anything if we have no pet yet +pet {} +---- + +The predefined `null` value is defined as `Null(new Dynamic {})`. +In other words, amending `null` is equivalent to amending `Dynamic {}` (the empty dynamic object): + +[source%tested,{pkl}] +---- +pet = null +---- + +[source%tested,{pkl}] +---- +pet { + name = "Parry the Parrot" +} +---- + +In most cases, the `Null()` method is not used directly. +Instead, it is used under the hood to create implicit defaults for properties with nullable type: + +.template.pkl +[source%tested,{pkl}] +---- +class Pet { + name: String + animal: String = "bird" +} + +// defaults to `Null(Pet {})` +pet: Pet? +---- + +[source%tested,{pkl}] +---- +amends "template.pkl" + +pet { + name = "Perry the Parrot" +} +---- + +The general rule is: A property with nullable type `X?` defaults to `Null(x)` if type `X` has default value `x`, and to `null` if `X` has no default value. + +[[when-generators]] +=== When Generators + +`when` generators conditionally generate object members. +They come in two variants: + +. `when () { }` +. `when () { } else { }` + +The following code conditionally generates properties `hobby` and `idol`: +[source%tested,{pkl}] +---- +isSinger = true + +parrot { + lifespan = 20 + when (isSinger) { + hobby = "singing" + idol = "Frank Sinatra" + } +} +---- + +`when` generators can have an `else` part: + +[source%tested,{pkl}] +---- +isSinger = false + +parrot { + lifespan = 20 + when (isSinger) { + hobby = "singing" + idol = "Aretha Franklin" + } else { + hobby = "whistling" + idol = "Wolfgang Amadeus Mozart" + } +} +---- + +Besides properties, `when` generators can generate elements and entries: + +[source%tested,{pkl}] +---- +abilities { + "chirping" + when (isSinger) { + "singing" // <1> + } + "whistling" +} + +abilitiesByBird { + ["Barn owl"] = "hooing" + when (isSinger) { + ["Parrot"] = "singing" // <2> + } + ["Parrot"] = "whistling" +} +---- +<1> conditional element +<2> conditional entry + +[[for-generators]] +=== For Generators + +`for` generators generate object members in a loop. +They come in two variants: + +. `for ( in ) { }` +. `for (, in ) { }` + +The following code generates a `birds` object containing three elements. +Each element is an object with properties `name` and `lifespan`. + +[source%tested,{pkl}] +---- +names = List("Pigeon", "Barn owl", "Parrot") + +birds { + for (_name in names) { + new { + name = _name + lifespan = 42 + } + } +} +---- + +The following code generates a `birdsByName` object containing three entries. +Each entry is an object with properties `name` and `lifespan` keyed by name. + +[source%tested,{pkl}] +---- +namesAndLifespans = Map("Pigeon", 8, "Barn owl", 15, "Parrot", 20) + +birdsByName { + for (_name, _lifespan in namesAndLifespans) { + [_name] { + name = _name + lifespan = _lifespan + } + } +} +---- + +The following types are iterable: + +|=== +|Type |Key |Value + +|`IntSeq` +|element index (`Int`) +|element value (`Int`) + +|`List` +|element index (`Int`) +|element value (`Element`) + +|`Set` +|element index (`Int`) +|element value (`Element`) + +|`Map` +|entry key (`Key`) +|entry value (`Value`) + +|`Listing` +|element index (`Int`) +|element value (`Element`) + +|`Mapping` +|entry key (`Key`) +|entry value (`Value`) + +|`Dynamic` +|element index (`Int`) + +entry key + +property name (`String`) +|element value + +entry value + +property value +|=== + +Indices are zero based. +Note that `for` generators can generate elements and entries but not properties.footnote:[More precisely, they cannot generate properties with a non-constant name.] + +[[spread-syntax]] +=== Spread Syntax (`\...`) + +Spread syntax generates object members from an iterable value. + +There are two variants of spread syntax, a non-nullable variant and a nullable variant. + +1. `\...` +2. `\...?` + +Spreading an xref:objects[`Object`] (one of `Dynamic`, `Listing` and `Mapping`) will unpack all of its members into the enclosing object footnote:[Values that are xref:typed-objects[`Typed`] are not iterable.]. +Entries become entries, elements become elements, and properties become properties. + +[source%tested,{pkl}] +---- +entries1 { + ["Pigeon"] = "Piggy the Pigeon" + ["Barn owl"] = "Barney the Barn owl" +} + +entries2 { + ...entries1 // <1> +} + +elements1 { 1; 2 } + +elements2 { + ...elements1 // <2> +} + +properties1 { + name = "Pigeon" + diet = "Seeds" +} + +properties2 { + ...properties1 // <3> +} +---- +<1> Spreads entries `["Pigeon"] = "Piggy the Pigeon"` and `["Barn owl"] = "Barney the Barn owl"` +<2> Spreads elements `1` and `2` +<3> Spreads properties `name = "Pigeon"` and `diet = "Seeds"` + +Spreading all other iterable types generates members determined by the iterable. +The following table describes how different iterables turn into object members: + +|=== +|Iterable type|Member type + +| `Map` +| Entry + +| `List` +| Element + +| `Set` +| Element + +| `IntSeq` +| Element +|=== + +These types can only be spread into enclosing objects that support that member type. +For example, a `List` can be spread into a `Listing`, but cannot be spread into a `Mapping`. + +In some ways, spread syntax can be thought of as a shorthand for a xref:for-generators[for generator]. One key difference is that spread syntax can generate properties, which is not possible with a for generator. + +[NOTE] +==== +Look out for duplicate key conflicts when using spreads. +When spreading entries or properties, it is possible that a spread causes conflict due to an existing definition of a key. + +In the following code snippet, `"Pigeon"` is declared twice in the `newPets` object, and thus is an error. + +[source%tested%error,{pkl}] +---- +oldPets { + ["Pigeon"] = "Piggy the Pigeon" + ["Parrot"] = "Perry the Parrot" +} + +newPets { + ...cast + ["Pigeon"] = "Toby the Pigeon" // <1> +} +---- +<1> Error: Duplicate definition of member `"Pigeon"`. +==== + +==== Nullable spread + +A non-nullable spread (`\...`) will error if the value being spread is `null`. + +In contrast, a nullable spread (`\...?`) is syntactic sugar for wrapping a spread in a xref:when-generators[`when`]. + +The following two snippets are logically identical. + +[source%parsed,{pkl}] +---- +result { + ...?myValue +} +---- + +[source%parsed,{pkl}] +---- +result { + when (myValue != null) { + ...myValue + } +} +---- + +[[member-predicates]] +=== Member Predicates (`[[...]]`) + +Occasionally it is useful to configure all object members matching a predicate. +This is especially true when configuring elements, which—unlike entries—cannot be accessed by key: + +[source%tested,{pkl}] +---- +environmentVariables { // <1> + new { name = "PIGEON"; value = "pigeon-value" } + new { name = "PARROT"; value = "parrot-value" } + new { name = "BARN OWL"; value = "barn-owl-value" } +} + +updated = (environmentVariables) { + [[name == "PARROT"]] { // <2> + value = "new-value" // <3> + } +} +---- +<1> a listing of environment variables +<2> amend element(s) whose name equals "PARROT" + +(`name` is shorthand for `this.name`) +<3> update value to "new-value" + +The predicate, enclosed in double brackets (`\[[...]]`), is matched against each member of the enclosing object. +Within the predicate, `this` refers to the member that the predicate is matched against. +Matching members are amended (`{ ... }`) or overridden (`= `). + +[[glob-patterns]] +=== Glob Patterns + +Resources and modules may be imported at the same time by globbing with the <> and <> features. + +Pkl's glob patterns mostly follow the rules described by link:{uri-glob-7}[glob(7)], with the following differences: + +* `*` includes names that start with a dot (`.`). +* `+++**+++` behaves like `*`, except it also matches directory boundary characters (`/`). +* Named character classes are not supported. +* Collating symbols are not supported. +* Equivalence class expressions are not supported. +* Support for <> (patterns within `{` and `}`) are added. + +Here is a full specification of how globs work: + +==== Wildcards + +The following tokens denote wildcards: + +[cols="1,2"] +|=== +|Wildcard |Meaning + +|`*` +|Match zero or more characters, until a directory boundary (`/`) is reached. + +|`**` +|Match zero or more characters, crossing directory boundaries. + +|`?` +|Match a single character. + +|`[...]` +|Match a single character represented by this <>. +|=== + +NOTE: Unlike globs within shells, the `*` wildcard includes names that start with a dot (`.`). + +[[character-classes]] +==== Character Classes + +Character classes are sequences delimited by the `[` and `]` characters, and represent a single +character as described by the sequence within the enclosed brackets. +For example, the pattern `[abc]` means "a single character that is a, b, or c". + +Character classes may be negated using `!`. +For example, the pattern `[!abc]` means "a single character that is not a, b, nor c". + +Character classes may use the `-` character to denote a range. +The pattern `[a-f]` is equivalent to `[abcdef]`. +If the `-` character exists at the beginning or the end of a character class, it does not carry any special meaning. + +Within a character class, the characters `{`, `}`, `\`, `*`, and `?` do not have any special meaning. + +A character class is not allowed to be empty. +Thus, if the first character within the character class is `]`, it treated literally and not as the closing delimiter of the character class. +For example, the glob pattern `[]abc]` matches a single character that is either `]`, `a`, `b`, or `c`. + +[[glob-sub-patterns]] +==== Sub-patterns + +Sub-patterns are glob patterns delimited by the `{` and `}` characters, and separated by the `,` character. +For example, the pattern `{pigeon,parrot}` will match either `pigeon` or `parrot`. + +Sub-patterns cannot be nested. The pattern `{foo,{bar,baz}}` is not a valid glob pattern, and an error will be thrown during evaluation. + +==== Escapes + +The escape character (`\`) can be used to remove the special meaning of a character. The following escapes are valid: + +* `\[` +* `\*` +* `\?` +* `\\` +* `\{` + +All other escapes are considered a syntax error and an error is thrown. + +TIP: If incorporating escape characters into a glob pattern, use <> to express the glob pattern. For example, `+++import*(#"\{foo.pkl"#)+++`. This way, the backslash is interpreted as a backslash and not a string escape. + +==== Examples +[cols="1,2"] +|=== +|Pattern |Description + +|`*.pc[lf]` +|Anything suffixed by `.pkl`, or `.pcf`. + +|`**.y{a,}ml` +|Anything suffixed by either `yml` or `yaml`, crossing directory boundaries. + +|`birds/{\*.yml,*.json}` +|Anything within the `birds` subdirectory that ends in `.yml` or `.json`. This pattern is equivalent to `birds/*.{yml,json}`. + +|`a?*.txt` +|Anything starting with `a` and at least one more letter, and suffixed with `.txt`. + +|`modulepath:/**.pkl` +|All Pkl files in the module path. +|=== + +[[quoted-identifiers]] +=== Quoted Identifiers + +An identifier is the name part of an entity in Pkl. +Entities that are named by identifiers include classes, properties, typealiases, and modules. +For example, `class Bird` has the identifier `Bird`. + +Normally, an identifier must conform to Unicode's {uri-unicode-identifier}[UAX31-R1-1 syntax], with the additions of `_` and `$` permitted as identifier start characters. +Additionally, an identifier cannot clash with a keyword. + +To define an identifier that is otherwise illegal, enclose them in backticks. +This is called a _quoted identifier_. + +[source,{pkl}] +---- +`A Bird's First Flight Time` = 5.s +---- + +[NOTE] +==== +Backticks are not part of a quoted identifier's name, and surrounding an already legal identifier with backticks is redundant. + +[source,{pkl}] +---- +`number` = 42 // <1> +res1 = `number` // <2> +res2 = number // <3> +---- +<1> Equivalent to `number = 42` +<2> References property `{backtick}number{backtick}` +<3> Also references property `{backtick}number{backtick}` +==== + +[[doc-comments]] +=== Doc Comments + +Doc comments are the user-facing documentation of a module and its members. +They consist of one or more lines starting with a triple slash (`///`). +Here is a doc comment for a module: + +[source%tested,{pkl}] +---- +/// An aviated animal going by the name of [bird](https://en.wikipedia.org/wiki/Bird). +/// +/// These animals live on the planet Earth. +module com.animals.Birds +---- + +Doc comments are written in Markdown. +The following Markdown features are supported: + +* all link:{uri-common-mark}[CommonMark] features +* https://help.github.com/articles/organizing-information-with-tables[GitHub flavored Markdown tables] + +[NOTE] +==== +Plaintext URLs are only rendered as links when enclosed in angle brackets: + +[source,{pkl}] +---- +/// A link is *not* generated for https://example.com. +/// A link *is* generated for . +---- +==== + +Doc comments are consumed by humans reading source code, the _Pkldoc_ documentation generator, code generators, and editor/IDE plugins. +They are programmatically accessible via the link:{uri-stdlib-reflectModule}/[pkl.reflect] Pkl API and link:{uri-pkl-core-ModuleSchema}[ModuleSchema] Java API. + +[TIP] +.Doc Comment Style Guidelines +==== +* Use proper spelling and grammar. +* Start each sentence on a new line and capitalize the first letter. +* End each sentence with a punctuation mark. +* The first paragraph of a doc comment is its _summary_. + Keep the summary short (a single sentence is common) + and insert an empty line (`///`) before the next paragraph. +==== + +Doc comments can be attached to module, class, type alias, property, and method declarations. +Here is a comprehensive example: + +.Birds.pkl +[source%tested,{pkl}] +---- +/// An aviated animal going by the name of [bird](https://en.wikipedia.org/wiki/Bird). +/// +/// These animals live on the planet Earth. +module com.animals.Birds + +/// A bird living on Earth. +/// +/// Has [name] and [lifespan] properties and an [isOlderThan()] method. +class Bird { + /// The name of this bird. + name: String + + /// The lifespan of this bird. + lifespan: UInt8 + + /// Tells if this bird is older than [bird]. + function isOlderThan(bird: Bird): Boolean = lifespan > bird.lifespan +} + +/// An adult [Bird]. +typealias Adult = Bird(lifespan >= 2) + +/// A common [Bird] found in large cities. +pigeon: Bird = new { + name = "Pigeon" + lifespan = 8 +} + +/// Creates a [Bird] with the given [_name] and lifespan `0`. +function Infant(_name: String): Bird = new { name = _name; lifespan = 0 } +---- + +[[member-links]] +==== Member Links + +To link to a member declaration, write the member's name enclosed in square brackets (`[]`): + +[source,{pkl}] +---- +/// A common [Bird] found in large cities. +---- + +To customize the link text, insert the desired text, enclosed in square brackets, before the member name: + +[source,{pkl}] +---- +/// A [common Bird] found in large cities. +---- + +Custom link text can use markup: + +[source,{pkl}] +---- +/// A [*common* Bird] found in large cities. +---- + +The short link `[Bird]` is equivalent to `[{backtick}Bird{backtick}][Bird]`. + +Member links are resolved according to Pkl's normal name resolution rules. +The syntax for linking to the members of _Birds.pkl_ (see above) is as follows: + +Module:: +* `[module]` (from same module) +* `[Birds]` (from a module that contains `import "Birds.pkl"`) + +Class:: +* `[Bird]` (from same module) +* `[Birds.Bird]` (from a module that contains `import "Birds.pkl"`) + +Type Alias:: +* `[Adult]` (from same module) +* `[Birds.Adult]` (from a module that contains `import "Birds.pkl"`) + +Class Property:: +* `[name]` (from same class) +* `[Bird.name]` (from same module) +* `[Birds.Bird.name]` (from a module that contains `import "Birds.pkl"`) + +Class Method:: +* `[greet()]` (from same class) +* `[Bird.greet()]` (from same module) +* `[Birds.Bird.greet()]` (from a module that contains `import "Birds.pkl"`) + +Class Method Parameter:: +* `[bird]` (from same method) + +Module Property:: +* `[pigeon]` (from same module) +* `[Birds.pigeon]` (from a module that contains `import "Birds.pkl"`) + +Module Method:: +* `[isPigeon()]` (from same module) +* `[Birds.isPigeon()]` (from a module that contains `import "Birds.pkl"`) + +Module Method Parameter:: +* `[bird]` (from same method) + +Members of `pkl.base` can be linked to by their simple name: + +[source,{pkl}] +---- +/// Returns a [String]. +---- + +Module-level members can be prefixed with `module.` to resolve name conflicts: + +[source,{pkl}] +---- +/// See [module.pigeon]. +---- + +To exclude a member from documentation and code completion, annotate it with `@Unlisted`: + +[source%parsed,{pkl}] +---- +@Unlisted +pigeon: Bird +---- + +The following member links are marked up as code but not rendered as links:footnote:[Only applies to links without custom link text.] + +* `[null]`, `[true]`, `[false]`, `[this]`, `[unknown]`, `[nothing]` +* self-links +* subsequent links to the same member from the same doc comment +* links to a method's own parameters + +Nevertheless, it is a good practice to use member links in the above cases. + +[[name-resolution]] +=== Name Resolution + +Consider this snippet of code buried deep inside a config file: + +[source%parsed,{pkl}] +---- +a = x + 1 +---- + +The call site's "variable" syntax reveals that `x` refers to a _LAMP_ +(let binding, anonymous function parameter, method parameter, or property) definition. But which one? + +To answer this question, Pkl follows these steps: + +. Search the lexically enclosing scopes of `x`, + starting with the scope in which `x` occurs and continuing outwards + up to and including the enclosing module's top-level scope, for a LAMP definition named `x`. + If a match is found, this is the answer. +. Search the `pkl.base` module for a top-level definition of property `x`. + If a match is found, this is the answer. +. Search the <> of `this`, from bottom to top, for a definition of property `x`. + If a match is found, this is the answer. +. Throw a "name `x` not found" error. + +NOTE: Pkl's LAMP name resolution is inspired by link:{uri-newspeak}[Newspeak]. +The goal is for name resolution to be stable with regard to changes in external modules. +This is why lexically enclosing scopes are searched before the prototype chain of `this`, +and why the prototype chains of lexically enclosing scopes are not searched, +which sometimes requires the use of `outer.` or `module.`. +For name resolution to fully stabilize, the list of top-level properties defined in `pkl.base` needs to be freezed. +This is tentatively planned for Pkl 1.0. + +Consider this snippet of code buried deep inside a config file: + +[source%parsed,{pkl}] +---- +a = x("foo") + 1 +---- + +The call site's method call syntax reveals that `x` refers to a method definition. But which one? + +To answer this question, Pkl follows these steps: + +. Search the call sites' lexically enclosing scopes, + starting with the scope in which the call site occurs and continuing outwards + up to and including the enclosing module's top-level scope, for a definition of method `x`. + If a match is found, this is the answer. +. Search the `pkl.base` module for a top-level definition of method `x`. + If a match is found, this is the answer. +. Seach the class inheritance chain of `this`, starting with the class of `this` + and continuing upwards until and including class `Any`, for a method named `x.` + If a match is found, this is the answer. +. Throw a "method `x` not found" error. + +NOTE: Pkl does not support arity or type based method overloading. +Hence, the argument list of a method call is irrelevant for method resolution. + +[[prototype-chain]] +==== Prototype Chain + +Pkl's object model is based on link:{uri-prototypical-inheritance}[prototypical inheritance]. +The prototype chain of objectfootnote:[An instance of `Listing`, `Mapping`, `Dynamic`, or (a subclass of) `Typed`.] `x` contains, from bottom to top: + +1. The chain of objects amended to create `x`, ending in `x` itself, in reverse order.footnote:[All objects in this chain are instances of the same class, + except when a direct conversion between listing, mapping, dynamic, and typed object has occurred. + For example, `Typed.toDynamic()` returns a dynamic object that amends a typed object.] +2. The prototype of the class of the top object in (1). + If no amending took place, this is the class of `x`. +3. The prototypes of the superclasses of (2). + +The prototype of class `X` is an instance of `X` that defines the defaults for properties defined in `X`. +Its direct ancestor in the prototype chain is the prototype of the superclass of `X`. + +The prototype of class `Any` sits at the top of every prototype chain. +To reduce the chance of naming collisions, `Any` does not define any property names.footnote:[Method resolution searches the class inheritance rather than prototype chain.] + +Consider the following code: + +[source%tested,{pkl}] +---- +one = new Dynamic { name = "Pigeon" } +two = (one) { lifespan = 8 } +---- + +The prototype chain of object `two` contains, now listed from top to bottom: + +. The prototype of class `Any`. +. The prototype of class `Dynamic`. +. `one` +. `two` + +Consider the following code: + +[source%tested,{pkl}] +---- +abstract class Named { + name: String +} +class Bird extends Named { + lifespan: Int = 42 +} +one = new Bird { name = "Pigeon" } +two = (one) { lifespan = 8 } +---- + +The prototype chain of object `two` contains, listed from top to bottom: + +. The prototype of class `Any`. +. The prototype of class `Typed`. +. The prototype of class `Named`. +. The prototype of class `Bird`. +. `one` +. `two` + +===== Non-object Values + +The prototype chain of non-object value `x` contains, from bottom to top: + +1. The prototype of the class of `x`. +2. The prototypes of the superclasses of (1). + +For example, the prototype chain of value `42` contains, now listed from top to bottom: + +. The prototype of class `Any`. +. The prototype of class `Number`. +. The prototype of class `Int`. + +A prototype chain never contains a non-object value, such as `42`. + +[[grammar-definition]] +=== Grammar Definition + +Pkl's link:{uri-antlr4}[ANTLR 4] grammar is defined in link:{uri-github-PklLexer}[PklLexer.g4] and link:{uri-github-PklParser}[PklParser.g4]. + +[[reserved-keywords]] +=== Reserved keywords + +The following keywords are reserved in the language. +They cannot be used as a regular identifier, and currently do not have any meaning. + +* `protected` +* `override` +* `record` +* `delete` +* `match` +* `case` +* `switch` +* `vararg` +* `const` + +To use these names in an identifier, <>. + +[[blank-identifiers]] +=== Blank Identifiers + +Blank identifiers can be used in many places to ignore parameters and variables. + +`_` is not a valid identifier. In order to use it as a parameter or variable name +it needs to be enclosed in backticks: +`_`+. + +==== Functions and methods + +[source%tested,{pkl}] +---- +birds = List("Robin", "Swallow", "Eagle", "Falcon") +indexes = birds.mapIndexed((i, _) -> i) + +function constantly(_, second) = second +---- + +==== For generators + +[source%tested,{pkl}] +---- +birdColors = Map("Robin", "blue", "Eagle", "white", "Falcon", "red") + +birds = new Listing { + for (name, _ in birdColors) { + name + } +} +---- + +==== Let bindings + +[source%tested,{pkl}] +---- +name = let (_ = trace("defining name")) "Eagle" +---- + +==== Object bodies + +[source%tested,{pkl}] +---- +birds = new Dynamic { + default { _ -> + species = "Bird" + } + ["Falcon"] {} + ["Eagle"] {} +} +---- + +[[projects]] +=== Projects + +A _project_ is a directory of Pkl modules, and other resources. +It is defined by the presence of a `PklProject` file, that amends standard library module +`pkl:Project`. + +Defining a project serves the following purposes: + +1. It allows defining common evaluator settings for Pkl modules within a logical project. +2. It helps with managing <> dependencies for Pkl modules within a logical project. +3. It enables packaging and sharing the contents of the project as a <>. +4. It allows importing packages via dependency notation. + +[[project-dependencies]] +==== Dependencies + +A project is useful for managing <> dependencies. + +Within a PklProject file, dependencies can be defined: + +.PklProject +[source%tested,{pkl}] +---- +amends "pkl:Project" + +dependencies { + ["birds"] { // <1> + uri = "package://example.com/birds@1.0.0" + } +} +---- +<1> Declare dependency on `package://example.com/birds@1.0.0` with simple name "birds". + +These dependencies can then be imported by their simple name. +This syntax is called _dependency notation_. + +Example: + +[source,{pkl}] +---- +import "@birds/Bird.pkl" // <1> + +pigeon: Bird = new { + name = "Pigeon" +} +---- +<1> Dependency notation; imports path `/Bird.pkl` within dependency `package://example.com/birds@1.0.0` + +NOTE: Internally, Pkl assigns URI scheme `projectpackage` to project dependencies imported using dependency notation. + +When the project gets published as a _package_, these names and URIs are preserved as the package's dependencies. + +[[resolving-dependencies]] +==== Resolving Dependencies + +Dependencies that are declared in a `PklProject` file must be _resolved_ via CLI command xref:pkl-cli:index.adoc#command-project-resolve[`pkl project resolve`]. +This builds a single dependency list, resolving all transitive dependencies and determines appropriate version for each package. +It creates or updates a file called `PklProject.deps.json` in the project's root directory with the list of resolved dependencies. + +When resolving version conflicts, the CLI will pick the latest link:{uri-semver}[semver] minor version of each package. +For example, if the project declares a dependency on package A at `1.2.0`, and a package transitively declares a dependency on package A at `1.3.0`, version `1.3.0` is selected. + +In short, the algorithm has the following steps: + +1. Gather a list of all dependencies, either directly declared or transitive. +2. For each dependency, keep only the newest minor version. + +The resolve command is idempotent; given a PklProject file, it always produces the same set of resolved dependencies. + +NOTE: This algorithm is adapted from Go's link:{uri-mvs-build-list}[minimum version selection]. + +==== Creating a Package + +Projects enable the creation of a <>. +To create a package, the `package` section of a `PklProject` module must be defined. + +.PklProject +[source,pkl] +---- +amends "pkl:Project" + +package { + name = "mypackage" // <1> + baseUri = "package://example.com/\(name)" // <2> + version = "1.0.0" // <3> + packageZipUrl = "https://example.com/\(name)/\(name)@\(version).zip" // <4> +} +---- +<1> The display name of the package. For display purposes only. +<2> The package URI, without the version part +<3> The version of the package +<4> The URL that the package's ZIP file will available for download at. + +The package itself is created by command xref:pkl-cli:index.adoc#command-project-package[`pkl project package`]. + +This command only prepares artifacts to be published. +Once the artifacts are prepared, they are expected to be uploaded to an HTTPS server such that the ZIP asset can be downloaded at path `packageZipUrl`, and the metadata can be downloaded at `+https://+`. + +==== Local dependencies + +A project can depend on a local project as a dependency. +This can be useful for: + +* Structuring a monorepo that publishes multiple packages. +* Temporarily testing out library changes when used within another project. + +To specify a local dependency, import its `PklProject` file. +The imported `PklProject` _must_ have a package section defined. + +.birds/PklProject +[source,{pkl}] +---- +amends "pkl:Project" + +dependencies { + ["fruit"] = import("../fruit/PklProject") // <1> +} + +package { + name = "birds" + baseUri = "package://example.com/birds" + version = "1.8.3" + packageZipUrl = "https://example.com/birds@\(version).zip +} +---- +<1> Specify relative project `../fruit` as a dependency. + +.fruit/PklProject +[source,{pkl}] +---- +amends "pkl:Project" + +package { + name = "fruit" + baseUri = "package://example.com/fruit" + version = "1.5.0" + packageZipUrl = "https://example.com/fruit@\(version).zip +} +---- + +From the perspective of project `birds`, `fruit` is just another package. +It can be imported using dependency notation, i.e. `import "@fruit/Pear.pkl"`. +At runtime, it will resolve to relative path `../fruit/Pear.pkl`. + +When packaging projects with local dependencies, both the project and its dependent project must be passed to the xref:pkl-cli:index.adoc#command-project-package[`pkl project package`] command. diff --git a/docs/modules/language-tutorial/images/pclHubRio.png b/docs/modules/language-tutorial/images/pclHubRio.png new file mode 100644 index 00000000..4268766f Binary files /dev/null and b/docs/modules/language-tutorial/images/pclHubRio.png differ diff --git a/docs/modules/language-tutorial/pages/01_basic_config.adoc b/docs/modules/language-tutorial/pages/01_basic_config.adoc new file mode 100644 index 00000000..a544ce40 --- /dev/null +++ b/docs/modules/language-tutorial/pages/01_basic_config.adoc @@ -0,0 +1,317 @@ += Basic Configuration +include::ROOT:partial$component-attributes.adoc[] + +In this first part of xref:index.adoc[the Pkl tutorial], you build familiarity with Pkl syntax and basic structure. +You also learn different ways to invoke Pkl to produce different formats. + +== Basic values + +Consider the following example Pkl file. + +[source,{pkl}] +.intro.pkl +---- +name = "Pkl: Configure your Systems in New Ways" +attendants = 100 +isInteractive = true +amountLearned = 13.37 +---- + +Running Pkl on this file gives + +[source,shell] +---- +$ pkl eval /Users/me/tutorial/intro.pkl +name = "Pkl: Configure your Systems in New Ways" +attendants = 100 +isInteractive = true +amountLearned = 13.37 +---- + +It may seem nothing happened. +However, Pkl tells you that it _accepts the input_. +In other words, you now know that `intro.pkl` does not contain any errors. + +You can ask Pkl to print this configuration in a different format, using the `-f` option. +For example, JSON: + +[source,shell] +---- +$ pkl eval -f json /Users/me/tutorial/intro.pkl +{ + "name": "Pkl: Configure your Systems in New Ways", + "attendants": 100, + "isInteractive": true, + "amountLearned": 13.37 +} +---- + +Or _PropertyList_ format: + +[source,shell] +---- +$ pkl eval -f plist /Users/me/tutorial/intro.pkl + + + + + name + Pkl: Configure your Systems in New Ways + attendants + 100 + isInteractive + + amountLearned + 13.37 + + +---- + +Notice that Pkl generated ``, ``, `` and `` for the values in your configuration. +This means it has _both_ correctly derived the types of the literal values _and_ translated those types to the corresponding elements in the PropertyList. +xref:03_writing_a_template.adoc[Part III] goes into types in more detail. + + +== Structure: Classes, objects, modules + +A configuration often requires more than just basic values. +Typically, you need some kind of (hierarchical) structure. +Pkl provides _immutable objects_ for this. + +Objects have three kinds of members: properties, elements and entries. +First, look at the syntax for objects and their members. + +=== Properties + +[source,{pkl}] +.simpleObjectWithProperties.pkl +---- +bird { // <1> + name = "Common wood pigeon" // <2> + diet = "Seeds" + taxonomy { // <3> + species = "Columba palumbus" + } +} +---- +<1> This _defines_ `bird` to be an object +<2> For primitive values, Pkl has the `=` syntax (more on this later). +<3> Just like `bird {`, but to show that objects can be nested. + +This defines an object called `bird` with three _named properties_: `name`, `diet`, and `taxonomy`. +The first two of these are strings, but `taxonomy` is another object. +This means properties in an object can have different types and objects can be nested. + +=== Elements + +Of course, you don't always have names for every individual structure in your configuration. +What if you want "just a bunch of things" without knowing how many? +Pkl offers _elements_ for this purpose. +Elements are object members, just like properties. +Where you index properties by their name, you index elements by an integer. +You can think of an object that only contains elements as _array_. +Much like arrays in many languages, you can use square brackets to access an element, for example, `myObject[42]`. + +You write an element, by writing only an expression. +Pkl derives the index from the number of elements already in the object. +For example: + +[source,{pkl}] +.simpleObjectsWithElements.pkl +---- +exampleObjectWithJustIntElements { + 100 // <1> + 42 +} + +exampleObjectWithMixedElements { + "Bird Breeder Conference" + (2000 + 23) // <2> + exampleObjectWithJustIntElements // <3> +} +---- +<1> When you write only the value (without a name), you describe an _element_. +<2> Elements don't have to be literal values; they can be arbitrary _expressions_. +<3> Elements can really be _any_ value, not just primitive values. + +=== Entries + +Objects can have one more kind of member; _entries_. +Like a _property_, an _entry_ is "named" (technically _keyed_). +Unlike a property, the name does not need to be known at declaration time. +Of course, we need a syntax to tell entries apart from properties. +You write entry "names" by enclosing them in square brackets ("names" is quoted, because the names do not need to be strings; any value can index entries). + +[source,{pkl}] +.simpleObjectsWithEntries.pkl +---- +pigeonShelter { + ["bird"] { // <1> + name = "Common wood pigeon" + diet = "Seeds" + taxonomy { + species = "Columba palumbus" + } + } + ["address"] = "355 Bird St." // <2> +} + +birdCount { + [pigeonShelter] = 42 // <3> +} +---- +<1> The difference with properties is the notation of the key: `[]`. +<2> As with properties, entries can be primitive values or objects. +<3> Any object can be used as a key for an entry. + + +=== Mixed members + +In the examples so far, you have seen objects with properties, object with elements and object with entries. +These object members can be freely mixed. + +[source,{pkl}] +.mixedObject.pkl +---- +mixedObject { + name = "Pigeon" + lifespan = 8 + "wing" + "claw" + ["wing"] = "Not related to the _element_ \"wing\"" + 42 + extinct = false + [false] { + description = "Construed object example" + } +} +---- + +Notice, how properties (`name`, `lifespan` and `extinct`), elements (`"wing"`, `"claw"`, `42`) and entries (`"wing"`, `false`) are mixed together in this one object. +You don't have to order them by kind, and you don't require (other) special syntax. + +=== Collections + +This free-for-all mixing of object members can become confusing. +Also, target formats are often considerably more restrictive. +In the following example, you see what happens when you try to produce JSON from `mixedObject`: + +[source,shell] +---- +$ pkl eval -f json /Users/me/tutorial/mixedObject.pkl +–– Pkl Error –– +Cannot render object with both properties/entries and elements as JSON. +Object: "Pigeon" + +89 | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/0.24.0/stdlib/base.pkl#L90) +---- + +This is why Pkl has two special types of object, namely _listings_, which contain _exclusively_ elements, and _mappings_, which contain _exclusively_ entries. +Both listings and mappings _are_ "just objects," so, they don't require syntax besides that of objects: + +[source,{pkl}] +.collections.pkl +---- +birds { // <1> + "Pigeon" + "Parrot" + "Barn owl" + "Falcon" +} + +habitats { // <2> + ["Pigeon"] = "Streets" + ["Parrot"] = "Parks" + ["Barn owl"] = "Forests" + ["Falcon"] = "Mountains" +} +---- +<1> A listing containing four elements. +<2> A mapping containing four entries. + +[NOTE] +==== +_Technically_, the correct way to define `birds` and `habitats` is by using `new Listing {...}` and `new Mapping {...}` explicitly. +You will see what these mean in part xref:03_writing_a_template.adoc[three] of this tutorial. +==== + +When you render _this_ configuration as JSON, everything works: + +[source,json] +---- +{ + "birds": [ + "Pigeon", + "Parrot", + "Barn owl", + "Falcon" + ], + "habitats": { + "Pigeon": "Streets", + "Parrot": "Parks", + "Barn owl": "Forests", + "Falcon": "Mountains" + } +} +---- + +Notice particularly, that you rendered the listing as a JSON _array_. +When you index the listing with an integer, you're referring to the element inside the listing at the corresponding position (starting from `0`). +For example: + +[source,{pkl}] +.indexedListing.pkl +---- +birds { + "Pigeon" + "Parrot" + "Barn owl" + "Falcon" +} + +relatedToSnowOwl = birds[2] +---- +results in +[source,{pkl}] +---- +birds { + "Pigeon" + "Parrot" + "Barn owl" + "Falcon" +} +relatedToSnowOwl = "Barn owl" +---- + +== Exercises + +1. Given the following JSON snippet (taken from W3C examples), write the `.pkl` file that produces this JSON: + ++ +[source,json] +---- +{ + "name": "Common wood pigeon", + "lifespan": 8, + "friends": { + "bird1": "Parrot", + "bird2": "Albatross", + "bird3": "Falcon" + } +} +---- + +2. For some reason, we decide we no longer need the birdX names of the different birds; we just need them as an array. + Change your solution to the previous question to produce the following JSON result: + ++ +[source,json] +---- +{ + "name": "Common wood pigeon", + "lifespan": 8, + "birds": ["Parrot", "Barn owl", "Falcon"] +} +---- diff --git a/docs/modules/language-tutorial/pages/02_filling_out_a_template.adoc b/docs/modules/language-tutorial/pages/02_filling_out_a_template.adoc new file mode 100644 index 00000000..ddbbb497 --- /dev/null +++ b/docs/modules/language-tutorial/pages/02_filling_out_a_template.adoc @@ -0,0 +1,438 @@ += Filling out a Template +include::ROOT:partial$component-attributes.adoc[] + +In this second part of xref:index.adoc[the Pkl tutorial], you will learn how to write one (part of a) configuration in terms of another. +You will also find and fill out an existing _template_. + +== Composing configurations +=== Amending + +The central mechanism in Pkl for expressing one (part of a) configuration in terms of another is _amending_. +Consider the following example. + +[source,{pkl}] +.amendingObjects.pkl +---- +bird { + name = "Pigeon" + diet = "Seeds" + taxonomy { + kingdom = "Animalia" + clade = "Dinosauria" + order = "Columbiformes" + } +} + +parrot = (bird) { + name = "Parrot" + diet = "Berries" + taxonomy { + order = "Psittaciformes" + } +} +---- + +Parrot and Pigeon have nearly identical properties. +They only differ in their name and taxonomy, so if you have already written out `bird`, you can say that `parrot` is just like `pigeon` except `name` is `"Parrot"`, diet is `"Berries"` the `taxonomy.order` is `"Psittaciformes"`. +When you run this, Pkl expands everything fully. + +[source,{pkl}] +---- +bird { + name = "Common wood pigeon" + diet = "Seeds" + taxonomy { + kingdom = "Animalia" + clade = "Dinosauria" + order = "Columbiformes" + } +} +parrot { + name = "Parrot" + diet = "Berries" + taxonomy { + kingdom = "Animalia" + clade = "Dinosauria" + order = "Psittaciformes" + } +} +---- + +[IMPORTANT] +==== +_Amending_ does not allow us to _add_ properties to the (typed) object we are amending. +The xref:03_writing_a_template.adoc[next part of the tutorial] discusses types in more detail. +There, you see that amending _never changes the type_ of the object. +==== + +You can also amend nested objects. +This allows you to only describe the difference with the outermost object for arbitrarily deeply nested structures. +Consider the following example. + +[source,{pkl}] +.nestedAmends.pkl +---- +woodPigeon { + name = "Common wood pigeon" + diet = "Seeds" + taxonomy { + species = "Columba palumbus" + } +} + +stockPigeon = (woodPigeon) { + name = "Stock pigeon" + taxonomy { // <1> + species = "Columba oenas" + } +} + +dodo = (stockPigeon) { // <2> + name = "Dodo" + extinct = true // <3> + taxonomy { + species = "Raphus cucullatus" + } +} +---- +<1> This amends `species`, _as it occurs in_ `stockPigeon`. +<2> Amended objects can, themselves, be amended. +<3> New fields can be added to objects when amending. + +Notice how you only have to change `taxonomy.species`. +In this example, `bird.taxonomy` has `kingdom`, `clade`, `order` and `species`. +You are amending `stockPigeon`, to define `woodPigeon`. +They have the same `taxonomy`, except for `species`. +This notation says that everything in `taxonomy` should be what it is in the object you are amending (`stockPigeon`), except for `species`, which should be `"Columba palumbus"` . + +For the input above, Pkl produces the following output. +[source,{pkl}] +---- +woodPigeon { + name = "Common wood pigeon" + diet = "Seeds" + taxonomy { + species = "Columba palumbus" + } +} +stockPigeon { + name = "Stock pigeon" + diet = "Seeds" + taxonomy { + species = "Columba oenas" + } +} +dodo { + name = "Dodo" + diet = "Seeds" + extinct = true + taxonomy { + species = "Raphus cucullatus" + } +} +---- + +So far, you have only amended _properties_. +Since you refer to them by name, it makes sense that you "overwrite" the value from the object you're amending. +What if you include _elements_ or _entries_ in an amends expression? + +[source,{pkl}] +.amendElementsAndEntries.pkl +---- +favoriteFoods { + "red berries" + "blue berries" + ["Barn owl"] { + "mice" + } +} + +adultBirdFoods = (favoriteFoods) { + [1] = "pebbles" // <1> + "worms" // <2> + ["Falcon"] { // <3> + "insects" + "amphibians" + } + ["Barn owl"] { // <4> + "fish" + } +} +---- +<1> Explicitly amending _by index_ replaces the element at that index. +<2> Without explicit indices, Pkl can't know which element to overwrite, so, instead, it _adds_ an element to the object you're amending. +<3> When you write "new" entries (using a key that does not occur in the object you're amending), Pkl also _adds_ them. +<4> When you write an entry using a key that exists, this notation amends its value. + +Pkl can't know which of the `favoriteFoods` to overwrite only by their _value_. +When you want to _replace_ an element, you have to explicitly amend the element at a specific index. +This is why a "plain" element in an amends expression is _added_ to the object being amended. +Result: + +[source,{pkl}] +---- +favoriteFoods { + ["Barn owl"] { + "mice" + } + "red berries" + "blue berries" +} +adultBirdFoods { + ["Barn owl"] { + "mice" + "fish" + } + "red berries" + "pebbles" + ["Falcon"] { + "insects" + "amphibians" + } + "worms" +} +---- + + +=== Modules + +A `.pkl` file describes a _module_. +Modules are objects that can be referred to from other modules. +Going back to the example above, you can write `parrot` as a separate module. + +[source,{pkl}] +.pigeon.pkl +---- +name = "Common wood pigeon" +diet = "Seeds" +taxonomy { + species = "Columba palumbus" +} +---- + +You can `import` this module and express `parrot` like you did before. + +[source,{pkl}] +.parrot.pkl +---- +import "pigeon.pkl" // <1> + +parrot = (pigeon) { + name = "Great green macaw" + diet = "Berries" + species { + species = "Ara ambiguus" + } +} +---- +<1> Importing `foo.pkl` creates the object `foo`, so you can refer to `pigeon` in this code, like you did before. + +If you run Pkl on both, you will see that it works. +Looking at the result, however, you see a (possibly) unexpected difference. + +[source,{pkl}] +---- +$ pkl eval /Users/me/tutorial/pigeon.pkl +name = "Common wood pigeon"" +diet = "Seeds" +taxonomy { + species = "Columba palumbus" +} + +$ pkl eval /Users/me/tutorial/parrot.pkl +parrot { + name = "Great green macaw" + diet = "Berries" + taxonomy { + species = "Ara ambiguus" + } +} +---- + +The object `pigeon` is "spread" in the top-level, while `parrot` is a nested and named object. +This is because writing `parrot {...}` defines an object property _in_ the "current" module. + +In order to say that "this module is an object, amended from the `pigeon` module," you use an _amends clause_. + +[source,{pkl}] +.parrot.pkl +---- +amends "pigeon.pkl" // <1> + +name = "Great green macaw" +---- +<1> "This" module is the same as `"pigeon.pkl"`, except for what is in the remainder of the file. + +[NOTE] +==== +As a first intuition, think of "amending a module" as "filling out a form." +==== + +== Amending templates + +A Pkl file can be either a _template_ or a _"normal" module_. +This terminology describes the _intended use_ of the module and doesn't imply anything about its structure. +In other words: just by looking at Pkl code, you can't tell whether it is a template or a "normal" module. + +[source,{pkl}] +.acmecicd.pkl +---- +module acmecicd + +class Pipeline { + name: String(nameRequiresBranchName)? + + hidden nameRequiresBranchName = (_) -> + if (branchName == null) + throw("Pipelines that set a 'name' must also set a 'branchName'.") + else true + + branchName: String? +} + +timeout: Int(this >= 3) + +pipelines: Listing + +output { + renderer = new YamlRenderer {} +} +---- + +Remember that amending is like filling out a form. +That's exactly what you're doing here; you're filling out "work order forms". + +Next, add a time-out of one minute for your job. + +[source,{pkl}] +.cicd.pkl +---- +amends "acmecicd.pkl" + +timeout = 1 +---- +Unfortunately, Pkl does not accept this configuration and provides a rather elaborate error message: +[source,plain] +---- +–– Pkl Error –– // <1> +Type constraint `this >= 3` violated. // <2> +Value: 1 // <3> + +225 | timeout: Int(this >= 3)? // <4> + ^^^^^^^^^ +at acmecicd#timeout (file:///Users/me/tutorial/acmecicd.pkl, line 8) + +3 | timeout = 1 // <5> + ^ +at cicd#timeout (file:///Users/me/tutorial/cicd.pkl, line 3) + +90 | text = renderer.renderDocument(value) // <6> + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/e4d8c882d/stdlib/base.pkl#L90) + +---- +<1> Pkl found an error. +<2> Which error Pkl found. +<3> What the offending value is. +<4> Where Pkl found its expectation (line 8 of the amended module). +<5> Where Pkl found the offending value (line 3 of the input module). +<6> What Pkl evaluated to discover the error. + +When Pkl prints source locations, it also prints clickable links for easy access. +For local files, it generates a link for your development environment (https://pkl-lang.org/main/current/pkl-cli/index.html#settings-file[configurable in `+~/.pkl/settings.pkl+`]). +For packages imported from elsewhere, if available, Pkl produces `https://` links to their repository. + +Pkl complains about a _type constraint_. +Pkl's type system doesn't just protect you from providing a `String` where you expected an `Int`, it even checks which _values_ are allowed. +In this case, the minimum time-out is _three_ minutes. +If you change the value to `3`, Pkl accepts your configuration. + +[source, shell] +---- +$ pkl eval cicd.pkl +timeout: 3 +pipelines: [] +---- + +You can now define a pipeline. +Start off by specifying the name of the pipeline and nothing else. + +[source,{pkl}] +.cicd.pkl +---- +amends "acmecicd.pkl" + +timeout = 3 +pipelines { + new { // <1> + name = "prb" + } +} +---- +<1> There is no pipeline object to amend. The `new` keyword gives you an object to amend. + +So far, you've defined objects the same way you amended them. +When the name `foo` didn't occur before, `foo { ... }` _creates_ a property called `foo` and assigns to it the object specified on the `...`. +If `foo` is an existing object, this notation is an _amend expression_; resulting in a new _object_ (value), but _not_ a new (named) property. +Since `pipelines` is a listing, you can _add_ elements by writing expressions in an amend expression. + +In this case, though, there is no object to amend. Writing `myNewPipeline { ... }` defines a _property_, but listings may only include _elements_. +This is where you can use the keyword `new`. + +`new` gives you an object to amend. +Pkl derives from the context in which `new` is used and what the object to amend should look like. +This is called the _default value_ for the context. +xref:03_writing_a_template.adoc[The next part] goes into detail about how Pkl does this. + +Running Pkl on your new configuration produces a verbose error. + +[source,plain] +.cicd.pkl +---- +–– Pkl Error –– +Pipelines that set a 'name' must also set a 'branchName'. + +8 | throw("Pipelines that set a 'name' must also set a 'branchName'.") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at acmecicd#Pipeline.nameRequiresBranchName. (file:///Users/me/tutorial/acmecicd.pkl, line 8) + +6 | name = "prb" + ^^^^^ +at cicd#pipelines[#1].name (file:///Users/me/tutorial/cicd.pkl, line 6) + +90 | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/e4d8c882d/stdlib/base.pkl#L90) + +---- + +You have hit another type constraint, like `timeout: Int(this >= 3)` before. +In this case, the error message consists of an English language sentence, instead of Pkl code. +When constraints are complicated or very application specific, template authors can `throw` a more descriptive error message like this. + +The message is quite instructive, so you can fix the error by adding a `branchName`. + +[source,{pkl}] +.cicd.pkl +---- +amends "acmecicd.pkl" + +timeout = 3 +pipelines { + new { + name = "prb" + branchName = "main" + } +} +---- + +and indeed + +[source,{pkl}] +---- +$ pkl eval -f yml /Users/me/tutorial/cicd.pkl +timeout: 3 +pipelines: +- name: prb + branchName: main +---- diff --git a/docs/modules/language-tutorial/pages/03_writing_a_template.adoc b/docs/modules/language-tutorial/pages/03_writing_a_template.adoc new file mode 100644 index 00000000..cf7024ce --- /dev/null +++ b/docs/modules/language-tutorial/pages/03_writing_a_template.adoc @@ -0,0 +1,441 @@ += Writing a Template +include::ROOT:partial$component-attributes.adoc[] + +In parts xref:01_basic_config.adoc[one] and xref:02_filling_out_a_template.adoc[two], you saw that Pkl provides _validation_ of our configurations. +It checks syntax, types and constraints. +As you saw in the `acmecicd` example xref:02_filling_out_a_template.adoc#amending-templates[here], the template can provide informative error messages when an amending module violates a type constraint. + +In this final part, you will see some of Pkl's techniques that are particularly relevant for writing a template. + +== Basic types + +Pkl always checks the _syntax_ of its input. +As it evaluates your configuration, it also checks _types_. +You've seen objects, listings, and mappings already. +These provide ways to write structured configuration. +Before you can write types for them, you need to know how to write the types for the simplest (unstructured) values. + +These are all Pkl's _basic_ types: + +[source,{pkl}] +.pklTutorialPart3.pkl +---- +name: String = "Writing a Template" + +part: Int = 3 + +hasExercises: Boolean = true + +amountLearned: Float = 13.37 + +duration: Duration = 30.min + +bandwidthRequirementPerSecond: DataSize = 50.mb +---- + +In the above, you've explicitly annotated the code with type signatures. +The default output of Pkl is actually `pcf`, which is a subset of Pkl. +Since `pcf` does not have type signatures, running Pkl on this example removes them. + +[source,shell] +---- +$ pkl eval pklTutorialPart3.pkl +name = "Writing a Template" +part = 3 +hasExercises = true +amountLearned = 13.37 +duration = 30.min +bandwidthRequirementPerSecond = 50.mb +---- + +Note how `Duration` and `DataSize` help you prevent https://en.wikipedia.org/wiki/Mars_Climate_Orbiter[unit errors] in these common (for configuration) domains. + +== Typed objects, properties and amending + +Having a notation for basic types, you can now write _typed objects_. + +[source,{pkl}] +.simpleClass.pkl +---- +class Language { // <1> + name: String +} + +bestForConfig: Language = new { // <2> + name = "Pkl" +} +---- +<1> A class definition. +<2> A property definition, using the `Language` class. + +[NOTE] +==== +Although not required (or enforced), it's customary to name properties starting with a lower-case letter. Class names, by that same convention, start with an upper-case letter. +==== + +You can type objects with _classes_. +In this example, you define a class called `Language`. +You can now be certain that every instance of `Language` has a property `name` with type `String`. + +Types and values are different things in Pkl. +Pkl does not render types in its output,footnote:[Although, some output formats can contain their own form of type annotation. This may be derived from the Pkl type. Type definitions (`class` and `typealias`) themselves are never rendered.] so when you run Pkl on this, you don't see the class _definition_ at all. + +[source,{pkl}] +---- +$ pkl eval simpleClass.pkl +bestForConfig { + name = "Pkl" +} + +---- + +Did you notice that the output doesn't just omit the type signature, but also the `= new`? +We will discuss this further in the next section. + +When your configuration describes a few different parts like this, you can define one instance and amend it for every other instance. + +For example: + +[source,{pkl}] +.pklTutorialParts.pkl +---- +class TutorialPart { + name: String + + part: Int + + hasExercises: Boolean + + amountLearned: Float + + duration: Duration + + bandwidthRequirementPerSecond: DataSize +} + +pklTutorialPart1: TutorialPart = new { + name = "Basic Configuration" + part = 1 + hasExercises = true + amountLearned = 13.37 + duration = 30.min + bandwidthRequirementPerSecond = 50.mib.toUnit("mb") +} + +pklTutorialPart2: TutorialPart = (pklTutorialPart1) { + name = "Filling out a Template" + part = 2 +} + +pklTutorialPart3: TutorialPart = (pklTutorialPart1) { + name = "Writing a Template" + part = 3 +} + +---- + +You can read this as saying "``pklTutorialPart2`` & `pklTutorialPart3` are exactly like `pklTutorialPart1`, except for their `name` and `part`." +Running Pkl confirms this: + +[source,shell] +---- +$ pkl eval pklTutorialParts.pkl +pklTutorialPart1 { + name = "Basic Configuration" + part = 1 + hasExercises = true + amountLearned = 13.37 + duration = 30.min + bandwidthRequirementPerSecond = 50.mb +} +pklTutorialPart2 { + name = "Filling out a Template" + part = 2 + hasExercises = true + amountLearned = 13.37 + duration = 30.min + bandwidthRequirementPerSecond = 50.mb +} +pklTutorialPart3 { + name = "Writing a Template" + part = 3 + hasExercises = true + amountLearned = 13.37 + duration = 30.min + bandwidthRequirementPerSecond = 50.mb +} + +---- + +Sadly, `pklTutorialParts.pkl` is a _rewrite_ of `pklTutorial.pkl`. +It creates a separate `class TutorialPart` and instantiates three properties with it (`pklTutorialPart1`, `pklTutorialPart2` and `pklTutorialPart3`). +In doing so, it implicitly moves everything "down" one level (`pklTutorialPart3` is now a property in the module `pklTutorialParts`, whereas above, in `pklTutorialPart3.pkl` it was its own module). +This is not very DRY. +As a matter of fact, you don't need this rewrite. + +Any `.pkl` file defines a _module_ in Pkl. +Any module is represented by a _module class_, which is an actual Pkl `class`. +A module is not quite the same as any other class, because Pkl never renders class definitions on the output. +However, when you ran Pkl on `pklTutorialPart3.pkl`, it _did_ produce an output. +This is because a module also defines an _instance_ of the module class. + +The values given to properties in a module (or in any "normal" class) are called _default values_. +When you instantiate a class, all the properties for which you _don't_ provide a value are populated from the class' default values. + +In our examples of tutorial parts, only the `name` and `part` varied across instances. +You can express this by adding default values to the (module) class definition. +Instead of starting from a particular tutorial part, you can define the module `tutorialPart` as follows: + +[source,{pkl}] +.TutorialPart.pkl +---- +name: String // <1> + +part: Int // <1> + +hasExercises: Boolean = true // <2> + +amountLearned: Float = 13.37 // <2> + +duration: Duration = 30.min // <2> + +bandwidthRequirementPerSecond: DataSize = 50.mb // <2> +---- +<1> No default value given. +<2> Default value given. + +Running this through Pkl gives an error, or course, because of the missing values: + +[source, shell] +---- +$ pkl eval TutorialPart.pkl +–– Pkl Error –– +Tried to read property `name` but its value is undefined. + +1 | name: String + ^^^^ +... +---- + +An individual part now only has to fill in the missing fields, so you can change `pklTutorialPart3.pkl` to amend this: + +[source,{pkl}] +.pklTutorialPart3.pkl +---- +amends "TutorialPart.pkl" + +name = "Writing a Template" + +part = 3 +---- + +This results in + +[source, shell] +---- +$ pkl eval pklTutorialPart3.pkl +name = "Writing a Template" +part = 3 +hasExercises = true +amountLearned = 13.37 +duration = 30.min +bandwidthRequirementPerSecond = 50.mb + +---- + +This now behaves exactly like our `pklTutorialPart3: TutorialPart = (pklTutorialPart1) {...` before. +`pklTutorialPart3` is now defined as the value we get by amending `tutorialPart` and giving it a `name` and a `part`. + +[IMPORTANT] +==== +Amending anything _never changes its type_. +When we amend an object of type `Foo`, the result will always be precisely of type `Foo`. +By "precisely" we mean, that amending an object also can't "turn it into" an instance of a sub-class of the class of the object being amended. +==== + +== A new template + +Now that you know about types, you can start writing your first template. +So far, you've written configurations with Pkl, either without a template, or using a template on Pkl Hub. +It is often easiest to first write a (typical) configuration for which you want to create a template. +Suppose you want to define what a live workshop for this tutorial looks like. +Consider this example: + +[source,{pkl}] +.workshop2023.pkl +---- +title = "Pkl: Configure your Systems in New Ways" +interactive = true +seats = 100 +occupancy = 0.85 +duration = 1.5.h +`abstract` = """ + With more systems to configure, the software industry is drowning in repetitive and brittle configuration files. + YAML and other configuration formats have been turned into programming languages against their will. + Unsurprisingly, they don’t live up to the task. + Pkl puts you back in control. + """ + +event { + name = "Migrating Birds between hemispheres" + year = 2023 +} + +instructors { + "Kate Sparrow" + "Jerome Owl" +} + +sessions { + new { + date = "8/14/2023" + time = 30.min + } + new { + date = "8/15/2023" + time = 30.min + } +} + +assistants { + ["kevin"] = "Kevin Parrot" + ["betty"] = "Betty Harrier" +} + +agenda { + ["beginners"] { + title = "Basic Configuration" + part = 1 + duration = 45.min + } + ["intermediates"] { + title = "Filling out a Template" + part = 2 + duration = 45.min + } + ["experts"] { + title = "Writing a Template" + part = 3 + duration = 45.min + } +} +---- + +Call your new template `Workshop.pkl`. +Although not required, it's good practice to always name your template with a `module`-clause. +Defining the first few properties are like you saw in the previous section: + +[source,{pkl}] +---- +module Workshop + +title: String + +interactive: Boolean + +seats: Int + +occupancy: Float + +duration: Duration + +`abstract`: String +---- + +Unlike these first few properties, `event` is an object with multiple properties. +To be able to type `event`, you need a `class`. +You've seen before how to define this: + +[source,{pkl}] +---- +class Event { + name: String + + year: Int +} + +event: Event +---- + +Next, `instructors` isn't an object with properties, but a list of unnamed values. +Pkl offers the `Listing` type for this: + + +[source,{pkl}] +---- +instructors: Listing +---- + +`sessions` is a `Listing` of objects, so you need a `Session` class. +[source,{pkl}] +---- +class Session { + time: Duration + + date: String +} + +sessions: Listing +---- + +`assistants` has a structure like an object, in that all the values are named, but the set of names is not fixed for all possible workshops (and some workshops may have more assistants than others). The Pkl type for this is a `Mapping`: + +[source,{pkl}] +---- +assistants: Mapping +---- + +Finally, for every workshop session, there is an `agenda`, which describes which ``TutorialPart``s are covered. +You already defined `TutorialPart.pkl` as its own module, so you should not define a separate class, but rather `import` that module and reuse it here: + +[source,{pkl}] +---- +import "TutorialPart.pkl" // <1> + +agenda: Mapping +---- +<1> This `import` clause brings the name `TutorialPart` into scope, which is the module class as discussed above. Note that import clauses must appear before property definitions. + +Putting it all together, your `Workshop.pkl` template looks like this: + +[source,{pkl}] +.Workshop.pkl +---- +module Workshop + +import "TutorialPart.pkl" + +title: String + +interactive: Boolean + +seats: Int + +occupancy: Float + +duration: Duration + +`abstract`: String + +class Event { + name: String + + year: Int +} + +event: Event + +instructors: Listing + +class Session { + time: Duration + + date: String +} + +sessions: Listing + +assistants: Mapping + +agenda: Mapping +---- diff --git a/docs/modules/language-tutorial/pages/index.adoc b/docs/modules/language-tutorial/pages/index.adoc new file mode 100644 index 00000000..718bfcdc --- /dev/null +++ b/docs/modules/language-tutorial/pages/index.adoc @@ -0,0 +1,17 @@ += Tutorial +include::ROOT:partial$component-attributes.adoc[] + +Welcome to the Pkl tutorial. We will get you up and running quickly! +If you are new to Pkl, we recommend that you follow along with the code examples. +This tutorial describes interactions with the xref:pkl-cli:index.adoc#repl[REPL]. +For an even more interactive experience, follow along using a xref:main:ROOT:tools.adoc[supported editor]. + +For more comprehensive documentation, see xref:language-reference:index.adoc[Language Reference]. +For ready-to-go examples with full source code, see xref:ROOT:examples.adoc[]. +For API documentation, see xref:ROOT:standard-library.adoc[Standard Library]. + +Pick a tutorial by topic: + +1. xref:01_basic_config.adoc[Basic Configuration] +2. xref:02_filling_out_a_template.adoc[Filling out a Template] +3. xref:03_writing_a_template.adoc[Writing a Template] diff --git a/docs/modules/pkl-cli/pages/index.adoc b/docs/modules/pkl-cli/pages/index.adoc new file mode 100644 index 00000000..9ef22298 --- /dev/null +++ b/docs/modules/pkl-cli/pages/index.adoc @@ -0,0 +1,782 @@ += CLI +include::ROOT:partial$component-attributes.adoc[] +:uri-homebrew: https://brew.sh +:uri-pkl-macos-download: {github-releases}/pkl-cli-macos-{pkl-artifact-version}.bin +:uri-pkl-linux-amd64-download: {github-releases}/pkl-cli-linux-amd64-{pkl-artifact-version}.bin +:uri-pkl-linux-aarch64-download: {github-releases}/pkl-cli-linux-aarch64-{pkl-artifact-version}.bin +:uri-pkl-alpine-download: {github-releases}/pkl-cli-alpine-amd64-{pkl-artifact-version}.bin +:uri-pkl-java-download: {github-releases}/pkl-cli-java-{pkl-artifact-version}.jar +:uri-pkl-stdlib-docs-settings: {uri-pkl-stdlib-docs}/settings/ +:uri-pkl-cli-main-sources: {uri-github-tree}/pkl-cli/src/main/kotlin/org/pkl/cli +:uri-pkl-cli-CliEvaluatorOptions: {uri-pkl-cli-main-sources}/CliEvaluatorOptions.kt +:uri-certificates: {uri-github-tree}/pkl-commons-cli/src/main/resources/org/pkl/commons/cli/commands + +The `pkl` command-line interface (CLI) evaluates Pkl modules and writes their output to the console or a file. +For interactive development, the CLI includes a Read-Eval-Print Loop (REPL). + +[[installation]] +== Installation + +The CLI comes in multiple flavors: + +* Native macOS executable for amd64 (tested on macOS 10.15) +* Native Linux executable for amd64 (tested on Oracle Linux 8) +* Native Linux executable for aarch64 (tested on Oracle Linux 8) +* Native Alpine Linux executable for amd64 (cross-compiled and tested on Oracle Linux 8) +* Java executable (tested with Java 8/11/14 on macOS and Oracle Linux) + +On macOS and Linux, we recommend using the native executables. +They are self-contained, start up instantly, and run complex Pkl code much faster than the Java executable. + +.What is the Difference Between the Linux and Alpine Linux Executables? +[NOTE] +==== +The Linux executable is dynamically linked against _glibc_ and _libstdc{plus}{plus}_, +whereas, the Alpine Linux executable is statically linked against _musl libc_ and _libstdc{plus}{plus}_. +==== + +The Java executable works on multiple platforms and has a smaller binary size than the native executables. +However, it requires a Java 8 (or higher) runtime on the system path, has a noticeable startup delay, +and runs complex Pkl code slower than the native executables. + +All flavors are built from the same codebase and undergo the same automated testing. +Except where noted otherwise, the rest of this page discusses the native executables. + +//TODO uncomment this after brew formula is merged and available +// [[homebrew]] +// === Homebrew +// +// Release versions can be installed with {uri-homebrew}[Homebrew]. +// +// ifdef::is-release-version[] +// To install Pkl, run: +// +// [source,shell] +// ---- +// brew install pkl +// ---- +// +// To update Pkl, run: +// +// [source,shell] +// ---- +// brew update +// brew upgrade pkl # or just `brew upgrade` +// ---- +// endif::[] +// +// ifndef::is-release-version[] +// For instructions, switch to a release version of this page. +// endif::[] + +[[download]] +=== Download + +Development and release versions can be downloaded and installed manually. + +=== macOS Executable + +[source,shell] +[subs="+attributes"] +---- +curl -o pkl {uri-pkl-macos-download} +chmod +x pkl +./pkl --version +---- + +This should print something similar to: + +[source,shell] +[subs="+attributes"] +---- +Pkl {pkl-version} (macOS, native) +---- + +[[linux-executable]] +=== Linux Executable + +The Linux executable is dynamically linked against _glibc_ and _libstdc{plus}{plus}_ for the amd64 and aarch64 architectures. +For a statically linked executable, see <>. + +On amd64: + +[source,shell] +[subs="+attributes"] +---- +# on amd64 +curl -o pkl {uri-pkl-linux-amd64-download} +chmod +x pkl +./pkl --version +---- + +On aarch64: + +[source,shell] +[subs="+attributes"] +---- +curl -o pkl {uri-pkl-linux-aarch64-download} +chmod +x pkl +./pkl --version +---- + +This should print something similar to: + +[source,shell] +[subs="+attributes"] +---- +Pkl {pkl-version} (Linux, native) +---- + +[[alpine-linux-executable]] +=== Alpine Linux Executable + +The Alpine Linux executable is statically linked against _musl libc_ and _libstdc{plus}{plus}_. +For a dynamically linked executable, see <>. + +[source,shell] +[subs="+attributes"] +---- +curl -o pkl {uri-pkl-alpine-download} +chmod +x pkl +./pkl --version +---- + +This should print something similar to: + +[source,shell] +[subs="+attributes"] +---- +Pkl {pkl-version} (Linux, native) +---- + +NOTE: We currently do not support the aarch64 architecture for Alpine Linux. + +=== Java Executable + +[source,shell] +[subs="+attributes"] +---- +curl -o jpkl {uri-pkl-java-download} +chmod +x jpkl +./jpkl --version +---- + +This should print something similar to: + +[source,shell] +[subs="+attributes"] +---- +Pkl {pkl-version} (macOS 10.16, Java 11.0.9) +---- + +[[usage]] +== Usage + +*Synopsis:* `pkl [] []` + +For a brief description of available options, run `pkl -h`. + +NOTE: The Java executable is named `jpkl`. + +[[command-eval]] +=== `pkl eval` + +*Synopsis:* `pkl eval [] []` + +Evaluate the given Pkl `` and produce their rendering results. + +:: +The absolute or relative URIs of the modules to evaluate. +Relative URIs are resolved against the working directory. + +==== Options + +[[format]] +.-f, --format +[%collapsible] +==== +Default: (none) + +Example: `yaml` + +The output format to generate. +The default output renderer for a module supports the following formats: + +* `json` +* `jsonnet` +* `pcf` +* `plist` +* `properties` +* `textproto` +* `xml` +* `yaml` + +If no format is set, the default renderer chooses `pcf`. +==== + +[[output-path]] +.-o, --output-path +[%collapsible] +==== +Default: (none) + +Example: "config.yaml" + +The file path where the output file is placed. +Relative paths are resolved against the project directory. + +// suppress inspection "AsciiDocLinkResolve" +This option is mutually exclusive with link:#multiple-file-output-path[`--multiple-file-output-path`]. +If neither option is set, each module's `output.text` is written to standard output. + +If multiple source modules are given, placeholders can be used to map them to different output files. +The following placeholders are supported: + +`%\{moduleDir}`::: +The directory path of the module, relative to the working directory. +Only available when evaluating file-based modules. + +`%\{moduleName}`::: +The simple module name as inferred from the module URI. +For hierarchical URIs such as `+file:///foo/bar/baz.pkl+`, this is the last path segment without file extension. + +`%\{outputFormat}`::: +The requested output format. +Only available if `--format` is set. + +If multiple source modules are mapped to the same output file, their outputs are concatenated. +By default, module outputs are separated with `---`, as in a YAML stream. +// suppress inspection "AsciiDocLinkResolve" +The separator can be customized using the link:#module-output-separator[`--module-output-separator`] option. +==== + +[[module-output-separator]] +.--module-output-separator +[%collapsible] +==== +Default: `---` (as in a YAML stream) + +The separator to use when multiple module outputs are written to the same file, or to standard output. +==== + +[[multiple-file-output-path]] +.-m, --multiple-file-output-path +[%collapsible] +==== +Default: (none) + +Example: "output/" + +The directory where a module's output files are placed. + +Setting this option causes Pkl to evaluate a module's `output.files` property +and write the files specified therein. +Within `output.files`, a key determines a file's path relative to `--multiple-file-output-path`, +and a value determines the file's contents. + +// suppress inspection "AsciiDocLinkResolve" +This option cannot be used together with any of the following: + +* xref:output-path[`--output-path`] +* xref:expression[`--expression`] + +// suppress inspection "AsciiDocLinkResolve" +This option supports the same placeholders as link:#output-path[`--output-path`]. + +Examples: + +[source,shell] +---- +# Write files to `output/` +pkl eval -m output/ myFiles.pkl + +# Write files to the current working directory +pkl eval -m . myFiles.pkl + +# Write foo.pkl's files to the `foo` directory, and bar.pkl's files +# to the `bar` directory +pkl eval -m "%{moduleName}" foo.pkl bar.pkl +---- + +For additional details, see xref:language-reference:index.adoc#multiple-file-output[Multiple File Output] +in the language reference. +==== + +[[expression]] +.-x, --expression +[%collapsible] +==== +Default: (none) + +The expression to be evaluated within the module. + +This option causes Pkl to evaluate the provided expression instead of the module's `output.text` or `output.files` properties. +The resulting value is then stringified, and written to either standard out, or the designated output file. + +For example, consider the following Pkl module: + +.pigeon.pkl +[source%tested,{pkl}] +---- +metadata { + species = "Pigeon" +} +---- + +The following command prints `Pigeon` to the console: + +[source,shell] +---- +pkl -x metadata.name pigeon.pkl +# => Pigeon +---- + +Setting an `--expression` flag can be thought of as substituting the expression in place of a module's `output.text` property. +Running the previous command is conceptually the same as if the below module were evaluated without the `--expression` flag: + +[source,pkl] +---- +metadata { + species = "Pigeon" +} + +output { + text = metadata.name.toString() +} +---- +==== + +This command also takes <>. + +[[command-server]] +=== `pkl server` + +*Synopsys:* `pkl server` + +Run as a server that communicates over standard input/output. + +This option is used for embedding Pkl in an external client, such as xref:swift:ROOT:index.adoc[pkl-swift] or xref:go:ROOT:index.adoc[pkl-go]. + +[[command-test]] +=== `pkl test` + +*Synopsys:* `pkl test [] []` + +Evaluate the given `` as _tests_, producing a test report and appropriate exit code. + +Renderers defined in test files will be ignored by the `test` command. + +:: +The absolute or relative URIs of the modules to test. Relative URIs are resolved against the working directory. + +==== Options + +[[junit-reports]] +.--junit-reports +[%collapsible] +==== +Default: (none) + +Example: `./build/test-results` + +Directory where to store JUnit reports. + +No JUnit reports will be generated if this option is not present. +==== + +[[overwrite]] +.--overwrite +[%collapsible] +==== +Force generation of expected examples. + +The old expected files will be deleted if present. +==== + +This command also takes <>. + +[[command-repl]] +=== `pkl repl` + +*Synopsys:* `pkl repl []` + +Start a REPL session. + +This command takes <>. + +[[command-project-package]] +=== `pkl project package` + +*Synopsis:* `pkl project package ` + +This command prepares a project to be published as a package. +Given a project directory, it creates the following artifacts: + +* `@` - the package metadata file +* `@.sha256` - the dependency metadata file's SHA-256 checksum +* `@.zip` - the package archive +* `@.zip.sha256` - the package archive's SHA-256 checksum + +These artifacts are expected to be published to an HTTPS server, such that the metadata and zip files can be fetched at their expected locations. + +The package ZIP should be available at the `packageZipUrl` location specified in the `PklProject` file +The package metadata should be available at the package URI's derived HTTPS URL. +For example, given package `package://example.com/mypackage@1.0.0`, the metadata file should be published to `+https://example.com/mypackage@1.0.0+`. + +During packaging, this command runs these additional steps: + +1. Run the package's API tests, if any are defined. +2. Validates that if the package has already been published, that the package's metadata is identical. This step can be skipped using the `--skip-publish-check` flag. + +Examples: + +[source,shell] +---- +# Search the current working directory for a project, and package it. +pkl project package + +# Package all projects within the `packages/` directory to `.out`, writing each package's artifacts to its own directory. +pkl project package --output-path ".out/%{name}@%{version}/" packages/*/ +---- + +==== Options + +.--output-path +[%collapsible] +==== +Default: `.out` + +The directory to write artifacts to. +Accepts the following placeholders: + +`%\{name}`:: The name of the package +`%\{version}`:: The version of the package +==== + +.--skip-publish-check +[%collapsible] +==== +Skips checking whether a package has already been published with different contents. + +By default, the packager will check whether a package at the same version has already been published. +If the package has been published, it validates that the package's metadata is identical to the locally generated metadata. +==== + +.--junit-reports +[%collapsible] +==== +Default: (none) + +Example: `./build/test-results` + +Directory where to store JUnit reports. + +No JUnit reports will be generated if this option is not present. +==== + +.--overwrite +[%collapsible] +==== +Force generation of expected examples. + +The old expected files will be deleted if present. +==== + +This command also takes <>. + +[[command-project-resolve]] +=== `pkl project resolve` + +*Synopsis:* `pkl project resolve ` + +This command takes the dependencies of a project, and writes the resolved versions a file at path `PklProject.deps.json`. + +It builds a dependency list, taking the latest minor version in case of version conflicts. +For more details, see the xref:language-reference:index.adoc#resolving-dependencies[resolving dependencies] section of the language reference. + +Examples: +[source,shell] +---- +# Search the current working directory for a project, and resolve its dependencies. +pkl project resolve + +# Resolve dependencies for all projects within the `packages/` directory. +pkl project resolve packages/*/ +---- + +==== Options + +This command accepts <>. + +[[command-download-package]] +=== `pkl download-package` + +*Synopsis*: `pkl download-package ` + +This command downloads the specified packages to the cache directory. +If the +package already exists in the cache directory, this command is a no-op. + +==== Options + +This command accepts <>. + +[[common-options]] +=== Common options + +The <>, <>, <>, <>, <>, and <> commands support the following common options: + +include::../../pkl-cli/partials/cli-common-options.adoc[] + +The <>, <>, <>, and <> commands also take the following options: + +include::../../pkl-cli/partials/cli-project-options.adoc[] + +== Evaluating Modules + +Say we have the following module: + +[[config.pkl]] +.config.pkl +[source,{pkl}] +---- +bird { + species = "Pigeon" + diet = "Seeds" +} +parrot = (bird) { + species = "Parrot" + diet = "Berries" +} +---- + +To evaluate this module and write its output to standard output, run: + +[source,shell] +---- +pkl eval config.pkl +---- + +You should see the following output: + +[source,{pkl}] +---- +bird { + species = "Pigeon" + diet = "Seeds" +} +parrot { + species = "Parrot" + diet = "Berries" +} +---- + +To render output as JSON, YAML, XML property list, or Java properties, +use `--format json`, `--format yaml`, `--format plist`, or `--format properties`, respectively. + +To control the output format from within Pkl code, see xref:language-reference:index.adoc#module-output[Module Output]. + +To read a source module from standard input rather than a file, use `-` as a module name: + +[source,shell] +---- +echo mod2.pkl | pkl eval mod1.pkl - mod3.pkl +---- + +This is especially useful in environments that don't support `/dev/stdin`. + +To write output to a file rather than standard output, use `--output-path some/file.ext`. + +[[batch-evaluation]] +=== Batch Evaluation + +Multiple modules can be evaluated at once: + +[source,shell] +---- +pkl eval config1.pkl config2.pkl config3.pkl +---- + +To write module outputs to separate output files, `--output-path` supports the following placeholders: + +`%\{moduleDir}`:: the directory path of the source module, relative to the working directory (only available for file based modules) +`%\{moduleName}`:: the last path segment of the module URI, without file extension +`%\{outputFormat}`:: the target format (only available if `--format` is set) + +The following run produces three JSON files placed next to the given source modules: + +[source,shell] +---- +pkl eval --format=json --output-path=%{moduleDir}/%{moduleName}.json config1.pkl config2.pkl config3.pkl +---- + +If multiple module outputs are written to the same file, or to standard output, their outputs are concatenated. +By default, module outputs are separated with `---`, as in a YAML stream. +The separator can be customized using the `--module-output-separator` option. + +[[repl]] +== Working with the REPL + +To start a REPL session, run `pkl repl`: + +[source,shell] +[subs="+attributes"] +---- +$ pkl repl +Welcome to Pkl {pkl-version}. +Type an expression to have it evaluated. +Type :help or :examples for more information. + +pkl> +---- + +NOTE: The Java executable is named `jpkl`. + +=== Loading Modules + +To load <> into the REPL, run: + +[source,shell] +---- +pkl> :load config.pkl +---- + +To evaluate the `bird.name` property, run: + +[source,shell] +---- +pkl> bird.name +"Pigeon" +---- + +To evaluate the entire module, force-evaluate `this`: + +[source,shell] +---- +pkl> :force this +---- + +=== REPL Commands + +Commands start with `:` and can be tab-completed: + +[source,shell] +[subs="+attributes,+macros"] +---- +pkl> :{empty}kbd:[Tab] +clear examples force help load quit reset +pkl> :q{empty}kbd:[Tab] +pkl> :quit{empty}kbd:[Return] +$ +---- + +Commands can be abbreviated with any unique name prefix: + +[source,shell] +[subs="+attributes,+macros"] +---- +pkl> :q{empty}kbd:[Return] +$ +---- + +To learn more about each command, run the `:help` command. + +Some commands support further command-specific tab completion. +For example, the `:load` command supports completing file paths. + +With commands out of the way, let's move on to evaluating code. + +=== Evaluating Code + +To evaluate an expression, type the expression and hit kbd:[Return]. + +[source,shell] +---- +pkl> 2 + 4 +6 +---- + +Apart from expressions, the REPL also accepts property, function, and class definitions. +(See the xref:language-reference:index.adoc[Language Reference] to learn more about these language concepts.) + +[source,shell] +---- +pkl> hello = "Hello, World!" +pkl> hello +"Hello, World!" +pkl> function double(n) = 2 * n +pkl> double(5) +10 +pkl> class Bird { name: String } +pkl> new Bird { species = "Pigeon" } +{ + name = ? +} +---- + +Top-level expressions are only supported in the REPL. +In a regular module, every expression is contained in a definition, and only definitions exist at the top level. + +=== Redefining Members + +Existing members can be redefined: + +[source,shell] +---- +pkl> species = "Pigeon" +pkl> species +"Pigeon" +pkl> species = "Barn" +pkl> species +"Barn" +pkl> species += " Owl" +pkl> species +"Barn owl" +---- + +Due to Pkl's late binding semantics, redefining a member affects dependent members: + +[source,shell] +---- +pkl> name = "Barn" +pkl> species = "$name Owl" +pkl> species +"Barn owl" +pkl> name = "Elf" +pkl> species +"Elf Owl" +---- + +Redefining members is only supported in the REPL. Under the hood, +it works as follows: + +* The REPL environment is represented as a synthetic Pkl module. +* When a new member is defined, it is added to the current REPL module. +* When an existing member is redefined, it is added to a new REPL module that xref:language-reference:index.adoc#module-amend[amends] the previous REPL module. + +[[settings-file]] +== Settings File + +The Pkl settings file allows to customize the CLI experience. + +A settings file is a Pkl module amending the `pkl.settings` standard library module. +Its default location is `~/.pkl/settings.pkl`. +To use a different settings file, set the `--settings` command line option, for example `--settings mysettings.pkl`. +To enforce default settings, use `--settings pkl:settings`. +The settings file is also honored by (and configurable through) the Gradle plugin and `CliEvaluator` API. + +Here is a typical settings file: + +.~/.pkl/settings.pkl +[source%parsed,{pkl}] +---- +amends "pkl:settings" // <1> + +editor = Idea // <2> +---- +<1> A settings file should amend the `pkl.settings` standard library module. +<2> Configures IntelliJ IDEA as the preferred editor. +Other supported values are `System`, `GoLand`, `TextMate`, `Sublime`, `Atom`, and `VsCode`. + +With the above settings file in place, kbd:[Cmd]+Double-clicking a source code link in a stack trace opens the corresponding file in IntelliJ IDEA at the correct location. + +To learn more about available settings, see link:{uri-pkl-stdlib-docs-settings}[pkl.settings]. + +[[ca-certs]] +== CA Certificates + +When making TLS requests, Pkl comes with its own set of {uri-certificates}[CA certificates]. +These certificates can be overridden via either of the two options: + +- Set them directly via the CLI option `--ca-certificates `. +- Add them to a directory at path `~/.pkl/cacerts/`. + +Both these options will *replace* the default CA certificates bundled with Pkl. + +The CLI option takes precedence over the certificates in `~/.pkl/cacerts/`. + +Certificates need to be X.509 certificates in PEM format. diff --git a/docs/modules/pkl-cli/partials/cli-common-options.adoc b/docs/modules/pkl-cli/partials/cli-common-options.adoc new file mode 100644 index 00000000..7f31c8d0 --- /dev/null +++ b/docs/modules/pkl-cli/partials/cli-common-options.adoc @@ -0,0 +1,124 @@ +[[allowed-modules]] +.--allowed-modules +[%collapsible] +==== +Default: `pkl:,file:,modulepath:,https:,repl:,package:,projectpackage:` + +Comma-separated list of URI patterns that determine which modules can be loaded and evaluated. +Patterns are matched against the beginning of module URIs. +(File paths have been converted to `file:` URLs at this stage.) +At least one pattern needs to match for a module to be loadable. +Both source modules and transitive modules are subject to this check. +==== + +[[allowed-resources]] +.--allowed-resources +[%collapsible] +==== +Default: `env:,prop:,package:,projectpackage:` + +Comma-separated list of URI patterns that determine which external resources can be read. +Patterns are matched against the beginning of resource URIs. +At least one pattern needs to match for a resource to be readable. +==== + +[[cache-dir]] +.--cache-dir +[%collapsible] +==== +Default: `~/.pkl/cache` + +Example: `/path/to/module/cache/` + +The cache directory for storing packages. +==== + +.--no-cache +[%collapsible] +==== +Disable cacheing of packages. +==== + +.-e, --env-var +[%collapsible] +==== +Default: OS environment variables for the current process + +Example: `MY_VAR=myValue` + +Sets an environment variable that can be read by Pkl code with `read("env:")`. +Repeat this option to set multiple environment variables. +==== + +.-h, --help +[%collapsible] +==== +Display help information. +==== + +.--module-path +[%collapsible] +==== +Default: (empty) + +Example: `dir1:zip1.zip:jar1.jar` + +Directories, ZIP archives, or JAR archives to search when resolving `modulepath:` URIs. +Paths are separated by the platform-specific path separator (`:` on *nix, `;` on Windows). +Relative paths are resolved against the working directory. +==== + +.-p, --property +[%collapsible] +==== +Default: (none) + +Example: `myProp=myValue` + +Sets an external property that can be read by Pkl code with `read("prop:")`. +Repeat this option to set multiple external properties. +==== + +.--root-dir +[%collapsible] +==== +Default: (none) + +Example: `/some/path` + +Root directory for `file:` modules and resources. +If set, access to file-based modules and resources is restricted to those located under the specified root directory. +Any symlinks are resolved before this check is performed. +==== + +.--settings +[%collapsible] +==== +Default: (none) + +Example: `mySettings.pkl` + +File path of the Pkl settings file to use. +If not set, `~/.pkl/settings.pkl` or defaults specified in the `pkl.settings` standard library module are used. +==== + +.-t, --timeout +[%collapsible] +==== +Default: (none) + +Example: `30` + +Duration, in seconds, after which evaluation of a source module will be timed out. +Note that a timeout is treated the same as a program error in that any subsequent source modules will not be evaluated. +==== + +.-v, --version +[%collapsible] +==== +Display version information. +==== + +.-w, --working-dir +[%collapsible] +==== +Base path that relative module paths passed as command-line arguments are resolved against. +Defaults to the current working directory. +==== + +.--ca-certificates +[%collapsible] +==== +Default: (none) + +Example: `/some/path/certificates.pem` + +Path to a file containing CA certificates to be used for TLS connections. + +Setting this option replaces the existing set of CA certificates bundled into the CLI. +Certificates need to be X.509 certificates in PEM format. + +For other methods of configuring certificates, see xref:pkl-cli:index.adoc#ca-certs[CA Certificates]. +==== diff --git a/docs/modules/pkl-cli/partials/cli-project-options.adoc b/docs/modules/pkl-cli/partials/cli-project-options.adoc new file mode 100644 index 00000000..f6d98f33 --- /dev/null +++ b/docs/modules/pkl-cli/partials/cli-project-options.adoc @@ -0,0 +1,26 @@ +[[project-dir]] +.--project-dir +[%collapsible] +==== +Default: (none) + +Example: `/some/path` + +Directory where the project lives. + +A project is a directory that contains a `PklProject` file, which is used to declare package dependencies, as well as common evaluator settings to be applied in the project. + +If omitted, this is determined by searching up from the working directory for a directory that contains a `PklProject` file, until `--root-dir` or the file system root is reached. +==== + +[[omit-project-settings]] +.--omit-project-settings +[%collapsible] +==== +Disables loading evaluator settings from the PklProject file. +==== + +[[no-project]] +.--no-project +[%collapsible] +==== +Disables all behavior related to projects. +==== diff --git a/docs/modules/pkl-core/examples/CoreEvaluatorExample.java b/docs/modules/pkl-core/examples/CoreEvaluatorExample.java new file mode 100644 index 00000000..23ab63f1 --- /dev/null +++ b/docs/modules/pkl-core/examples/CoreEvaluatorExample.java @@ -0,0 +1,26 @@ +import org.pkl.core.Evaluator; +import org.pkl.core.ModuleSource; +import java.util.List; + +import org.pkl.core.PModule; +import org.pkl.core.PObject; +import org.junit.jupiter.api.Test; + +// the pkl/pkl-examples repo has a similar example +@SuppressWarnings({"unchecked", "unused", "ConstantConditions"}) +public class CoreEvaluatorExample { + @Test + public void usage() { + // tag::usage[] + PModule module; + try (var evaluator = + Evaluator.preconfigured()) { // <1> + module = evaluator.evaluate( + ModuleSource.text("pigeon { age = 30; hobbies = List(\"swimming\", \"surfing\") }")); // <2> + } + var pigeon = (PObject) module.get("pigeon"); // <3> + var className = pigeon.getClassInfo().getQualifiedName(); // <4> + var hobbies = (List) pigeon.get("hobbies"); // <5> + // end::usage[] + } +} diff --git a/docs/modules/pkl-core/pages/index.adoc b/docs/modules/pkl-core/pages/index.adoc new file mode 100644 index 00000000..0b82b7ed --- /dev/null +++ b/docs/modules/pkl-core/pages/index.adoc @@ -0,0 +1,127 @@ += pkl-core Library +include::ROOT:partial$component-attributes.adoc[] +:uri-pkl-core-maven-module: {uri-maven-docsite}/artifact/org.pkl-lang/pkl-core +:uri-pkl-core-main-sources: {uri-github-tree}/pkl-core/src/main/java/org/pkl/core +:uri-pkl-core-test-sources: {uri-github-tree}/pkl-core/src/test/java/org/pkl/core +:uri-pkl-core-Evaluator: {uri-pkl-core-main-sources}/Evaluator.java +:uri-pkl-core-PModule: {uri-pkl-core-main-sources}/PModule.java +:uri-pkl-core-PklException: {uri-pkl-core-main-sources}/PklException.java +:uri-pkl-core-ValueVisitor: {uri-pkl-core-main-sources}/ValueVisitor.java +:uri-pkl-core-JsonRenderer: {uri-pkl-core-main-sources}/JsonRenderer.java +:uri-pkl-core-PcfRenderer: {uri-pkl-core-main-sources}/PcfRenderer.java +:uri-pkl-core-PListRenderer: {uri-pkl-core-main-sources}/PListRenderer.java +:uri-pkl-core-YamlRenderer: {uri-pkl-core-main-sources}/YamlRenderer.java +:uri-pkl-core-SecurityManagers: {uri-pkl-core-main-sources}/SecurityManagers.java +:uri-pkl-core-ModuleKeyFactories: {uri-pkl-core-main-sources}/module/ModuleKeyFactories.java + +The _pkl-core_ library contains the Pkl parser, evaluator, REPL server, and xref:ROOT:standard-library.adoc[Standard Library]. +It is the foundation for most of Pkl's other libraries and tools. +The library can also be used to embed Pkl in Java libraries and applications. + +[[pkl-core-installation]] +== Installation + +The _pkl-core_ library is available {uri-pkl-core-maven-module}[from Maven Central]. +It requires Java 11 or higher. + +=== Gradle + +To use the library in a Gradle project, declare the following dependency: + +[tabs] +==== +Groovy:: ++ +.build.gradle +[source,groovy,subs="+attributes"] +---- +dependencies { + compile "org.pkl-lang:pkl-core:{pkl-artifact-version}" +} + +ifndef::is-release-build[] +repositories { + maven { url "{uri-sonatype}" } +} +endif::[] +---- + +Kotlin:: ++ +.build.gradle.kts +[source,kotlin,subs="+attributes"] +---- +dependencies { + compile("org.pkl-lang:pkl-core:{pkl-artifact-version}") +} + +ifndef::is-release-build[] +repositories { + maven { url = uri("{uri-sonatype}") } +} +endif::[] +---- +==== + +=== Maven + +To use the library in a Maven project, declare the following dependency: + +.pom.xml +[source,xml,subs="+attributes"] +---- + + + org.pkl-lang + pkl-core + {pkl-artifact-version} + +ifndef::is-release-build[] + + + sonatype-s01 + Sonatype S01 + {uri-sonatype} + + +endif::[] + +---- + +== Usage + +{uri-pkl-core-Evaluator}[`Evaluator`] is the core evaluator that exposes multiple methods of evaluation. + +The main evaluation method is `evaluate`, which returns a Java representation of the Pkl module object. +If evaluation succeeds, a {uri-pkl-core-PModule}[`PModule`] object representing the fully evaluated module is returned. +Otherwise, an {uri-pkl-core-PklException}[`PklException`] with error details is thrown. + +Let's look at an example: + +[[config-evaluator-core-example]] +[source,java,indent=0] +---- +include::{examplesdir}/CoreEvaluatorExample.java[tags=usage] +---- +<1> Build an `Evaluator` with default configuration. +The evaluator should be closed once it is no longer needed. +In this example, this is done with a try-with-resources statement. +Note that objects returned by the evaluator remain valid after calling `close()`. +<2> Build a `ModuleSource` using the given text as the module's contents. Evaluate the given module source. Alternatively, it's possible to build a `ModuleSource` from a file, path, uri, and other sources. +<3> Get the module's `"pigeon"` property, which is represented as `PObject` in Java. +<4> Get the class name for this object. In this example, the class name is `pkl.base#Dynamic`. +<5> Get pigeon's `"diet"` property, which is represented as `List` in Java. + +[[value-visitor]] +Often, {uri-pkl-core-ValueVisitor}[`ValueVisitor`] is a better way to process a module. +See {uri-pkl-core-PcfRenderer}[`PcfRenderer`], {uri-pkl-core-JsonRenderer}[`JsonRenderer`], {uri-pkl-core-YamlRenderer}[`YamlRenderer`] and {uri-pkl-core-PListRenderer}[`PListRenderer`] for examples. + +[[security-manager-spi]] +The (Pkl, not Java) security manager can be configured and customized using {uri-pkl-core-SecurityManagers}[`SecurityManagers`] and related classes. + +[[module-loader-spi]] +Module loaders can be configured and customized using {uri-pkl-core-ModuleKeyFactories}[`ModuleKeyFactories`] and related classes. + +== Further Information + +Refer to the Javadoc and sources published with the library, or browse the library's {uri-pkl-core-main-sources}[main] and {uri-pkl-core-test-sources}[test] sources. diff --git a/docs/modules/pkl-doc/assets/images/pkldoc-search.gif b/docs/modules/pkl-doc/assets/images/pkldoc-search.gif new file mode 100644 index 00000000..3c20a672 Binary files /dev/null and b/docs/modules/pkl-doc/assets/images/pkldoc-search.gif differ diff --git a/docs/modules/pkl-doc/pages/index.adoc b/docs/modules/pkl-doc/pages/index.adoc new file mode 100644 index 00000000..66fd53fc --- /dev/null +++ b/docs/modules/pkl-doc/pages/index.adoc @@ -0,0 +1,241 @@ += Pkldoc +include::ROOT:partial$component-attributes.adoc[] +:uri-pkl-doc-maven: {uri-maven-docsite}/artifact/org.pkl-lang/pkl-doc +:uri-DocsiteInfo: {uri-pkl-stdlib-docs}/DocsiteInfo/ +:uri-DocPackageInfo: {uri-pkl-stdlib-docs}/DocPackageInfo/ +:uri-CliDocGenerator: {uri-pkl-doc-main-sources}/CliDocGenerator.kt +:uri-DocGenerator: {uri-pkl-doc-main-sources}/DocGenerator.kt + +_Pkldoc_ is a documentation website generator that produces navigable and searchable API documentation for Pkl modules. + +Pkldoc's look and feel is inspired by Scaladoc. +To get a first impression, browse the link:{uri-pkl-stdlib-docs-index}[Standard Library API Docs]. + +== Features + +Pkldoc offers the following features: + +Code navigation:: +Easily navigate between hyperlinked modules, classes, functions, and properties. +Member search:: +Search the entire documentation by member name. +See the next section for details. +Comment folding:: +Expand and collapse multi-paragraph doc comments. +Markdown support:: +Write doc comments in Markdown. +See xref:language-reference:index.adoc#doc-comments[Doc Comments] for details. +Member links:: +Link to other members from your doc comments. +See xref:language-reference:index.adoc#member-links[Member Links] for details. +Member anchors:: +Get a member's deep link by clicking its anchor symbol and copying the URL in the address bar. +Cross-site links:: +Enable cross-site member links simply by providing the URLs of other Pkldoc websites such as the standard library docs. + +[[member-search]] +=== Member Search + +To get a first impression of Pkldoc's member search, let's try and find property `MinFiniteFloat` in the link:{uri-pkl-stdlib-docs-index}[standard library docs]: + +image::pkldoc-search.gif[title="Searching the standard library docs."] + +To start a search, press kbd:[s]. Search results are displayed as you type. + +To limit the search to a particular kind of member, prefix the search term with _m:_ for modules, _c:_ for classes, _f:_ for functions, or _p:_ for properties. +For example, search term _p:min_ finds property `MinFiniteFloat` but not function `min()`. + +Camel case matching is always enabled and does not require capitalizing the search term. +For example, search term _mff_ matches properties `MinFiniteFloat` and `MaxFiniteFloat`. + +Both search terms and member names may contain non-ASCII Unicode characters. +As characters are normalized to their base form, search term _res_ matches `Réseau`. + +The `@AlsoKnownAs` annotation, defined and used throughout the _pkl.base_ module, documents alternative names for a member used in other programming languages or earlier versions of a module. +Pkldoc's search takes these alternative names into account. +For example, searching the standard library docs for _count_ or _size_ finds property `String.length`. +Feel free to use `@AlsoKnownAs` in your own modules. + +Search results are categorized into _exact_ and _other_ (partial) matches. +On module and class pages, additional categories show matches in the same module and class. +Within a category, results are ranked by similarity with the search term. + +To navigate to a search result, either click the result or select it with the up/down arrow keys and press kbd:[Enter]. + +== Installation + +Pkldoc is offered as Gradle plugin, Java library, and CLI. + +=== Gradle Plugin + +See xref:pkl-gradle:index.adoc#installation[Installation] in the _Gradle Plugin_ chapter. + +[[install-library]] +=== Java Library + +The `pkl-doc` library is available {uri-pkl-doc-maven}[from Maven Central]. +It requires Java 11 or higher. + +ifndef::is-release-version[] +NOTE: Snapshots are published to repository `{uri-sonatype}`. +endif::[] + +==== Gradle + +To use the library in a Gradle project, declare the following dependency: + +[tabs] +==== +Groovy:: ++ +.build.gradle +[source,groovy,subs="+attributes"] +---- +dependencies { + compile "org.pkl-lang:pkl-doc:{pkl-artifact-version}" +} + +ifndef::is-release-build[] +repositories { + maven { url "{uri-sonatype}" } +} +endif::[] +---- + +Kotlin:: ++ +.build.gradle.kts +[source,kotlin,subs="+attributes"] +---- +dependencies { + compile("org.pkl-lang:pkl-doc:{pkl-artifact-version}") +} + +ifndef::is-release-build[] +repositories { + maven { url = uri("{uri-sonatype}") } +} +endif::[] +---- +==== + +==== Maven + +To use the library in a Maven project, declare the following dependency: + +.pom.xml +[source,xml,subs="+attributes"] +---- + + + org.pkl-lang + pkl-doc + {pkl-artifact-version} + +ifndef::is-release-build[] + + + sonatype-s01 + Sonatype S01 + {uri-sonatype} + + +endif::[] + +---- + +[[install-cli]] +=== CLI + +The CLI is bundled with the library and does not currently ship as a native executable or a self-contained Jar. +We recommend to provision it with a Maven compatible build tool as shown in <>. + +[[usage]] +== Usage + +The Pkldoc tool is offered as Gradle plugin, Java library, and CLI. +It can generate documentation either for modules directly, or generate documentation for _package uris_. + +The tool requires an argument of a module named `_docsite-info.pkl`, that amends link:{uri-DocsiteInfo}[pkl.DocsiteInfo]. + +[discrete] +==== Generating documentation for modules directly + +Modules can be passed directly to Pkldoc for documentation generation. +When generating documentation for these modules, there must also be a module named _doc-package-info.pkl_ that amends link:{uri-DocPackageInfo}[pkl.DocPackageInfo]. + +The _doc-package-info.pkl_ module defines a _doc package_, which describes how modules are grouped and versioned together. + +When generating documentation for modules, each such module must declare a module name that starts with a package name declared in a _doc-package-info.pkl_ module. +For example, the following are valid module declarations for package _com.example_: + +* `module com.example.Birds` +* `module com.example.Birds.Parrot` + +The part of the module name that comes after the package name +must match the module's relative path in its source code repository. +For example, module _com.example.Bird.Parrot_ is expected to be found at _$sourceCode/Bird/Parrot.pkl_, +where _sourceCode_ is configured in _doc-package-info.pkl_. + +[discrete] +==== Generating documentation for a _package_ + +Pkldoc can alternatively generate documentation for a _package_. +When generating documentation for a package, the URI of the package must be passed as an argument to Pkldoc. +These packages must already be published and downloadable. + +When generating documentation for packages, modules within a package must declare a module name that is prefixed by the package's name declared in the `Package.name` property of its `PklProject` file. +For example, the following are valid module declarations for package `com.example`: + +* `module com.example.Birds` +* `module com.example.Birds.Parrot` + +The part of the module name that comes after the package name +must match the module's relative path in its source code repository. +For example, module _com.example.Bird.Parrot_ is expected to be found at _$sourceCode/Bird/Parrot.pkl_, +where _sourceCode_ is configured in the `Package.sourceCode` property of its `PklProject` file. + +=== Gradle Plugin + +See xref:pkl-gradle:index.adoc#pkldoc-generation[Pkldoc Generation] in the _Gradle Plugin_ chapter. + +=== Java Library + +The Java library offers two APIs: + +* A high-level link:{uri-CliDocGenerator}[CliDocGenerator] API whose feature set corresponds to the CLI. +* A low-level link:{uri-DocGenerator}[DocGenerator] API that offers additional features and control. + +For more information, refer to the Javadoc documentation. + +=== CLI + +As mentioned in <>, the CLI is bundled with the library. +To run the CLI, execute the library Jar or its `org.pkl.doc.Main` class. + +*Synopsis:* `java -cp -jar pkl-doc.jar [] ` + +``:: +The absolute or relative URIs of docsite descriptors, package descriptors, and the modules for which to generate documentation. + +Relative URIs are resolved against the working directory. + +==== Options + +.-o, --output-dir +[%collapsible] +==== +Default: (none) + +Example: `pkldoc` +The directory where generated documentation is placed. +==== + +Common CLI options: + +include::../../pkl-cli/partials/cli-common-options.adoc[] + +[[full-example]] +== Full Example + +For a ready-to-go example with full source code and detailed walkthrough, +see link:{uri-pkldoc-example}[pkldoc] in the _pkl/pkl-examples_ repository. diff --git a/docs/modules/pkl-gradle/pages/index.adoc b/docs/modules/pkl-gradle/pages/index.adoc new file mode 100644 index 00000000..5af2d87c --- /dev/null +++ b/docs/modules/pkl-gradle/pages/index.adoc @@ -0,0 +1,693 @@ += Gradle Plugin +include::ROOT:partial$component-attributes.adoc[] +:uri-pkl-gradle-maven-module: {uri-maven-docsite}/artifact/org.pkl-lang/org.pkl-lang.gradle.plugin +:uri-pkl-gradle-main-sources: {uri-github-tree}/pkl-gradle/src/main/java/org/pkl/gradle +:uri-pkl-gradle-Eval: {uri-pkl-gradle-main-sources}/Eval.java +:uri-pkl-gradle-JavaCodeGen: {uri-pkl-gradle-main-sources}/JavaCodeGen.java +:uri-pkl-gradle-KotlinCodeGen: {uri-pkl-gradle-main-sources}/KotlinCodeGen.java +:uri-pkl-gradle-Pkldoc: {uri-pkl-gradle-main-sources}/Pkldoc.java + +The Gradle plugin offers the following features: + +* <> +* <> +* <> +* <> + +Plugin versions coincide with Pkl versions. +That is, plugin version `x.y.z` uses Pkl version `x.y.z`. + +[[installation]] +== Installation + +The Gradle plugin is available {uri-pkl-gradle-maven-module}[from Maven Central]. +It requires Java 11 or higher and Gradle 6.8 or higher. +Earlier Gradle versions are not supported. + +ifndef::is-release-version[] +NOTE: Snapshots are published to repository `{uri-sonatype}`. +endif::[] + +The plugin is applied as follows: + +[tabs] +==== +Groovy:: ++ +.build.gradle +[source,groovy,subs="+attributes"] +---- +plugins { + id "org.pkl-lang" version "{pkl-artifact-version}" +} +---- ++ +.settings.gradle +[source,groovy,subs="+attributes"] +---- +pluginManagement { + repositories { +ifdef::is-release-build[] + mavenCentral() +endif::[] +ifndef::is-release-build[] + maven { url "{uri-sonatype}" } +endif::[] + } +} +---- + +Kotlin:: ++ +.build.gradle.kts +[source,kotlin,subs="+attributes"] +---- +plugins { + id("org.pkl-lang") version "{pkl-artifact-version}" +} +---- ++ +.settings.gradle.kts +[source,kotlin,subs="+attributes"] +---- +pluginManagement { + repositories { +ifdef::is-release-build[] + mavenCentral() +endif::[] +ifndef::is-release-build[] + maven { url = uri("{uri-sonatype}") } +endif::[] + } +} +---- +==== + +[[module-evaluation]] +== Module Evaluation + +This feature integrates the xref:pkl-cli:index.adoc[Pkl evaluator] into Gradle builds. + +=== Usage + +To add an evaluator to the build, add a named configuration block inside `pkl.evaluators`: + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + evaluators { + evalPkl { + sourceModules.add(file("module1.pkl")) + transitiveModules.from file("module2.pkl") + outputFile = layout.buildDirectory.file("module1.yaml") + outputFormat = "yaml" + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + evaluators { + register("evalPkl") { + sourceModules.add(file("module1.pkl")) + transitiveModules.from(file("module2.pkl")) + outputFile.set(layout.buildDirectory.file("module1.yaml")) + outputFormat.set("yaml") + } + } +} +---- +==== + +To guarantee correct Gradle up-to-date behavior, +`transitiveModules` needs to contain all module files transitively referenced by `sourceModules`. + +For each declared evaluator, the Pkl plugin creates an equally named task. +Hence the above evaluator can be run with: + +[source,shell script] +---- +$ ./gradlew evalPkl +---- + +For a ready-to-go example with full source code, +see link:{uri-build-eval-example}[codegen-java] in the _pkl/pkl-examples_ repository. + +=== Configuration Options + +[[output-format]] +.outputFormat: Property +[%collapsible] +==== +Default: `"pcf"` + +Example: `outputFormat = "yaml"` + +The output format to generate. +The default output renderer for a module supports the following formats: + +* `"json"` +* `"jsonnet"` +* `"pcf"` +* `"plist"` +* `"properties"` +* `"textproto"` +* `"xml"` +* `"yaml"` +==== + +[[output-file]] +.outputFile: RegularFileProperty +[%collapsible] +==== +Default: `file("%\{moduleDir}/%\{moduleName}.%\{outputFormat}")` (places output files next to the source modules) + +Example: `outputFile = layout.projectDirectory.file("config.yaml")` + +The file path where the output file is placed. +Relative paths are resolved against the project directory. + +If multiple source modules are given, placeholders can be used to map them to different output files. +The following placeholders are supported: + +`%\{moduleDir}`::: +The directory path of the module, relative to the working directory. +Only available when evaluating file-based modules. + +`%\{moduleName}`::: +The simple module name as inferred from the module URI. +For hierarchical module URIs such as `+file:///foo/bar/baz.pkl+`, this is the last path segment without file extension. + +`%\{outputFormat}`::: +The requested output format. +Only available if `outputFormat` is set. + +If multiple sources modules are mapped to the same output file, their outputs are concatenated. +By default, module outputs are separated with `---`, as in a YAML stream. +// suppress inspection "AsciiDocLinkResolve" +The separator can be customized using the link:#module-output-separator[`moduleOutputSeparator`] option. +==== + +[[multiple-file-output-dir]] +.multipleFileOutputDir: DirectoryProperty +[%collapsible] +==== +Example 1: `multipleFileOutputDir = layout.projectDirectory.dir("output")` + +Example 2: `+multipleFileOutputDir = layout.projectDirectory.file("%{moduleDir}/output")+` +The directory where a module's output files are placed. + +Setting this option causes Pkl to evaluate a module's `output.files` property +and write the files specified therein. +Within `output.files`, a key determines a file's path relative to `multipleFileOutputDir`, +and a value determines the file's contents. + +This option cannot be used together with any of the following: + +* xref:output-file[outputFile] +* xref:expression[expression] + +This option supports the same placeholders as xref:output-file[outputFile]. + +For additional details, see xref:language-reference:index.adoc#multiple-file-output[Multiple File Output] +in the language reference. +==== + +[[module-output-separator]] +.moduleOutputSeparator: Property +[%collapsible] +==== +Default: `"---"` (as in a YAML stream) + +The separator to use when multiple module outputs are written to the same file. +==== + +[[expression]] +.expression: Property +[%collapsible] +==== +Default: (none) + +Example: `expression = "topLevelProperty.subValue"` + +The expression to be evaluated within the module. + +This option causes Pkl to evaluate the provided expression instead of the module's `output.text` or `output.files` properties. +The resulting value is then stringified, and written to the designated output file. + +For example, consider the following Pkl module: + +.my-pod.pkl +[source%tested,{pkl}] +---- +metadata { + name = "my-pod" +} +---- + +The expression `metadata.name` evaluates to text `my-pod`. +==== + +Common properties: + +include::../partials/gradle-modules-properties.adoc[] + +[[tests]] +== Tests + +This feature integrates the xref:pkl-cli:index.adoc#usage[Pkl test evaluator] into Gradle builds. + +=== Usage + +To add tests to the build, add a named configuration block inside `pkl.tests`: + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + tests { + testPkl { + sourceModules.add(files("module1_test.pkl", "module2_test.pkl")) + junitReportsDir = layout.buildDirectory.dir("reports") + overwrite = false + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + tests { + register("testPkl") { + sourceModules.addAll(files("module1_test.pkl", "module2_test.pkl")) + junitReportsDir.set(layout.buildDirectory.dir("reports")) + overwrite.set(false) + } + } +} +---- +==== + +[[junit-reports-path]] +.junitReportsDir: DirectoryProperty +[%collapsible] +==== +Default: `null` + +Example: `junitReportsDir = layout.buildDirectory.dir("reports")` + +Whether and where to generate JUnit XML reports. +==== + +[[overwrite]] +.overwrite: Property +[%collapsible] +==== +Default: `false` + +Whether to ignore expected example files and generate them again. +==== + +Common properties: + +include::../partials/gradle-modules-properties.adoc[] + +[[java-code-gen]] +== Java Code Generation + +This feature integrates the xref:java-binding:codegen.adoc[Java code generator] into Gradle builds. + +=== Usage + +To add a Java code generator to the build, add a named configuration block inside `pkl.javaCodeGenerators`: + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + javaCodeGenerators { + genJava { + sourceModules.addAll(files("Template1.pkl", "Template2.pkl")) + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + javaCodeGenerators { + register("genJava") { + sourceModules.addAll(files("Template1.pkl", "Template2.pkl")) + } + } +} +---- +==== + +To compile generated classes together with test code rather than main code, use `sourceSet = sourceSets.test`. + +To generate getter methods instead of public final fields, use `generateGetters = true`. + +For each declared Java code generator, the Pkl plugin creates an equally named task. +Hence, the above generator can be run with: + +[source,shell script] +---- +$ ./gradlew genJava +---- + +For a ready-to-go example with full source code, +see link:{uri-codegen-java-example}[codegen-java] in the _pkl/pkl-examples_ repository. + +=== Configuration Options + +.generateGetters: Property +[%collapsible] +==== +Default: `false` + +Example: `generateGetters = true` + +Whether to generate private final fields and public getter methods rather than public final fields. +==== + +// TODO: fixme (paramsAnnotation, nonNullAnnotation) +.preferJavaxInjectAnnotation: Boolean +[%collapsible] +==== +Default: `false` + +Example: `preferJavaxInjectAnnotation = true` + +Whether to annotate constructor parameters with `@javax.inject.Named` instead of `@org.pkl.config.java.mapper.Named`. +If `true`, the generated code will have a compile dependency on `javax.inject:javax.inject:1`. +==== + +Common code generation properties: + +include::../partials/gradle-codegen-properties.adoc[] + +Common properties: + +include::../partials/gradle-modules-properties.adoc[] + +[[kotlin-code-gen]] +== Kotlin Code Generation + +This feature integrates the xref:kotlin-binding:codegen.adoc[Kotlin code generator] into Gradle builds. + +=== Usage + +To add a Kotlin code generator to the build, add a named configuration block inside `pkl.kotlinCodeGenerators`: + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + kotlinCodeGenerators { + genKotlin { + sourceModules.addAll(files("Template1.pkl", "Template2.pkl")) + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + kotlinCodeGenerators { + register("genKotlin") { + sourceModules.addAll(files("Template1.pkl", "Template2.pkl")) + } + } +} +---- +==== + +To compile generated classes together with test code rather than main code, use `sourceSet = sourceSets.test`. + +For each declared Kotlin code generator, the Pkl plugin creates an equally named task. Hence the above generator can be run with: + +[source,shell script] +---- +$ ./gradlew genKotlin +---- + +For a ready-to-go example with full source code, +see link:{uri-codegen-kotlin-example}[codegen-kotlin] in the _pkl/pkl-examples_ repository. + +=== Configuration Options + +// TODO: fixme (generateKdoc) +(None) + +Common code generation properties: + +include::../partials/gradle-codegen-properties.adoc[] + +Common properties: + +include::../partials/gradle-modules-properties.adoc[] + +[[pkldoc-generation]] +== Pkldoc generation + +This features integrates the xref:pkl-doc:index.adoc[Pkldoc] generator into Gradle builds. + +=== Usage + +To add a Pkldoc generator to the build, add a named configuration block inside `pkl.pkldocGenerators`: + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + pkldocGenerators { + pkldoc { + sourceModules.addAll(files("doc-package-info.pkl", "Template1.pkl", "Template2.pkl")) + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + pkldocGenerators { + register("pkldoc") { + sourceModules.addAll(files("doc-package-info.pkl", "Template1.pkl", "Template2.pkl")) + } + } +} +---- +==== + +For each declared Pkldoc generator, the Pkl plugin creates an equally named task. +Hence, the above generator can be run with: + +[source,shell script] +---- +$ ./gradlew pkldoc +---- + +For a ready-to-go example with full source code, +see link:{uri-pkldoc-example}[pkldoc] in the _pkl/pkl-examples_ repository. + +=== Configuration Options + +The following properties can be configured inside a Pkldoc generator's configuration block: + +.outputDir: DirectoryProperty +[%collapsible] +==== +Default: `layout.buildDirectory.dir("pkldoc/")` + +Example: `outputDir = layout.projectDirectory.dir("pkl-docs")` + +The directory where generated documentation is placed. +==== + +Common properties: + +include::../partials/gradle-modules-properties.adoc[] + +[[project-package]] +== Project packaging +This feature is the Gradle analogy for the xref:pkl-cli:index.adoc#command-project-package[project package] command in the CLI. +It prepares package assets to be published from a project. + +There are two differences between this feature and the CLI: + +* Input project directories are required (the CLI determines a project from the current working directory if arguments are omitted). +* Output directory defaults to a path within the build directory. + +=== Usage + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + project { + packagers { + makePackages { + projectDirectories.from(file("pkl-config/")) + } + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + project { + packagers { + register("makePackages") { + projectDirectories.from(file("pkl-config/")) + } + } + } +} +---- +==== + +For each declared packager, the Pkl plugin creates an equally named task. +Hence, the above packager can be run with: + +[source,shell] +---- +$ ./gradlew makePackages +---- + +=== Configuration Options + +.projectDirectories: ConfigurableFileCollection +[%collapsible] +==== +Default: (none) + +Example: `projectDirectories.from(file("pkl-config/""))` + +The project directories to create packages for. +==== + +.skipPublishCheck: Property +[%collapsible] +==== +Default: (false) + +Example: `skipPublishCheck.set(true)` + +Skips checking whether a package has already been published with different contents. + +By default, the packager will check whether a package at the same version has already been published. +If the package has been published, it validates that the package's metadata is identical to the locally generated metadata. +==== + +.outputPath: DirectoryProperty +[%collapsible] +==== +Default: `project.getLayout().getBuildDirectory().dir("generated/pkl/packages")` + +The directory to write artifacts to. +Accepts the following placeholders: + +`%\{name}`:: The name of the package +`%\{version}`:: The version of the package +==== + +.junitReportsDir: DirectoryProperty +[%collapsible] +==== +Default: `null` + +Example: `junitReportsDir = layout.buildDirectory.dir("reports")` + +Whether and where to generate JUnit XML reports. +==== + +.overwrite: Property +[%collapsible] +==== +Default: `false` + +Whether to ignore expected example files and generate them again. +==== + +Common propeties: + +include::../partials/gradle-common-properties.adoc[] + +== Project Resolving + +This feature is the Gradle analogy for the xref:pkl-cli:index.adoc#command-project-resolve[project resolve] command in the CLI. +It takes the dependencies of a project, and writes the resolved versions a file at path `PklProject.deps.json`, within the root directory of the project. + +=== Usage + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + project { + resolvers { + resolvePklDeps { + projectDirectories.from(file("pkl-config/")) + } + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + project { + resolvers { + register("resolvePklDeps") { + projectDirectories.from(file("pkl-config/")) + } + } + } +} +---- +==== + +For each declared resolver, the Pkl plugin creates an equally named task. +Hence, the above resolver can be run with: + +[source,shell] +---- +$ ./gradlew resolvePklDeps +---- + +=== Configuration Options + +.projectDirectories: ConfigurableFileCollection +[%collapsible] +==== +Default: (none) + +Example: `projectDirectories.from(file("pkl-config/""))` + +The project directories to create packages for. +==== + +Common propeties: + +include::../partials/gradle-common-properties.adoc[] diff --git a/docs/modules/pkl-gradle/partials/gradle-codegen-properties.adoc b/docs/modules/pkl-gradle/partials/gradle-codegen-properties.adoc new file mode 100644 index 00000000..1911c588 --- /dev/null +++ b/docs/modules/pkl-gradle/partials/gradle-codegen-properties.adoc @@ -0,0 +1,39 @@ +.indent: Property +[%collapsible] +==== +Default: `" "` (two spaces) + +Example: `indent = "\t"` (one tab) + +The characters to use for indenting generated source code. +==== + +.outputDir: DirectoryProperty +[%collapsible] +==== +Default: `layout.buildDirectory.dir("generated/pkl/")` + +Example: `outputDir = layout.projectDirectory.dir("src/main/pkl")` + +The directory where generated classes are placed. + +The default places generated sources within the build directory of the project, to avoid sources from being committed into the repository on accident. +==== + +.sourceSet: Property +[%collapsible] +==== +Default: `sourceSets.main` (if it exists; no default otherwise) + +Example: `sourceSet = sourceSets.test` + +The Gradle source set that generated code is compiled together with. + +For the codegen tasks, the `modulePath` property defaults to the compilation classpath of this source set, as well as all of the source directories of the `resource` source directory set of this source set. This setup makes it possible to rely on modules defined in classpath dependencies of your project or in the resources of your project. + +For projects which apply the `idea` plugin and are opened in IntelliJ IDEA, this option determines whether generated sources are marked as test sources (if the source set's name contains the word "test") or regular sources (otherwise). +==== + +.generateSpringBootConfig: Property +[%collapsible] +==== +Default: `false` + +Example: `generateSpringBootConfig = true` + +Whether to generate config classes for use with Spring Boot. +==== + +// TODO: fixme (implementSerializable) diff --git a/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc b/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc new file mode 100644 index 00000000..de2152d3 --- /dev/null +++ b/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc @@ -0,0 +1,85 @@ +.allowedModules: ListProperty +[%collapsible] +==== +Default: `["pkl:", "file:", "modulepath:", "https:", "repl:", "package:", "projectpackage:"]` + +Example: `allowedModules = ["file:"]` + +URI patterns that determine which modules can be loaded and evaluated. +Patterns are matched against the beginning of module URIs. +(File paths have been converted to `file:` URLs at this stage.) +At least one pattern needs to match for a module to be loadable. +Both source modules and transitive modules are subject to this check. +==== + +.allowedResources: ListProperty +[%collapsible] +==== +Default: `["env:", "prop:", "modulepath:", "https:", "file:", "package:", "projectpackage:"]` + +Example: `allowedResources = ["env:", "prop:"]` + +URL patterns that determine which external resources can be read. +Patterns are matched against the beginning of resource URLs. +At least one pattern needs to match for a resource to be readable. +==== + +.environmentVariables: MapProperty +[%collapsible] +==== +Default: `[:]` (note that Gradle default differs from CLI default) + +Example 1: `environmentVariables = ["MY_VAR_1": "myValue1", "MY_VAR_2": "myValue2"]` + +Example 2: `environmentVariables = System.getenv()` + +Environment variables that can be read by Pkl code with `read("env:")`. +==== + +.evalRootDir: DirectoryProperty +[%collapsible] +==== +Default: `rootProject.layout.projectDirectory` + +Example 1: `evalRootDir = layout.projectDirectory.dir("pkl-modules")` + +Example 2: `evalRootDir.fileValue file("/some/absolute/path")` + + +Root directory for `file:` modules and resources. +If non-null, access to file-based modules and resources is restricted to those located under the root directory. +Any symlinks are resolved before this check is performed. +==== + +.evalTimeout: Property +[%collapsible] +==== +Default: `null` + +Example: `evalTimeout = Duration.ofSeconds(10)` + +Duration after which evaluation of a source module will be timed out. +Note that a timeout is treated the same as a program error in that any subsequent source modules will not be evaluated. +==== + +.externalProperties: MapProperty +[%collapsible] +==== +Default: `[:]` + +Example: `externalProperties = ["myProp1": "myValue1", "myProp2": "myValue2"]` + +External properties that can be read by Pkl code with `read("prop:")`. +==== + +.moduleCacheDir: DirectoryProperty +[%collapsible] +==== +Default: `null` + +Example 1: `moduleCacheDir = layout.buildDirectory.dir("pkl-module-cache")` + +Example 2: `moduleCacheDir.fileValue file("/absolute/path/to/cache")` + +The cache directory for storing packages. +If `null`, defaults to `~/.pkl/cache`. +==== + +.noCache: Property +[%collapsible] +==== +Default: `false` + +Disable cacheing of packages. +==== + +.modulePath: ConfigurableFileCollection +[%collapsible] +==== +Default: `files()` (empty collection) + +Example: `modulePath.from files("dir1", "zip1.zip", "jar1.jar")` + +The directories, ZIP archives, or JAR archives to search when resolving `modulepath:` URIs. +Relative paths are resolved against the project directory. +==== diff --git a/docs/modules/pkl-gradle/partials/gradle-modules-properties.adoc b/docs/modules/pkl-gradle/partials/gradle-modules-properties.adoc new file mode 100644 index 00000000..059397ee --- /dev/null +++ b/docs/modules/pkl-gradle/partials/gradle-modules-properties.adoc @@ -0,0 +1,69 @@ + +.sourceModules: ListProperty +[%collapsible] +==== +Default: `[]` + +Example 1: `sourceModules = ["module1.pkl", "module2.pkl"]` + +Example 2: `+sourceModules = fileTree("config").include("**/*.pkl")+` + +List of Pkl modules which are used for this operation. + +This property accepts the following types to represent a module: + +* `java.net.URI` +* `java.io.File` +* `java.nio.file.Path` +* `java.net.URL` +* `java.lang.CharSequence` - if the represented string looks like a URI (it contains a scheme), the input is treated as a URI. Otherwise, it is treated as a path. Relative paths are resolved against the project directory. +* `org.gradle.api.file.FileSystemLocation` +==== + +.transitiveModules: ConfigurableFileCollection +[%collapsible] +==== +Default: `files()` (empty collection) + +Example 1: `transitiveModules.from files("module1.pkl", "module2.pkl")` + +Example 2: `+transitiveModules.from fileTree("config").include("**/*.pkl")+` + +File paths of modules that are directly or indirectly used by source modules. +Setting this option enables correct Gradle up-to-date checks, which ensures that your Pkl tasks are executed if any of the transitive files are modified; it does not affect evaluation otherwise. +Including source modules in `transitiveModules` is permitted but not required. +Relative paths are resolved against the project directory. +==== + +.projectDir: DirectoryProperty +[%collapsible] +==== +Default: `null` + +Example 1: `projectDir = layout.projectDirectory.dir("pkl")` + +Example 2: `projectDir.fileValue file("/some/absolute/path")` + +Directory where the project lives. + +A project is a directory that contains a `PklProject` file, which is used to declare package dependencies, as well as common evaluator settings to be applied in the project. + +If `null`, this is determined by searching up from the working directory for a directory that contains a `PklProject` file, until `evalRootDir` or the file system root is reached. +==== + +.omitProjectSettings: Property +[%collapsible] +==== +Disables loading evaluator settings from the PklProject file. +==== + +.noProject: Property +[%collapsible] +==== +Disables all behavior related to projects. +==== + +.settingsModule: Property +[%collapsible] +==== +Default: `null` + +Example: `settingsModule = layout.projectDirectory.file("mySettings.pkl")` + +The Pkl settings module to use. +This property accepts the same input types as the `sourceModules` property. + +If `null`, `~/.pkl/settings.pkl` or defaults specified in the `pkl.settings` standard library module are used. +==== + +include::../partials/gradle-common-properties.adoc[] diff --git a/docs/modules/release-notes/pages/0.25.adoc b/docs/modules/release-notes/pages/0.25.adoc new file mode 100644 index 00000000..dd33bf8b --- /dev/null +++ b/docs/modules/release-notes/pages/0.25.adoc @@ -0,0 +1,8 @@ += Pkl 0.25 Release Notes +:version: 0.25 +:version-minor: 0.25.0 +:release-date: + +This is the first release of Pkl! + +To learn more, refer to our xref:blog:introducing-pkl.adoc[blog post]. diff --git a/docs/modules/release-notes/pages/changelog.adoc b/docs/modules/release-notes/pages/changelog.adoc new file mode 100644 index 00000000..720f7ab7 --- /dev/null +++ b/docs/modules/release-notes/pages/changelog.adoc @@ -0,0 +1,8 @@ += Changelog +include::ROOT:partial$component-attributes.adoc[] + +[[release-0.25.0]] +== 0.25.0 (2024-02-01) + +xref:0.25.adoc[Release notes] + diff --git a/docs/modules/release-notes/pages/index.adoc b/docs/modules/release-notes/pages/index.adoc new file mode 100644 index 00000000..75b25a3a --- /dev/null +++ b/docs/modules/release-notes/pages/index.adoc @@ -0,0 +1,4 @@ += Release Notes + +* xref:0.25.adoc[0.25 Release Notes] +* xref:changelog.adoc[Changelog] diff --git a/docs/modules/release-notes/template.adoc b/docs/modules/release-notes/template.adoc new file mode 100644 index 00000000..726038e3 --- /dev/null +++ b/docs/modules/release-notes/template.adoc @@ -0,0 +1,71 @@ += Pkl XXX Release Notes +:version: XXX (e.g., 0.9) +:version-minor: XXX (e.g., 0.9.0) +:release-date: XXX (e.g., July 11, 2018) + +include::ROOT:partial$component-attributes.adoc[] + +Pkl {version} was released on {release-date}. + +[.small]#The latest bugfix release is {version-minor}. (xref:changelog.adoc[All Versions])# + +XXX + +The next release (XXX) is scheduled for XXX (e.g., August 2, 2021). + +Please send feedback and questions to https://github.com/apple/pkl/discussions[GitHub Discussions], or submit an issue on https://github.com/apple/pkl/issues/new[Github]. + + +[small]#Pkl is hosted on https://github.com/apple/pkl[GitHub]. +To get started, follow xref:pkl-cli:index.adoc#installation[Installation].# + +== Highlights [small]#💖# + +News you don't want to miss. + +.XXX +[%collapsible] +==== +XXX +==== + +== Noteworthy [small]#🎶# + +Ready when you need them. + +.XXX +[%collapsible] +==== +XXX +==== + +== Breaking Changes [small]#💔# + +Things to watch out for when upgrading. + +.XXX +[%collapsible] +==== +XXX +==== + +== Work In Progress [small]#🚆# + +They missed the train but deserve a mention. + +.XXX +[%collapsible] +==== +XXX +==== + +== Contributors [small]#🙏# + +We would like to thank the contributors to this release (in alphabetical order): + +* XXX + +== Closed Radars [small]#🔒# + +XXX Radars down, Inbox Zero in sight ... + +[smaller] +. XXX (https://github.com/apple/pkl/issues/new[XXX]) diff --git a/docs/modules/style-guide/pages/index.adoc b/docs/modules/style-guide/pages/index.adoc new file mode 100644 index 00000000..09b58f76 --- /dev/null +++ b/docs/modules/style-guide/pages/index.adoc @@ -0,0 +1,802 @@ += Pkl Style Guide +:icons: font +:source-highlighter: highlight.js +:pkl-expr: pkl expression +:pkl: pkl +:sectnums: + +This document serves as the Pkl team's recommended coding standard for the Pkl configuration language. + +== Files + +=== Filename + +Use the `.pkl` extension for all files. + +Follow these rules for casing the file's name: + +[cols="1,3,1"] +|=== +| Casing | Description | Example +| PascalCase +| It is designed to be used as a template, or used as a class (i.e. imported and instantiated). +| `K8sResource.pkl` +| camelCase +| It is designed to be used as a value. +| `myDeployment.pkl` +| kebab-case +| It is designed to be used as a CLI tool. +| `do-convert.pkl` +|=== + +*Exception*: If a file is meant to render into a static configuration file, the filename should match the target file's name without the extension. +For example, `config.pkl` turns into `config.yml`. + +*Exception*: The `PklProject` file cannot have any extension. + +=== File Encoding + +Encode all files using UTF-8. + +== Module Structure + +=== Header + +Separate each section of the module header by one blank line. + +A module header consists of the following clauses, each of which is optional: + +- Module clause +- `amends` or `extends` clause +- Import clauses + +.module.pkl +[source%parsed,{pkl}] +---- +module com.example.Foo // <1> + +extends "Bar.pkl" // <2> + +import "baz.pkl" // <3> +import "Buz.pkl" // <3> +---- +<1> Module clause +<2> `extends` clause +<3> Import clause + +==== Module name + +Match the name of the module with the name of the file. + +.MyModule.pkl +[source%tested,{pkl}] +---- +module MyModule + +---- + +If a module is meant to be published, add a module clause, `@ModuleInfo` annotation, and doc comments. + +Modules that do not get published anywhere may omit a module clause. + +.MyModule.pkl +[source%tested,{pkl}] +---- +/// Used for some type of purpose. <1> +@ModuleInfo { minPklVersion = "0.24.0" } // <2> +module MyModule // <3> + +---- +<1> Doc comments +<2> `@ModuleInfo` annotation +<3> Module clause + +==== `amends` vs. `extends` clause + +A module that doesn't add new properties shouldn't use the `extends` clause. + +==== Imports + +Sort imports sections using https://en.wikipedia.org/wiki/Natural_sort_order[natural sorting] by their module URI. +Relative path imports should be in their own section, separated by a newline. +There should be no unused imports. + +[source%parsed,{pkl}] +---- +import "modulepath:/foo.pkl" +import "package://example.com/mypackage@1.0.0#/foo.pkl" + +import ".../my/file/bar2.pkl" +import ".../my/file/bar11.pkl" +---- + +=== Module body + +Within a module body, define members in this order: + +1. Properties +2. Methods +3. Classes and type aliases +4. The amended xref:language-reference:index.adoc#in-language[output] property. + +*Exception*: local members can be close to their usage. + +*Exception*: functions meant to be a class constructor can be next to the class declaration. + +.constructor.pkl +[source%tested,{pkl}] +---- +function MyClass(_name: String): MyClass = new { name = _name } + +class MyClass { + name: String +} +---- + +=== Module URIs + +If possible, use xref:language-reference:index.adoc#triple-dot-module-uris[triple-dot Module URIs] to reference ancestor modules +instead of multiple `../`. + +.good.pkl +[source%parsed,{pkl}] +---- +amends ".../ancestor.pkl" + +import ".../ancestor2.pkl" +---- + +.bad.pkl +[source%parsed,{pkl}] +---- +amends "../../../ancestor.pkl" + +import "../../../ancestor2.pkl" +---- + +== Objects + +=== Member spacing + +Object members (properties, elements, and entries) should be separated by at most one blank line. + +.good.pkl +[source%tested,{pkl}] +---- +foo = "bar" + +baz = "buz" +---- + +.good.pkl +[source%tested,{pkl}] +---- +foo = "bar" +baz = "buz" +---- + +.bad.pkl +[source%tested,{pkl}] +---- +foo = "bar" + + +baz = "buz" +---- + +Too many lines separate `foo` and `baz`. + +=== Overridden properties + +Properties that override an existing property shouldn't have doc comments nor type annotations, +unless the type is intentionally overridden via `extends`. + +[source%tested,{pkl}] +---- +amends "myOtherModule.pkl" + +foo = "bar" +---- + +=== New property definitions + +Each property definition should have a type annotation and <>. +Successive definitions should be separated by a blank line. + +.good.pkl +[source%parsed,{pkl}] +---- +/// Denotes something. +myFoo: String + +/// Something else +myOtherFoo: String +---- + +.bad.pkl +[source%parsed,{pkl}] +---- +/// Denotes something. +myFoo: String +/// Something else +myOtherFoo: String +---- + +=== Objects with `new` + +When initializing a `Typed` object using `new`, omit the type. +For example, use `new {}` instead of `new Foo {}`. + +This rule does not apply when initializing a property to a subtype of the property's declared type. + +.good.pkl +[source%parsed,{pkl}] +---- +myFoo: Foo = new { foo = "bar" } +---- + +.good.pkl +[source%parsed,{pkl}] +---- +open class Foo {} +class Bar extends Foo {} + +foo: Foo = new Bar {} +---- + +This is okay because this is meaning to initialize `Bar` instead of `Foo`. + +.bad.pkl +[source%parsed,{pkl}] +---- +myFoo1: Foo = new Foo { foo = "bar" } // <1> + +myFoo2 = new Foo { foo = "bar" } // <2> +---- +<1> Unnecessary `new Foo { ... }` +<2> Unless amending/extendinge a module where `myFoo2` is already defined, `myFoo2` is effectively the `unknown` type, i.e. `myFoo2: unknown`. + +== Comments + +Use doc comments to convey information to users of a module. +Use line comments or block comments to convey implementation concerns to authors of a module, or to comment out code. + +[[doc-comment]] +=== Doc comments + +Doc comments should start with a one sentence summary paragraph, followed by additional paragraphs if necessary. +Start new sentences on their own line. +Add a single space after `///`. + +[source%parsed,{pkl}] +---- +/// The time alotted for eating lunch. +/// +/// Note: +/// * Hamburgers typically take longer to eat than salad. +/// * Pizza gets prepared per-order. +/// +/// Orders must be placed on-prem. +/// See for more details. +lunchHours: Duration +---- + +=== Line comments + +If a comment relates to a property definition, place it after the property's doc comments. +Add a single space after `//`. + +.good.pkl +[source%parsed,{pkl}] +---- +/// Designates whether it is zebra party time. +// TODO: Add constraints here? +partyTime: Boolean +---- + +A line comment may also be placed at the end of a line, as long as the line doesn't exceed 100 characters. + +.good.pkl +[source%tested,{pkl}] +---- +/// Designates whether it is zebra party time. +partyTime: Booleean // TODO: Add constraints here? +---- + +=== Block comments + +A single-line block comment should have a single space after `+++/*+++` and before `+++*/+++`. + +.good.pkl +[source%tested,{pkl}] +---- +/* Let's have a zebra party */ +---- + +.bad.pkl +[source%tested,{pkl}] +---- +/*Let's have a zebra party*/ +---- + +== Classes + +=== Class names + +Name classes in PascalCase. + +.good.pkl +[source%tested,{pkl}] +---- +class ZebraParty {} +---- + +.bad.pkl +[source%tested,{pkl}] +---- +class zebraParty {} +class zebraparty {} +---- + +== Strings + +=== Custom String Delimiters + +Use xref:language-reference:index.adoc#custom-string-delimiters[custom string delimiters] to avoid the need for string escaping. + +.good.pkl +[source%tested,{pkl}] +---- +myString = #"foo \ bar \ baz"# +---- + +.bad.pkl +[source%tested,{pkl}] +---- +myString = "foo \\ bar \\ baz" +---- + +NOTE: Sometimes, using custom string delimiters makes source code harder to read. For example, the `+\#+` literal reads better using escapes (`"\\#"`) than using custom string delimimters (`+##"\#"##+`). + +=== Interpolation + +Prefer interpolation to string concatenation. + +.good.pkl +[source%parsed,{pkl}] +---- +greeting = "Hello, \(name)" +---- + +.bad.pkl +[source%parsed,{pkl}] +---- +greeting = "Hello, " + name +---- + +== Formatting + +=== Line width + +Lines shouldn't exceed 100 characters. + +*Exceptions:* + +1. String literals +2. Code snippets within doc comments + +=== Indentation + +Use two spaces per indentation level. + +==== Members within braces + +Members within braces should be indented one level deeper than their parents. + +[source%tested,{pkl}] +---- +foo { + bar { + baz = "hi" + } +} +---- + +==== Assignment operator (`=`) + +An assignee that starts after a newline should be indented. + +.good.pkl +[source%tested,{pkl}] +---- +foo = + "foo" + +bar = + new { + baz = "baz" + biz = "biz" + } +---- + +.bad.pkl +[source%tested,{pkl}] +---- +foo = +"foo" + +bar = +new { + baz = "baz" + biz = "biz" +} +---- + +An assignee that starts on the same line should not be indented. + +.good.pkl +[source%tested,{pkl}] +---- +foo = new { + baz = "baz" + biz = "biz" +} +---- + +.bad.pkl +[source%tested,{pkl}] +---- +foo = new { + baz = "baz" + biz = "biz" + } +---- + +==== `if` and `let` expressions + +`if` and `let` bodies that start on their own line should be indented. +Child bodies may also be inline, and the `else` branch of `if` expressions may be inline of `if`. + +.good.pkl +[source%parsed,{pkl-expr}] +---- +if (bar) + bar +else + foo +---- + +.good.pkl +[source%parsed,{pkl-expr}] +---- +if (bar) bar else foo +---- + +.good.pkl +[source%parsed,{pkl-expr}] +---- +if (bar) bar +else foo +---- + +.good.pkl +[source%parsed,{pkl-expr}] +---- +let (foo = "bar") + foo.toUpperCase() +---- + +.good.pkl +[source%parsed,{pkl-expr}] +---- +let (foo = "bar") foo.toUpperCase() +---- + +.bad.pkl +[source%parsed,{pkl-expr}] +---- +if (bar) +bar +else +foo +---- + +.bad.pkl +[source%parsed,{pkl-expr}] +---- +let (foo = "bar") +foo.toUpperCase() +---- + +*Exception*: A nested `if` expression within the `else` branch should have the same indentation level as its parent, and start on the same line as the parent `else` keyword. + +.good.pkl +[source%parsed,{pkl-expr}] +---- +if (bar) + bar +else if (baz) + baz +else + foo +---- + +.bad.pkl +[source%parsed,{pkl-expr}] +---- +if (bar) + bar +else + if (baz) + baz + else + foo +---- + +==== Multiline chained method calls + +Indent successive multiline chained method calls. + +[source%parsed,{pkl-expr}] +---- +foo() + .bar() + .baz() + .biz() +---- + +==== Multiline binary operators + +Place operators after the newline, and indent successive lines to the same level. + +.good.pkl +[source%parsed,{pkl}] +---- +foo = bar + |> baz + |> biz + +myNum = 1 + + 2 + + 3 + + 4 +---- + +.bad.pkl +[source%parsed,{pkl}] +---- +foo = bar |> + baz |> + biz + +myNum = 1 + + 2 + + 3 + + 4 +---- + +.bad.pkl +[source%tested,{pkl}] +---- +foo = bar +|> baz +|> biz +---- + +.bad.pkl +[source%tested,{pkl}] +---- +foo = bar + |> baz + |> biz +---- + +*Exception*: the minus operator must come before the newline, because otherwise it is parsed as a unary minus. + +.good.pkl +[source%tested,{pkl}] +---- +myNum = 1 - + 2 - + 3 - + 4 +---- + +.bad.pkl +[source%tested,{pkl}] +---- +myNum = 1 + - 2 + - 3 + - 4 +---- + +=== Spaces + +Add a space: + +[source%parsed,{pkl}] +---- +amends "Foo.pkl" // <1> + +res1 { "foo" } // <2> +res2 = 1 + 2 // <3> +res3 = res2 as Number // <3> +res4 = List(1, 2, 3) // <4> +res5 = if (foo) bar else baz // <5> +---- +<1> After keywords +<2> Before and after braces +<3> Around infix operators +<4> After a comma +<5> Before opening parentheses in control operators (`if`, `for`, `when` are control operators) + +NOTE: No spaces are added around the pipe symbol (`|`) in union types. + +[source%tested,{pkl}] +---- +typealias Foo = "foo"|"bar"|"baz" +---- + +=== Object bodies + +==== Single line + +An object body may be a single line if it only consists of primitive elements, or if it contains two or fewer members. +Otherwise, split them into multiple lines. + +Separate each member of a single line object with a semicolon and a space. + +.good.pkl +[source%tested,{pkl}] +---- +res1 = new { bar = "bar"; baz = "baz" } +res2 = new { 1; 2; 3; 4; 5; 6 } +---- + +.bad.pkl +[source%parsed,{pkl}] +---- +res1 = new { bar = "bar"; baz = "baz"; biz = "biz"; } // <1> + +res2 = new { 1 2 3 4 5 6 } // <2> +---- + +<1> Too many members and trailing `;` +<2> No semicolon + +==== Multiline + +Multiline objects should have their members separated by at least one line break and at most one blank line. + +.good.pkl +[source%tested,{pkl}] +---- +res { + foo = "foo" + bar = "bar" +} + +res2 { + ["foo"] = "foo" + ["bar"] = "bar" +} + +res3 { + "foo" + "bar" +} +---- + +.good.pkl +[source%tested,{pkl}] +---- +res { + foo = "foo" + + bar = "bar" +} + +res2 { + ["foo"] = "foo" + + ["bar"] = "bar" +} + +res3 { + "foo" + + "bar" +} +---- + +.bad.pkl +[source%tested,{pkl}] +---- +res { + foo = "foo" + + + bar = "bar" // <1> +} + +res2 { + ["foo"] = "foo" + + + ["bar"] = "bar" // <1> +} + +res3 { + "foo" + + + "bar" // <1> +} + +res4 { + foo = "foo"; bar = "bar" // <2> +} +---- +<1> Too many blank lines between members +<2> No line break separating members + +Put the opening brace on the same line. + +.good.pkl + +[source%tested,{pkl}] +---- +res { + foo = "foo" + bar = "bar" +} +---- + +.bad.pkl +[source%tested,{pkl}] +---- +res +{ + foo = "foo" + bar = "bar" +} +---- + +== Programming Practices + +=== Prefer `for` generators + +When programmatically creating elements and entries, prefer +xref:language-reference:index.adoc#for-generators[for generators] over using the collection API. +Using for generators preserves xref:language-reference:index.adoc#late-binding[late binding]. + +.good.pkl +[source%tested,{pkl}] +---- +numbers { + 1 + 2 + 3 + 4 +} + +squares { + for (num in numbers) { + num ** 2 + } +} +---- + +.bad.pkl +[source%tested,{pkl}] +---- +numbers { + 1 + 2 + 3 + 4 +} + +squares = numbers.toList().map((num) -> num ** 2).toListing() +---- diff --git a/docs/nav.adoc b/docs/nav.adoc new file mode 100644 index 00000000..aac43c55 --- /dev/null +++ b/docs/nav.adoc @@ -0,0 +1,44 @@ +* xref:pkl-cli:index.adoc#installation[Installation] +* xref:language-tutorial:index.adoc[Tutorial] +** xref:language-tutorial:01_basic_config.adoc[Basic Configuration] +** xref:language-tutorial:02_filling_out_a_template.adoc[Filling out a Template] +** xref:language-tutorial:03_writing_a_template.adoc[Writing a Template] +* xref:language-reference:index.adoc[Language Reference] + +* xref:introduction:index.adoc[Introduction] +** xref:introduction:use-cases.adoc[Use Cases] +** xref:introduction:concepts.adoc[Concepts] +** xref:introduction:comparison.adoc[Comparison] + +* xref:ROOT:language.adoc[Language] +** xref:language-tutorial:index.adoc[Tutorial] +** xref:language-reference:index.adoc[Language Reference] +** xref:ROOT:standard-library.adoc[Standard Library] + +* xref:ROOT:tools.adoc[Tools] +** xref:pkl-cli:index.adoc[CLI] +** xref:pkl-doc:index.adoc[Pkldoc] +** xref:pkl-gradle:index.adoc[Gradle Plugin] +** Editor support +*** xref:intellij:ROOT:index.adoc[IntelliJ] +*** xref:vscode:ROOT:index.adoc[VSCode] +*** xref:neovim:ROOT:index.adoc[Neovim] + +* xref:ROOT:language-bindings.adoc[Language Bindings] +** xref:java-binding:index.adoc[Java] +*** xref:java-binding:codegen.adoc[Code Generator] +*** xref:pkl-core:index.adoc[pkl-core Library] +*** xref:java-binding:pkl-config-java.adoc[pkl-config-java Library] + +** xref:kotlin-binding:index.adoc[Kotlin] +*** xref:kotlin-binding:codegen.adoc[Code Generator] +*** xref:kotlin-binding:pkl-config-kotlin.adoc[pkl-config-kotlin Library] + +** xref:swift:ROOT:index.adoc[Swift] +** xref:go:ROOT:index.adoc[Go] + +* xref:ROOT:examples.adoc[Examples] + +* xref:release-notes:index.adoc[Release Notes] +** xref:release-notes:0.25.adoc[0.25 Release Notes] +** xref:release-notes:changelog.adoc[Changelog] diff --git a/docs/src/test/kotlin/DocSnippetTests.kt b/docs/src/test/kotlin/DocSnippetTests.kt new file mode 100644 index 00000000..1662dc73 --- /dev/null +++ b/docs/src/test/kotlin/DocSnippetTests.kt @@ -0,0 +1,369 @@ +import org.junit.platform.commons.annotation.Testable +import org.junit.platform.engine.* +import org.junit.platform.engine.TestDescriptor.Type +import org.junit.platform.engine.discovery.ClassSelector +import org.junit.platform.engine.discovery.MethodSelector +import org.junit.platform.engine.discovery.PackageSelector +import org.junit.platform.engine.discovery.UniqueIdSelector +import org.junit.platform.engine.support.descriptor.* +import org.junit.platform.engine.support.hierarchical.EngineExecutionContext +import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine +import org.junit.platform.engine.support.hierarchical.Node +import org.junit.platform.engine.support.hierarchical.Node.DynamicTestExecutor +import org.opentest4j.MultipleFailuresError +import org.pkl.commons.test.FileTestUtils.rootProjectDir +import org.pkl.core.Loggers +import org.pkl.core.SecurityManagers +import org.pkl.core.StackFrameTransformers +import org.pkl.core.module.ModuleKeyFactories +import org.pkl.core.parser.LexParseException +import org.pkl.core.parser.Parser +import org.pkl.core.parser.antlr.PklParser +import org.pkl.core.repl.ReplRequest +import org.pkl.core.repl.ReplResponse +import org.pkl.core.repl.ReplServer +import org.pkl.core.resource.ResourceReaders +import org.pkl.core.util.IoUtils +import org.antlr.v4.runtime.ParserRuleContext +import java.nio.file.Files +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import kotlin.io.path.useDirectoryEntries + +@Testable +class DocSnippetTests + +class DocSnippetTestsEngine : HierarchicalTestEngine() { + private val projectDir = rootProjectDir.resolve("docs") + private val docsDir = projectDir.resolve("modules") + + companion object { + val headingRegex = Regex("""(?u)^\s*(=++)\s*(.+)""") + val collapsibleBlockRegex = Regex("""(?u)^\s*\[%collapsible""") + val codeBlockRegex = Regex("""(?u)^\s*\[source(?:%(tested|parsed)(%error)?)?(?:,(?:\{)?([a-zA-Z-_]+)}?)?""") + val codeBlockNameRegex = Regex("""(?u)^\s*\.(.+)""") + val codeBlockDelimiterRegex = Regex("""(?u)^\s*----""") + val graphicsRegex = Regex("\\[small]#.+#") + } + + override fun getId() = "pkl-doc-tests" + + override fun discover( + discoveryRequest: EngineDiscoveryRequest, + uniqueId: UniqueId + ): TestDescriptor { + val packageSelectors = discoveryRequest.getSelectorsByType(PackageSelector::class.java) + val classSelectors = discoveryRequest.getSelectorsByType(ClassSelector::class.java) + val methodSelectors = discoveryRequest.getSelectorsByType(MethodSelector::class.java) + val uniqueIdSelectors = discoveryRequest.getSelectorsByType(UniqueIdSelector::class.java) + + val testClass = DocSnippetTests::class.java + val packageName = testClass.`package`.name + val className = testClass.name + + if (methodSelectors.isEmpty() + && (packageSelectors.isEmpty() || packageSelectors.any { it.packageName == packageName }) + && (classSelectors.isEmpty() || classSelectors.any { it.className == className }) + ) { + + val rootDescriptor = Descriptor.Path(uniqueId, docsDir.fileName.toString(), ClassSource.from(testClass), docsDir) + doDiscover(rootDescriptor, uniqueIdSelectors) + return rootDescriptor + } + + // return empty descriptor w/o children + return EngineDescriptor(uniqueId, javaClass.simpleName) + } + + override fun createExecutionContext(request: ExecutionRequest): ExecutionContext { + val replServer = ReplServer( + SecurityManagers.defaultManager, + Loggers.stdErr(), + listOf( + ModuleKeyFactories.standardLibrary, + ModuleKeyFactories.classPath(DocSnippetTests::class.java.classLoader), + ModuleKeyFactories.file + ), + listOf( + ResourceReaders.environmentVariable(), + ResourceReaders.externalProperty() + ), + System.getenv(), + emptyMap(), + null, + null, + null, + IoUtils.getCurrentWorkingDir(), + StackFrameTransformers.defaultTransformer + ) + return ExecutionContext(replServer) + } + + private fun doDiscover(rootDescriptor: TestDescriptor, selectors: List) { + fun isMatch(other: UniqueId) = selectors.isEmpty() || selectors.any { + it.uniqueId.hasPrefix(other) || other.hasPrefix(it.uniqueId) + } + + docsDir.useDirectoryEntries { docsDirEntries -> + for (docsDirEntry in docsDirEntries) { + if (!docsDirEntry.isDirectory()) continue + + val docsDirEntryName = docsDirEntry.fileName.toString() + val docsDirEntryId = rootDescriptor.uniqueId.append("dir", docsDirEntryName) + if (!isMatch(docsDirEntryId)) continue + + val docsDirEntryDescriptor = Descriptor.Path( + docsDirEntryId, + docsDirEntryName, + DirectorySource.from(docsDirEntry.toFile()), + docsDirEntry + ) + rootDescriptor.addChild(docsDirEntryDescriptor) + + val pagesDir = docsDirEntry.resolve("pages") + if (!pagesDir.isDirectory()) continue + + pagesDir.useDirectoryEntries { pagesDirEntries -> + for (pagesDirEntry in pagesDirEntries) { + val pagesDirEntryName = pagesDirEntry.fileName.toString() + val pagesDirEntryId = docsDirEntryId.append("file", pagesDirEntryName) + if (!pagesDirEntry.isRegularFile() || + !pagesDirEntryName.endsWith(".adoc") || + !isMatch(pagesDirEntryId) + ) continue + + val pagesDirEntryDescriptor = Descriptor.Path( + pagesDirEntryId, + pagesDirEntryName, + FileSource.from(pagesDirEntry.toFile()), + pagesDirEntry + ) + docsDirEntryDescriptor.addChild(pagesDirEntryDescriptor) + + parseAsciidoc(pagesDirEntryDescriptor, selectors) + } + } + } + } + } + + private fun parseAsciidoc(docDescriptor: Descriptor.Path, selectors: List) { + var line = "" + var prevLine = "" + var lineNum = 0 + var codeBlockNum = 0 + val sections = ArrayDeque() + + Files.lines(docDescriptor.path).use { linesStream -> + val linesIterator = linesStream.iterator() + + fun advance() { + prevLine = line + line = linesIterator.next() + lineNum += 1 + } + + fun addSection(title: String, newLevel: Int) { + while (sections.isNotEmpty() && sections.first().level >= newLevel) { + sections.removeFirst() + } + + val parent = sections.firstOrNull() ?: docDescriptor + val normalizedTitle = title + .replace("", "") + .replace("", "") + .replace(graphicsRegex, "") + .trim() + val childSection = Descriptor.Section( + parent.uniqueId.append("section", normalizedTitle), + normalizedTitle, + FileSource.from(docDescriptor.path.toFile(), FilePosition.from(lineNum)), + newLevel + ) + + sections.addFirst(childSection) + parent.addChild(childSection) + codeBlockNum = 0 + } + + nextLine@ while (linesIterator.hasNext()) { + advance() + + val headingMatch = headingRegex.find(line) + if (headingMatch != null) { + val (markup, title) = headingMatch.destructured + val newLevel = markup.length + // ignore level 1 heading (we already have a test node for the file) + if (newLevel == 1) continue + addSection(title, newLevel) + continue + } + + val collapsibleBlockMatch = collapsibleBlockRegex.find(line) + if (collapsibleBlockMatch != null) { + val blockName = codeBlockNameRegex.find(prevLine)?.groupValues?.get(1) ?: "Details" + val newLevel = 999 + addSection(blockName, newLevel) + continue + } + + val codeBlockMatch = codeBlockRegex.find(line) + if (codeBlockMatch != null) { + codeBlockNum += 1 + val (testMode, error, language) = codeBlockMatch.destructured + if (testMode.isNotEmpty()) { + val blockName = codeBlockNameRegex.find(prevLine)?.groupValues?.get(1) ?: "snippet$codeBlockNum" + while (linesIterator.hasNext()) { + advance() + val startDelimiterMatch = codeBlockDelimiterRegex.find(line) + if (startDelimiterMatch != null) { + val jumpToLineNum = lineNum + 1 + val builder = StringBuilder() + while (linesIterator.hasNext()) { + advance() + val endDelimiterMatch = codeBlockDelimiterRegex.find(line) + if (endDelimiterMatch != null) { + val section = sections.first() + val snippetId = section.uniqueId.append("snippet", blockName) + if (selectors.isEmpty() || selectors.any { snippetId.hasPrefix(it.uniqueId) }) { + section.addChild( + Descriptor.Snippet( + snippetId, + blockName, + language, + FileSource.from(docDescriptor.path.toFile(), FilePosition.from(jumpToLineNum)), + builder.toString(), + testMode == "parsed", + error.isNotEmpty() + ) + ) + } + continue@nextLine + } + builder.appendLine(line) + } + } + } + } + } + } + } + } + + class ExecutionContext(val replServer: ReplServer) : EngineExecutionContext, AutoCloseable { + override fun close() { + replServer.close() + } + } + + private sealed class Descriptor( + uniqueId: UniqueId, + displayName: String, + source: TestSource + ) : AbstractTestDescriptor(uniqueId, displayName, source), Node { + + class Path( + uniqueId: UniqueId, + displayName: String, + source: TestSource, + val path: java.nio.file.Path + ) : Descriptor(uniqueId, displayName, source) { + + override fun getType() = Type.CONTAINER + } + + class Section( + uniqueId: UniqueId, + displayName: String, + source: TestSource, + val level: Int + ) : Descriptor(uniqueId, displayName, source) { + + override fun getType() = Type.CONTAINER + + override fun before(context: ExecutionContext): ExecutionContext { + context.replServer.handleRequest(ReplRequest.Reset("reset")) + return context + } + } + + class Snippet( + uniqueId: UniqueId, + displayName: String, + private val language: String, + source: TestSource, + private val code: String, + private val parseOnly: Boolean, + private val expectError: Boolean + ) : Descriptor(uniqueId, displayName, source) { + + override fun getType() = Type.TEST + + private val parsed: ParserRuleContext by lazy { + when (language) { + "pkl" -> Parser().parseModule(code) + "pkl-expr" -> Parser().parseExpressionInput(code) + else -> throw(Exception("Unrecognized language: $language")) + } + } + + override fun execute(context: ExecutionContext, executor: DynamicTestExecutor): ExecutionContext { + if (parseOnly) { + try { + parsed + if (expectError) { + throw AssertionError("Expected a parse error, but got none.") + } + } catch (e: LexParseException) { + if (!expectError) { + throw AssertionError(e.message) + } + } + return context + } + + context.replServer.handleRequest( + ReplRequest.Eval( + "snippet", + code, + !expectError, + !expectError + ) + ) + + val properties = parsed.children.filterIsInstance() + + val responses = mutableListOf() + + // force each property + for (prop in properties) { + responses.addAll(context.replServer.handleRequest( + ReplRequest.Eval( + "snippet", + prop.Identifier().text, + false, + true + ) + )) + } + if (expectError) { + if (responses.dropLast(1).any { it !is ReplResponse.EvalSuccess } || + responses.last() !is ReplResponse.EvalError) { + throw AssertionError( + "Expected %error snippet to fail at the end, but got the following REPL responses:\n\n" + + responses.joinToString("\n\n") { it.message }) + } + + return context + } + + val badResponses = responses.filter { it !is ReplResponse.EvalSuccess } + if (badResponses.isNotEmpty()) { + throw MultipleFailuresError(null, badResponses.map { AssertionError(it.message) }) + } + + return context + } + } + } +} diff --git a/docs/src/test/resources/META-INF/services/org.junit.platform.engine.TestEngine b/docs/src/test/resources/META-INF/services/org.junit.platform.engine.TestEngine new file mode 100644 index 00000000..b730fa81 --- /dev/null +++ b/docs/src/test/resources/META-INF/services/org.junit.platform.engine.TestEngine @@ -0,0 +1 @@ +DocSnippetTestsEngine \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..89e158ce --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# suppress inspection "UnusedProperty" for whole file + +group=org.pkl-lang +version=0.25.0 + +# google-java-format requires jdk.compiler exports +org.gradle.jvmargs= \ + -Dfile.encoding=UTF-8 \ + --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + +org.gradle.parallel=true +kotlin.stdlib.default.dependency=false +#org.gradle.workers.max=1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..5171a9a0 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,98 @@ +# NOTE: keep in sync with buildSrc/src/main/kotlin/Versions.kt until the latter can be removed +[versions] # ordered alphabetically +antlr = "4.+" +assertj = "3.+" +checksumPlugin = "1.2.0" +clikt = "3.5.1" +commonMark = "0.+" +downloadTaskPlugin = "4.1.2" +geantyref = "1.+" +# must not use `+` because used in download URL +graalVm = "22.3.1" +# intentionally empty; replaced by patch file when building pkl-cli macos/aarch64 +graalVM23JdkVersion = "replace-me" +# slightly hacky but convenient place so we remember to update the checksum +graalVmSha256-darwin-amd64 = "325afad5f1c4a07a458c95e7c444cff63514a6afa6f2655c12b4f494dccf2228" +graalVmSha256-linux-amd64 = "55547725a8be3ceb0a1da29a84cd3e958ba398ce4470ac89a8ba1bdb6d9bddb8" +graalVmSha256-linux-aarch64 = "b46a3f9c82ac70990a62282b1fbe4474e784d9ba453839a428f88e94d21f8abc" +ideaExtPlugin = "1.1" +javaPoet = "1.+" +javaxInject = "1" +jimfs = "1.+" +jansi = "2.+" +jline = "3.+" +jmh = "1.+" +jmhPlugin = "0.6.6" +jsr305 = "3.+" +junit = "5.+" +kotlin = "1.7.10" +# 1.7+ generates much more verbose code +kotlinPoet = "1.6.+" +kotlinxHtml = "0.+" +kotlinxSerialization = "1.+" +# replaces nuValidator's log4j dependency +# something related to log4j-1.2-api is apparently broken in 2.17.2 +log4j = "2.17.1" +nuValidator = "20.+" +paguro = "3.+" +shadowPlugin = "7.1.0" +slf4j = "1.+" +# Breaking change in snakeYaml 2.6 (removing DumpSettingsBuilder::setScalarResolver), so pin to 2.5 +snakeYaml = "2.5" +spotlessPlugin = "6.11.0" +msgpack = "0.9.0" +nexusPublishPlugin = "1.3.0" + +[libraries] # ordered alphabetically +antlr = { group = "com.tunnelvisionlabs", name = "antlr4", version.ref = "antlr" } +antlrRuntime = { group = "com.tunnelvisionlabs", name = "antlr4-runtime", version.ref = "antlr" } +assertj = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" } +clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt" } +commonMark = { group = "org.commonmark", name = "commonmark", version.ref = "commonMark" } +commonMarkTables = { group = "org.commonmark", name = "commonmark-ext-gfm-tables", version.ref = "commonMark" } +downloadTaskPlugin = { group = "de.undercouch", name = "gradle-download-task", version.ref = "downloadTaskPlugin" } +geantyref = { group = "io.leangen.geantyref", name = "geantyref", version.ref = "geantyref" } +graalCompiler = { group = "org.graalvm.compiler", name = "compiler", version.ref = "graalVm" } +graalSdk = { group = "org.graalvm.sdk", name = "graal-sdk", version.ref = "graalVm" } +graalJs = { group = "org.graalvm.js", name = "js", version.ref = "graalVm" } +javaPoet = { group = "com.squareup", name = "javapoet", version.ref = "javaPoet" } +javaxInject = { group = "javax.inject", name = "javax.inject", version.ref = "javaxInject" } +jimfs = { group = "com.google.jimfs", name = "jimfs", version.ref = "jimfs" } +jansi = { group = "org.fusesource.jansi", name = "jansi", version.ref = "jansi" } +jlineReader = { group = "org.jline", name = "jline-reader", version.ref = "jline" } +jlineTerminal = { group = "org.jline", name = "jline-terminal", version.ref = "jline" } +jlineTerminalJansi = { group = "org.jline", name = "jline-terminal-jansi", version.ref = "jline" } +jsr305 = { group = "com.google.code.findbugs", name = "jsr305", version.ref = "jsr305" } +junitApi = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } +junitEngine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } +junitParams = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" } +kotlinCompilerEmbeddable = { group = "org.jetbrains.kotlin", name = "kotlin-compiler-embeddable", version.ref = "kotlin" } +kotlinScriptingCompilerEmbeddable = { group = "org.jetbrains.kotlin", name = "kotlin-scripting-compiler-embeddable", version.ref = "kotlin" } +kotlinScriptUtil = { group = "org.jetbrains.kotlin", name = "kotlin-script-util", version.ref = "kotlin" } +kotlinPlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +kotlinPoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotlinPoet" } +kotlinReflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } +kotlinStdLib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlinxHtml = { group = "org.jetbrains.kotlinx", name = "kotlinx-html-jvm", version.ref = "kotlinxHtml" } +kotlinxSerializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +log4j12Api = { group = "org.apache.logging.log4j", name = "log4j-1.2-api", version.ref = "log4j" } +nuValidator = { group = "nu.validator", name = "validator", version.ref = "nuValidator" } +# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan +paguro = { group = "org.organicdesign", name = "Paguro", version.ref = "paguro" } +shadowPlugin = { group = "gradle.plugin.com.github.johnrengelman", name = "shadow", version.ref = "shadowPlugin" } +slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } +slf4jSimple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +snakeYaml = { group = "org.snakeyaml", name = "snakeyaml-engine", version.ref = "snakeYaml" } +spotlessPlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessPlugin"} +svm = { group = "org.graalvm.nativeimage", name = "svm", version.ref = "graalVm" } +truffleApi = { group = "org.graalvm.truffle", name = "truffle-api", version.ref = "graalVm" } +truffleDslProcessor = { group = "org.graalvm.truffle", name = "truffle-dsl-processor", version.ref = "graalVm" } +msgpack = { group = "org.msgpack", name = "msgpack-core", version.ref = "msgpack" } + +[plugins] # ordered alphabetically +checksum = { id = "org.gradle.crypto.checksum", version.ref = "checksumPlugin" } +ideaExt = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "ideaExtPlugin" } +jmh = { id = "me.champeau.jmh", version.ref = "jmhPlugin" } +kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadowPlugin" } +nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublishPlugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..41d9927a Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..8049c684 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/patches/graalVm23.patch b/patches/graalVm23.patch new file mode 100644 index 00000000..13df04fb --- /dev/null +++ b/patches/graalVm23.patch @@ -0,0 +1,529 @@ +diff --git a/bench/gradle.lockfile b/bench/gradle.lockfile +index 0673df5..3a9d913 100644 +--- a/bench/gradle.lockfile ++++ b/bench/gradle.lockfile +@@ -8,9 +8,9 @@ net.sf.jopt-simple:jopt-simple:5.0.4=jmh,jmhCompileClasspath,jmhImplementationDe + org.apache.commons:commons-math3:3.2=jmh,jmhCompileClasspath,jmhImplementationDependenciesMetadata,jmhRuntimeClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.compiler:compiler:22.3.1=graal +-org.graalvm.sdk:graal-sdk:22.3.1=graal,jmh,jmhRuntimeClasspath,truffle +-org.graalvm.truffle:truffle-api:22.3.1=graal,jmh,jmhRuntimeClasspath,truffle ++org.graalvm.compiler:compiler:23.0.2=graal ++org.graalvm.sdk:graal-sdk:23.0.2=graal,jmh,jmhRuntimeClasspath,truffle ++org.graalvm.truffle:truffle-api:23.0.2=graal,jmh,jmhRuntimeClasspath,truffle + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts +index 73eb367..6e526b6 100644 +--- a/buildSrc/build.gradle.kts ++++ b/buildSrc/build.gradle.kts +@@ -12,6 +12,6 @@ dependencies { + } + + java { +- sourceCompatibility = JavaVersion.VERSION_11 +- targetCompatibility = JavaVersion.VERSION_11 ++ sourceCompatibility = JavaVersion.VERSION_17 ++ targetCompatibility = JavaVersion.VERSION_17 + } +diff --git a/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts +index 4daf287..6ef0256 100644 +--- a/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts ++++ b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts +@@ -24,13 +24,13 @@ configurations { + + plugins.withType(JavaPlugin::class).configureEach { + val java = project.extensions.getByType() +- java.sourceCompatibility = JavaVersion.VERSION_11 +- java.targetCompatibility = JavaVersion.VERSION_11 ++ java.sourceCompatibility = JavaVersion.VERSION_17 ++ java.targetCompatibility = JavaVersion.VERSION_17 + } + + tasks.withType().configureEach { + kotlinOptions { +- jvmTarget = "11" ++ jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf("-Xjsr305=strict", "-Xjvm-default=all") + } + } +diff --git a/docs/gradle.lockfile b/docs/gradle.lockfile +index 196e592..e33eb75 100644 +--- a/docs/gradle.lockfile ++++ b/docs/gradle.lockfile +@@ -7,8 +7,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml +index 5171a9a..6761c58 100644 +--- a/gradle/libs.versions.toml ++++ b/gradle/libs.versions.toml +@@ -8,11 +8,11 @@ commonMark = "0.+" + downloadTaskPlugin = "4.1.2" + geantyref = "1.+" + # must not use `+` because used in download URL +-graalVm = "22.3.1" +-# intentionally empty; replaced by patch file when building pkl-cli macos/aarch64 +-graalVM23JdkVersion = "replace-me" ++graalVm = "23.0.2" ++graalVM23JdkVersion = "17.0.9" + # slightly hacky but convenient place so we remember to update the checksum + graalVmSha256-darwin-amd64 = "325afad5f1c4a07a458c95e7c444cff63514a6afa6f2655c12b4f494dccf2228" ++graalVmSha256-macos-aarch64 = "2214b6ecb32faacc84dffcbfae930450abe77c31730c4b6310e22d8f743959a5" + graalVmSha256-linux-amd64 = "55547725a8be3ceb0a1da29a84cd3e958ba398ce4470ac89a8ba1bdb6d9bddb8" + graalVmSha256-linux-aarch64 = "b46a3f9c82ac70990a62282b1fbe4474e784d9ba453839a428f88e94d21f8abc" + ideaExtPlugin = "1.1" +diff --git a/pkl-cli/gradle.lockfile b/pkl-cli/gradle.lockfile +index 1360caa..0892665 100644 +--- a/pkl-cli/gradle.lockfile ++++ b/pkl-cli/gradle.lockfile +@@ -9,13 +9,13 @@ net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.fusesource.jansi:jansi:2.4.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.compiler:compiler:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +-org.graalvm.nativeimage:native-image-base:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +-org.graalvm.nativeimage:objectfile:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +-org.graalvm.nativeimage:pointsto:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +-org.graalvm.nativeimage:svm:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +-org.graalvm.sdk:graal-sdk:22.3.1=compileClasspath,compileOnlyDependenciesMetadata,default,runtimeClasspath,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=compileClasspath,compileOnlyDependenciesMetadata,default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.compiler:compiler:23.0.2=compileClasspath,compileOnlyDependenciesMetadata ++org.graalvm.nativeimage:native-image-base:23.0.2=compileClasspath,compileOnlyDependenciesMetadata ++org.graalvm.nativeimage:objectfile:23.0.2=compileClasspath,compileOnlyDependenciesMetadata ++org.graalvm.nativeimage:pointsto:23.0.2=compileClasspath,compileOnlyDependenciesMetadata ++org.graalvm.nativeimage:svm:23.0.2=compileClasspath,compileOnlyDependenciesMetadata ++org.graalvm.sdk:graal-sdk:23.0.2=compileClasspath,compileOnlyDependenciesMetadata,default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=compileClasspath,compileOnlyDependenciesMetadata,default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -26,10 +26,14 @@ org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspat + org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath + org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.jline:jline-native:3.23.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.jline:jline-reader:3.23.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +@@ -45,4 +49,4 @@ org.msgpack:msgpack-core:0.9.0=default,runtimeClasspath,testRuntimeClasspath + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +-empty=annotationProcessor,archives,compile,intransitiveDependenciesMetadata,javaExecutable,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,shadow,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,archives,compile,intransitiveDependenciesMetadata,javaExecutable,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,shadow,signatures,sourcesJar,stagedAlpineLinuxAmd64Executable,stagedLinuxAarch64Executable,stagedLinuxAmd64Executable,stagedMacAarch64Executable,stagedMacAmd64Executable,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-codegen-java/gradle.lockfile b/pkl-codegen-java/gradle.lockfile +index 9bf6cba..901c8d8 100644 +--- a/pkl-codegen-java/gradle.lockfile ++++ b/pkl-codegen-java/gradle.lockfile +@@ -10,8 +10,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -22,10 +22,14 @@ org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspat + org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath + org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +@@ -36,4 +40,4 @@ org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMet + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +-empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-codegen-kotlin/gradle.lockfile b/pkl-codegen-kotlin/gradle.lockfile +index 42331c2..7974715 100644 +--- a/pkl-codegen-kotlin/gradle.lockfile ++++ b/pkl-codegen-kotlin/gradle.lockfile +@@ -10,8 +10,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testRuntimeClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.jetbrains.kotlin:kotlin-daemon-client:1.7.10=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +@@ -24,10 +24,14 @@ org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspat + org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,testRuntimeClasspath + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath + org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +@@ -39,4 +43,4 @@ org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMet + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +-empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-commons-cli/gradle.lockfile b/pkl-commons-cli/gradle.lockfile +index 6cac9f4..f603276 100644 +--- a/pkl-commons-cli/gradle.lockfile ++++ b/pkl-commons-cli/gradle.lockfile +@@ -8,8 +8,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -20,10 +20,14 @@ org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspat + org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath + org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +@@ -34,4 +38,4 @@ org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMet + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +-empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-commons/gradle.lockfile b/pkl-commons/gradle.lockfile +index 4170e1a..0a23c04 100644 +--- a/pkl-commons/gradle.lockfile ++++ b/pkl-commons/gradle.lockfile +@@ -27,4 +27,4 @@ org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImpleme + org.junit.platform:junit-platform-engine:1.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +-empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-config-java/gradle.lockfile b/pkl-config-java/gradle.lockfile +index 271ca49..3648401 100644 +--- a/pkl-config-java/gradle.lockfile ++++ b/pkl-config-java/gradle.lockfile +@@ -11,8 +11,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -23,10 +23,14 @@ org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspat + org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=pklCodegenJava ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=pklCodegenJava ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=pklCodegenJava ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=pklCodegenJava + org.jetbrains:annotations:13.0=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.junit.jupiter:junit-jupiter-engine:5.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +@@ -37,4 +41,4 @@ org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMet + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.organicdesign:Paguro:3.10.3=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath +-empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileOnly,compileOnlyDependenciesMetadata,fatJar,firstPartySourcesJars,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklCoreSourcesJar,runtime,runtimeOnlyDependenciesMetadata,shadow,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileOnly,compileOnlyDependenciesMetadata,fatJar,firstPartySourcesJars,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklCoreSourcesJar,runtime,runtimeOnlyDependenciesMetadata,shadow,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-config-kotlin/gradle.lockfile b/pkl-config-kotlin/gradle.lockfile +index 681ecdb..5c8f48b 100644 +--- a/pkl-config-kotlin/gradle.lockfile ++++ b/pkl-config-kotlin/gradle.lockfile +@@ -10,8 +10,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -22,10 +22,14 @@ org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspat + org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=pklCodegenKotlin ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=pklCodegenKotlin ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=pklCodegenKotlin ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=pklCodegenKotlin + org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.junit.jupiter:junit-jupiter-engine:5.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +@@ -36,4 +40,4 @@ org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMet + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.organicdesign:Paguro:3.10.3=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath +-empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklConfigJavaAll,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklConfigJavaAll,runtime,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-core/gradle.lockfile b/pkl-core/gradle.lockfile +index a4ad3ca..cf5be68 100644 +--- a/pkl-core/gradle.lockfile ++++ b/pkl-core/gradle.lockfile +@@ -13,9 +13,9 @@ org.antlr:ST4:4.3=antlr + org.antlr:antlr-runtime:3.5.2=antlr + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=compileClasspath,default,generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=compileClasspath,default,generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.truffle:truffle-dsl-processor:22.3.1=annotationProcessor ++org.graalvm.sdk:graal-sdk:23.0.2=compileClasspath,default,generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=compileClasspath,default,generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.truffle:truffle-dsl-processor:23.0.2=annotationProcessor + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -41,4 +41,4 @@ org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenc + org.organicdesign:Paguro:3.10.3=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.slf4j:slf4j-api:1.7.32=compileOnly + org.snakeyaml:snakeyaml-engine:2.5=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-empty=apiDependenciesMetadata,archives,compile,generatorAnnotationProcessor,generatorApiDependenciesMetadata,generatorCompileOnly,generatorCompileOnlyDependenciesMetadata,generatorIntransitiveDependenciesMetadata,generatorKotlinScriptDef,generatorKotlinScriptDefExtensions,generatorRuntimeOnlyDependenciesMetadata,intransitiveDependenciesMetadata,javaExecutable,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=apiDependenciesMetadata,archives,compile,generatorAnnotationProcessor,generatorApiDependenciesMetadata,generatorCompileOnly,generatorCompileOnlyDependenciesMetadata,generatorIntransitiveDependenciesMetadata,generatorKotlinScriptDef,generatorKotlinScriptDefExtensions,generatorRuntimeOnlyDependenciesMetadata,intransitiveDependenciesMetadata,javaExecutable,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-doc/gradle.lockfile b/pkl-doc/gradle.lockfile +index 9a78e5a..629fd77 100644 +--- a/pkl-doc/gradle.lockfile ++++ b/pkl-doc/gradle.lockfile +@@ -11,7 +11,7 @@ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=test + com.google.j2objc:j2objc-annotations:1.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + com.google.jimfs:jimfs:1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + com.ibm.icu:icu4j:58.2=validator +-com.ibm.icu:icu4j:71.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++com.ibm.icu:icu4j:72.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + com.shapesecurity:salvation:2.7.2=validator + com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,runtimeClasspath,testRuntimeClasspath + commons-codec:commons-codec:1.10=validator +@@ -38,10 +38,10 @@ org.commonmark:commonmark-ext-gfm-tables:0.21.0=compileClasspath,default,impleme + org.commonmark:commonmark:0.21.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.eclipse.jetty:jetty-util-ajax:9.4.18.v20190429=validator + org.eclipse.jetty:jetty-util:9.4.18.v20190429=validator +-org.graalvm.js:js:22.3.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.regex:regex:22.3.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.js:js:23.0.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.regex:regex:23.0.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=default,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=default,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -56,10 +56,14 @@ org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerP + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-serialization:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testImplementationDependenciesMetadata ++org.jetbrains.kotlin:kotlin-stdlib:1.8.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath + org.jetbrains.kotlin:kotlin-tooling-core:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-util-io:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.1=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +@@ -78,4 +82,4 @@ org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMet + org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +-empty=annotationProcessor,archives,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions ++empty=annotationProcessor,archives,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions +diff --git a/pkl-executor/gradle.lockfile b/pkl-executor/gradle.lockfile +index c5fa512..0d31a41 100644 +--- a/pkl-executor/gradle.lockfile ++++ b/pkl-executor/gradle.lockfile +@@ -6,8 +6,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +@@ -34,4 +34,4 @@ org.organicdesign:Paguro:3.10.3=testRuntimeClasspath + org.slf4j:slf4j-api:1.7.36=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.slf4j:slf4j-simple:1.7.36=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.snakeyaml:snakeyaml-engine:2.5=testRuntimeClasspath +-empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklDistribution,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime ++empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklDistribution,runtime,runtimeOnlyDependenciesMetadata,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime +diff --git a/pkl-gradle/gradle.lockfile b/pkl-gradle/gradle.lockfile +index 4db8f8d..d8eaca1 100644 +--- a/pkl-gradle/gradle.lockfile ++++ b/pkl-gradle/gradle.lockfile +@@ -17,10 +17,14 @@ org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspat + org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest + org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=compileClasspath,compileOnlyDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=compileClasspath,compileOnlyDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=compileClasspath,compileOnlyDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=compileClasspath,compileOnlyDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=compileOnlyDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=compileClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=compileOnlyDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=compileClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=compileOnlyDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=compileClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.10=compileOnlyDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=compileClasspath + org.jetbrains:annotations:13.0=compileClasspath,compileOnlyDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata + org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +diff --git a/pkl-server/gradle.lockfile b/pkl-server/gradle.lockfile +index 22b6965..813fd46 100644 +--- a/pkl-server/gradle.lockfile ++++ b/pkl-server/gradle.lockfile +@@ -6,8 +6,8 @@ net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependen + net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata + org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath + org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath + org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +diff --git a/pkl-tools/gradle.lockfile b/pkl-tools/gradle.lockfile +index a04dc3b..c1a960c 100644 +--- a/pkl-tools/gradle.lockfile ++++ b/pkl-tools/gradle.lockfile +@@ -10,13 +10,15 @@ io.leangen.geantyref:geantyref:1.3.14=default,runtimeClasspath,testRuntimeClassp + org.commonmark:commonmark-ext-gfm-tables:0.21.0=default,runtimeClasspath,testRuntimeClasspath + org.commonmark:commonmark:0.21.0=default,runtimeClasspath,testRuntimeClasspath + org.fusesource.jansi:jansi:2.4.0=default,runtimeClasspath,testRuntimeClasspath +-org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testRuntimeClasspath +-org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.sdk:graal-sdk:23.0.2=default,runtimeClasspath,testRuntimeClasspath ++org.graalvm.truffle:truffle-api:23.0.2=default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.kotlin:kotlin-reflect:1.7.10=default,runtimeClasspath,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.7.21=compileClasspath,testCompileClasspath ++org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=default,runtimeClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.7.21=compileClasspath,testCompileClasspath ++org.jetbrains.kotlin:kotlin-stdlib:1.8.21=default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.1=default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.1=default,runtimeClasspath,testRuntimeClasspath + org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.5.1=default,runtimeClasspath,testRuntimeClasspath +diff --git a/stdlib/gradle.lockfile b/stdlib/gradle.lockfile +index 00a8691..7e06c36 100644 +--- a/stdlib/gradle.lockfile ++++ b/stdlib/gradle.lockfile +@@ -6,12 +6,12 @@ com.github.ajalt.clikt:clikt:3.5.1=pkldoc + com.tunnelvisionlabs:antlr4-runtime:4.9.0=pkldoc + org.commonmark:commonmark-ext-gfm-tables:0.21.0=pkldoc + org.commonmark:commonmark:0.21.0=pkldoc +-org.graalvm.sdk:graal-sdk:22.3.1=pkldoc +-org.graalvm.truffle:truffle-api:22.3.1=pkldoc +-org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=pkldoc +-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=pkldoc +-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=pkldoc +-org.jetbrains.kotlin:kotlin-stdlib:1.7.10=pkldoc ++org.graalvm.sdk:graal-sdk:23.0.2=pkldoc ++org.graalvm.truffle:truffle-api:23.0.2=pkldoc ++org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=pkldoc ++org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.21=pkldoc ++org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.21=pkldoc ++org.jetbrains.kotlin:kotlin-stdlib:1.8.21=pkldoc + org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.1=pkldoc + org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.1=pkldoc + org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.5.1=pkldoc diff --git a/pkl-cli/README.adoc b/pkl-cli/README.adoc new file mode 100644 index 00000000..292b383f --- /dev/null +++ b/pkl-cli/README.adoc @@ -0,0 +1,6 @@ +Command-line interface for Pkl. + +The CLI provides the following tools: + +* Batch evaluator +* REPL diff --git a/pkl-cli/gradle.lockfile b/pkl-cli/gradle.lockfile new file mode 100644 index 00000000..1360caab --- /dev/null +++ b/pkl-cli/gradle.lockfile @@ -0,0 +1,48 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.github.ajalt.clikt:clikt-jvm:3.5.1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt:3.5.1=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,runtimeClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.fusesource.jansi:jansi:2.4.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.compiler:compiler:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +org.graalvm.nativeimage:native-image-base:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +org.graalvm.nativeimage:objectfile:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +org.graalvm.nativeimage:pointsto:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +org.graalvm.nativeimage:svm:22.3.1=compileClasspath,compileOnlyDependenciesMetadata +org.graalvm.sdk:graal-sdk:22.3.1=compileClasspath,compileOnlyDependenciesMetadata,default,runtimeClasspath,testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=compileClasspath,compileOnlyDependenciesMetadata,default,runtimeClasspath,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jline:jline-native:3.23.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jline:jline-reader:3.23.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jline:jline-terminal-jansi:3.23.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jline:jline-terminal:3.23.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.msgpack:msgpack-core:0.9.0=default,runtimeClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +empty=annotationProcessor,archives,compile,intransitiveDependenciesMetadata,javaExecutable,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,shadow,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-cli/pkl-cli.gradle.kts b/pkl-cli/pkl-cli.gradle.kts new file mode 100644 index 00000000..75f98326 --- /dev/null +++ b/pkl-cli/pkl-cli.gradle.kts @@ -0,0 +1,380 @@ +plugins { + pklAllProjects + pklKotlinLibrary + pklPublishLibrary + pklNativeBuild + `maven-publish` + + // already on build script class path (see buildSrc/build.gradle.kts), + // hence must only specify plugin ID here + @Suppress("DSL_SCOPE_VIOLATION") + id(libs.plugins.shadow.get().pluginId) + + @Suppress("DSL_SCOPE_VIOLATION") + alias(libs.plugins.checksum) +} + +// make Java executable available to other subprojects +val javaExecutableConfiguration: Configuration = configurations.create("javaExecutable") + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-cli") + description.set("Pkl CLI Java library.") + } + } + } +} + +val stagedMacAmd64Executable: Configuration by configurations.creating +val stagedMacAarch64Executable: Configuration by configurations.creating +val stagedLinuxAmd64Executable: Configuration by configurations.creating +val stagedLinuxAarch64Executable: Configuration by configurations.creating +val stagedAlpineLinuxAmd64Executable: Configuration by configurations.creating + +dependencies { + compileOnly(libs.svm) + + // CliEvaluator exposes PClass + api(project(":pkl-core")) + // CliEvaluatorOptions exposes CliBaseOptions + api(project(":pkl-commons-cli")) + + implementation(project(":pkl-commons")) + implementation(libs.jansi) + implementation(libs.jlineReader) + implementation(libs.jlineTerminal) + implementation(libs.jlineTerminalJansi) + implementation(project(":pkl-server")) + implementation(libs.clikt) { + // force clikt to use our version of the kotlin stdlib + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-common") + } + + testImplementation(project(":pkl-commons-test")) + + stagedMacAmd64Executable(files("$buildDir/executable/pkl-macos-amd64")) + stagedMacAarch64Executable(files("$buildDir/executable/pkl-macos-aarch64")) + stagedLinuxAmd64Executable(files("$buildDir/executable/pkl-linux-amd64")) + stagedLinuxAarch64Executable(files("$buildDir/executable/pkl-linux-aarch64")) + stagedAlpineLinuxAmd64Executable(files("$buildDir/executable/pkl-alpine-linux-amd64")) +} + +tasks.jar { + manifest { + attributes += mapOf("Main-Class" to "org.pkl.cli.Main") + } + + // not required at runtime + exclude("org/pkl/cli/svm/**") +} + +tasks.javadoc { + enabled = false +} + +tasks.shadowJar { + archiveFileName.set("jpkl") + + exclude("META-INF/maven/**") + exclude("META-INF/upgrade/**") + + // org.antlr.v4.runtime.misc.RuleDependencyProcessor + exclude("META-INF/services/javax.annotation.processing.Processor") + + exclude("module-info.*") +} + +val javaExecutable by tasks.registering(ExecutableJar::class) { + inJar.set(tasks.shadowJar.flatMap { it.archiveFile }) + outJar.set(file("$buildDir/executable/jpkl")) + + // uncomment for debugging + //jvmArgs.addAll("-ea", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") +} + +val testJavaExecutable by tasks.registering(Test::class) { + testClassesDirs = tasks.test.get().testClassesDirs + classpath = + // compiled test classes + sourceSets.test.get().output + + // java executable + javaExecutable.get().outputs.files + + // test-only dependencies + // (test dependencies that are also main dependencies must already be contained in java executable; + // to verify that, we don't want to include them here) + (configurations.testRuntimeClasspath.get() - configurations.runtimeClasspath.get()) +} + +tasks.check { + dependsOn(testJavaExecutable) +} + +// 0.14 Java executable was broken because javaExecutable.jvmArgs wasn't commented out. +// To catch this and similar problems, test that Java executable starts successfully. +val testStartJavaExecutable by tasks.registering(Exec::class) { + dependsOn(javaExecutable) +val outputFile = file("$buildDir/testStartJavaExecutable") // dummy output to satisfy up-to-date check + outputs.file(outputFile) + + executable = javaExecutable.get().outputs.files.singleFile.toString() + args("--version") + + doFirst { outputFile.delete() } + + doLast { outputFile.writeText("OK") } +} + +tasks.check { + dependsOn(testStartJavaExecutable) +} + +fun Exec.configureExecutable(isEnabled: Boolean, outputFile: File, extraArgs: List = listOf()) { + enabled = isEnabled + dependsOn(":installGraalVm") + + inputs.files(sourceSets.main.map { it.output }) + inputs.files(configurations.runtimeClasspath) + outputs.file(outputFile) + + workingDir = outputFile.parentFile + executable = "${buildInfo.graalVm.baseDir}/bin/native-image" + + // JARs to exclude from the class path for the native-image build. + val exclusions = + if (buildInfo.graalVm.isGraal22) emptyList() + else listOf(libs.truffleApi, libs.graalSdk).map { it.get().module.name } + // https://www.graalvm.org/22.0/reference-manual/native-image/Options/ + argumentProviders.add(CommandLineArgumentProvider { + listOf( + // currently gives a deprecation warning, but we've been told + // that the "initialize everything at build time" *CLI* option is likely here to stay + "--initialize-at-build-time=" + ,"--no-fallback" + ,"-H:IncludeResources=org/pkl/core/stdlib/.*\\.pkl" + ,"-H:IncludeResources=org/jline/utils/.*" + ,"-H:IncludeResources=org/pkl/commons/cli/commands/IncludedCARoots.pem" + //,"-H:IncludeResources=org/pkl/core/Release.properties" + ,"-H:IncludeResourceBundles=org.pkl.core.errorMessages" + ,"--macro:truffle" + ,"-H:Class=org.pkl.cli.Main" + ,"-H:Name=${outputFile.name}" + //,"--native-image-info" + //,"-Dpolyglot.image-build-time.PreinitializeContexts=pkl" + // the actual limit (currently) used by native-image is this number + 1400 (idea is to compensate for Truffle's own nodes) + ,"-H:MaxRuntimeCompileMethods=1800" + ,"-H:+EnforceMaxRuntimeCompileMethods" + ,"--enable-url-protocols=http,https" + //,"--install-exit-handlers" + ,"-H:+ReportExceptionStackTraces" + ,"-H:-ParseRuntimeOptions" // disable automatic support for JVM CLI options (puts our main class in full control of argument parsing) + //,"-H:+PrintAnalysisCallTree" + //,"-H:PrintAnalysisCallTreeType=CSV" + //,"-H:+PrintImageObjectTree" + //,"--features=org.pkl.cli.svm.InitFeature" + //,"-H:Dump=:2" + //,"-H:MethodFilter=ModuleCache.getOrLoad*,VmLanguage.loadModule" + //,"-g" + //,"-verbose" + //,"--debug-attach" + //,"-H:+AllowVMInspection" + //,"-H:+PrintHeapHistogram" + //,"-H:+ReportDeletedElementsAtRuntime" + //,"-H:+PrintMethodHistogram" + //,"-H:+PrintRuntimeCompileMethods" + //,"-H:NumberOfThreads=1" + //,"-J-Dtruffle.TruffleRuntime=com.oracle.truffle.api.impl.DefaultTruffleRuntime" + //,"-J-Dcom.oracle.truffle.aot=true" + //,"-J:-ea" + //,"-J:-esa" + // for use with https://www.graalvm.org/docs/tools/dashboard/ + //,"-H:DashboardDump=dashboard.dump", "-H:+DashboardAll" + // native-image rejects non-existing class path entries -> filter + ,"--class-path" + ,((sourceSets.main.get().output + configurations.runtimeClasspath.get()) + .filter { it.exists() && !exclusions.any { exclude -> it.name.contains(exclude) }}) + .asPath + // make sure dev machine stays responsive (15% slowdown on my laptop) + ,"-J-XX:ActiveProcessorCount=${ + Runtime.getRuntime().availableProcessors() / (if (buildInfo.os.isMacOsX && !buildInfo.isCiBuild) 4 else 1) + }" + ) + extraArgs + }) +} + +/** + * Builds the pkl CLI for macOS/amd64. + */ +val macExecutableAmd64: TaskProvider by tasks.registering(Exec::class) { + configureExecutable(buildInfo.os.isMacOsX && buildInfo.graalVm.isGraal22, file("$buildDir/executable/pkl-macos-amd64")) +} + +/** + * Builds the pkl CLI for macOS/aarch64. + * + * This requires that GraalVM be set to version 23.0 or greater, because 22.x does not support this + * os/arch pair. + */ +val macExecutableAarch64: TaskProvider by tasks.registering(Exec::class) { + configureExecutable( + buildInfo.os.isMacOsX && !buildInfo.graalVm.isGraal22, + file("$buildDir/executable/pkl-macos-aarch64"), + listOf( + "--initialize-at-run-time=org.msgpack.core.buffer.DirectBufferAccess", + "-H:+AllowDeprecatedBuilderClassesOnImageClasspath" + ) + ) +} + +/** + * Builds the pkl CLI for linux/amd64. + */ +val linuxExecutableAmd64: TaskProvider by tasks.registering(Exec::class) { + configureExecutable(buildInfo.os.isLinux && buildInfo.arch == "amd64", file("$buildDir/executable/pkl-linux-amd64")) +} + +/** + * Builds the pkl CLI for linux/aarch64. + * + * Right now, this is built within a container on Mac using emulation because CI does not have + * ARM instances. + */ +val linuxExecutableAarch64: TaskProvider by tasks.registering(Exec::class) { + configureExecutable(buildInfo.os.isLinux && buildInfo.arch == "aarch64", file("$buildDir/executable/pkl-linux-aarch64")) +} + +/** + * Builds a statically linked CLI for linux/amd64. + * + * Note: we don't publish the same for linux/aarch64 because native-image doesn't support this. + * Details: https://www.graalvm.org/22.0/reference-manual/native-image/ARM64/ + */ +val alpineExecutableAmd64: TaskProvider by tasks.registering(Exec::class) { + configureExecutable( + buildInfo.os.isLinux && buildInfo.arch == "amd64", + file("$buildDir/executable/pkl-alpine-linux-amd64"), + listOf( + "--static", + "--libc=musl", + "-H:CCompilerOption=-Wl,-z,stack-size=10485760", + "-Dorg.pkl.compat=alpine" + ) + ) +} + +tasks.assembleNative { + dependsOn(macExecutableAmd64, macExecutableAarch64, linuxExecutableAmd64, linuxExecutableAarch64, alpineExecutableAmd64) +} + +// make Java executable available to other subprojects +// (we don't do the same for native executables because we don't want tasks assemble/build to build them) +artifacts { + add("javaExecutable", javaExecutable.map { it.outputs.files.singleFile }) { + name = "pkl-cli-java" + classifier = null + extension = "jar" + builtBy(javaExecutable) + } +} + +//region Maven Publishing +publishing { + publications { + register("javaExecutable") { + artifactId = "pkl-cli-java" + + artifact(javaExecutable.map { it.outputs.files.singleFile }) { + classifier = null + extension = "jar" + builtBy(javaExecutable) + } + + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-cli") + description.set(""" + Pkl CLI executable for Java. + Can be executed directly on *nix (if the `java` command is found on the PATH) and with `java -jar` otherwise. + Requires Java 11 or higher. + """.trimIndent()) + } + } + create("macExecutableAmd64") { + artifactId = "pkl-cli-macos-amd64" + artifact(stagedMacAmd64Executable.singleFile) { + classifier = null + extension = "bin" + builtBy(stagedMacAmd64Executable) + } + pom { + name.set("pkl-cli-macos-amd64") + url.set("https://github.com/apple/pkl/tree/main/pkl-cli") + description.set("Native Pkl CLI executable for macOS/amd64.") + } + } + create("macExecutableAarch64") { + artifactId = "pkl-cli-macos-aarch64" + artifact(stagedMacAarch64Executable.singleFile) { + classifier = null + extension = "bin" + builtBy(stagedMacAarch64Executable) + } + pom { + name.set("pkl-cli-macos-aarch64") + url.set("https://github.com/apple/pkl/tree/main/pkl-cli") + description.set("Native Pkl CLI executable for macOS/aarch64.") + } + } + create("linuxExecutableAmd64") { + artifactId = "pkl-cli-linux-amd64" + artifact(stagedLinuxAmd64Executable.singleFile) { + classifier = null + extension = "bin" + builtBy(stagedLinuxAmd64Executable) + } + pom { + name.set("pkl-cli-linux-amd64") + url.set("https://github.com/apple/pkl/tree/main/pkl-cli") + description.set("Native Pkl CLI executable for linux/amd64.") + } + } + create("linuxExecutableAarch64") { + artifactId = "pkl-cli-linux-aarch64" + artifact(stagedLinuxAarch64Executable.singleFile) { + classifier = null + extension = "bin" + builtBy(stagedLinuxAarch64Executable) + } + pom { + name.set("pkl-cli-linux-aarch64") + url.set("https://github.com/apple/pkl/tree/main/pkl-cli") + description.set("Native Pkl CLI executable for linux/aarch64.") + } + } + create("alpineLinuxExecutableAmd64") { + artifactId = "pkl-cli-alpine-linux-amd64" + artifact(stagedAlpineLinuxAmd64Executable.singleFile) { + classifier = null + extension = "bin" + builtBy(stagedAlpineLinuxAmd64Executable) + } + pom { + name.set("pkl-cli-alpine-linux-amd64") + url.set("https://github.com/apple/pkl/tree/main/pkl-cli") + description.set("Native Pkl CLI executable for linux/amd64 and statically linked to musl.") + } + } + } +} + +signing { + sign(publishing.publications["javaExecutable"]) + sign(publishing.publications["linuxExecutableAarch64"]) + sign(publishing.publications["linuxExecutableAmd64"]) + sign(publishing.publications["macExecutableAarch64"]) + sign(publishing.publications["macExecutableAmd64"]) + sign(publishing.publications["alpineLinuxExecutableAmd64"]) +} +//endregion diff --git a/pkl-cli/src/main/java/org/pkl/cli/svm/InitFeature.java b/pkl-cli/src/main/java/org/pkl/cli/svm/InitFeature.java new file mode 100644 index 00000000..b1880a82 --- /dev/null +++ b/pkl-cli/src/main/java/org/pkl/cli/svm/InitFeature.java @@ -0,0 +1,35 @@ +/** + * 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.cli.svm; + +import org.graalvm.nativeimage.hosted.Feature; +import org.pkl.core.runtime.BaseModule; + +/** + * This class is registered with native-image via a CLI option (see Gradle task `nativeExecutable`). + */ +@SuppressWarnings({"unused", "ResultOfMethodCallIgnored"}) +public final class InitFeature implements Feature { + /** + * Enforce that {@link BaseModule#getModule()}'s static initializer completes before depending + * static initializers are invoked. This is necessary to avoid deadlocks in native-image's + * multi-threaded execution of static initializers. It's not clear at this point if multi-threaded + * initialization on the JVM could also deadlock, i.e., if this is a Pkl bug. + */ + public void duringSetup(DuringSetupAccess access) { + BaseModule.getModule(); + } +} diff --git a/pkl-cli/src/main/java/org/pkl/cli/svm/MessagePackRecomputations.java b/pkl-cli/src/main/java/org/pkl/cli/svm/MessagePackRecomputations.java new file mode 100644 index 00000000..aae64b17 --- /dev/null +++ b/pkl-cli/src/main/java/org/pkl/cli/svm/MessagePackRecomputations.java @@ -0,0 +1,34 @@ +/** + * 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.cli.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +/** + * native-image can't determine how calls to {@link sun.misc.Unsafe#arrayBaseOffset(Class)} affect + * static fields in msgpack's {@code MessageBuffer}. + * + *

This informs the compiler which field to re-compute. + */ +@SuppressWarnings("unused") +@TargetClass(className = "org.msgpack.core.buffer.MessageBuffer") +final class MessagePackRecomputations { + @Alias + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayBaseOffset, declClass = byte[].class) + static int ARRAY_BYTE_BASE_OFFSET; +} diff --git a/pkl-cli/src/main/java/org/pkl/cli/svm/PolyglotContextImplTarget.java b/pkl-cli/src/main/java/org/pkl/cli/svm/PolyglotContextImplTarget.java new file mode 100644 index 00000000..026c24be --- /dev/null +++ b/pkl-cli/src/main/java/org/pkl/cli/svm/PolyglotContextImplTarget.java @@ -0,0 +1,52 @@ +/** + * 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.cli.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.truffle.TruffleFeature; +import java.util.Map; + +/** + * Workaround to prevent the native-image build error "Detected a started Thread in the image + * heap.". The cause of this error is the use of {@link org.graalvm.polyglot.Context} in the + * (intentionally) statically reachable class {@link org.pkl.core.runtime.StdLibModule}. + * + *

A cleaner solution would be to have a separate {@link org.pkl.core.ast.builder.AstBuilder} for + * stdlib modules that produces a fully initialized module object without executing any Truffle + * nodes. + * + *

This class is automatically discovered by native-image; no registration is required. + */ +@SuppressWarnings({"unused", "ClassName"}) +@TargetClass( + className = "com.oracle.truffle.polyglot.PolyglotContextImpl", + onlyWith = {TruffleFeature.IsEnabled.class}) +public final class PolyglotContextImplTarget { + @Alias + @RecomputeFieldValue(kind = Kind.NewInstance, declClassName = "java.util.HashMap") + public Map threads; + + @Alias + @RecomputeFieldValue(kind = Kind.Reset) + public WeakAssumedValueTarget singleThreadValue; + + @Alias + @RecomputeFieldValue(kind = Kind.Reset) + public PolyglotThreadInfoTarget cachedThreadInfo; +} diff --git a/pkl-cli/src/main/java/org/pkl/cli/svm/PolyglotThreadInfoTarget.java b/pkl-cli/src/main/java/org/pkl/cli/svm/PolyglotThreadInfoTarget.java new file mode 100644 index 00000000..708bed21 --- /dev/null +++ b/pkl-cli/src/main/java/org/pkl/cli/svm/PolyglotThreadInfoTarget.java @@ -0,0 +1,25 @@ +/** + * 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.cli.svm; + +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.truffle.TruffleFeature; + +/** Makes non-public class PolyglotThreadInfo usable above. */ +@TargetClass( + className = "com.oracle.truffle.polyglot.PolyglotThreadInfo", + onlyWith = {TruffleFeature.IsEnabled.class}) +public final class PolyglotThreadInfoTarget {} diff --git a/pkl-cli/src/main/java/org/pkl/cli/svm/ThreadLocalHandshakeTarget.java b/pkl-cli/src/main/java/org/pkl/cli/svm/ThreadLocalHandshakeTarget.java new file mode 100644 index 00000000..21d4a9db --- /dev/null +++ b/pkl-cli/src/main/java/org/pkl/cli/svm/ThreadLocalHandshakeTarget.java @@ -0,0 +1,33 @@ +/** + * 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.cli.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.truffle.TruffleFeature; +import java.util.Map; + +@SuppressWarnings("unused") +@TargetClass( + className = "com.oracle.truffle.api.impl.ThreadLocalHandshake", + onlyWith = {TruffleFeature.IsEnabled.class}) +public final class ThreadLocalHandshakeTarget { + @Alias + @RecomputeFieldValue(kind = Kind.NewInstance, declClassName = "java.util.HashMap") + static Map SAFEPOINTS; +} diff --git a/pkl-cli/src/main/java/org/pkl/cli/svm/WeakAssumedValueTarget.java b/pkl-cli/src/main/java/org/pkl/cli/svm/WeakAssumedValueTarget.java new file mode 100644 index 00000000..6b97e345 --- /dev/null +++ b/pkl-cli/src/main/java/org/pkl/cli/svm/WeakAssumedValueTarget.java @@ -0,0 +1,25 @@ +/** + * 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.cli.svm; + +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.truffle.TruffleFeature; + +/** Makes non-public class WeakAssumedValue usable. */ +@TargetClass( + className = "com.oracle.truffle.polyglot.WeakAssumedValue", + onlyWith = {TruffleFeature.IsEnabled.class}) +public final class WeakAssumedValueTarget {} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliAbstractProjectCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliAbstractProjectCommand.kt new file mode 100644 index 00000000..c5de6d8e --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliAbstractProjectCommand.kt @@ -0,0 +1,48 @@ +/** + * 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.cli + +import java.nio.file.Files +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliBaseOptions.Companion.getProjectFile +import org.pkl.commons.cli.CliCommand +import org.pkl.commons.cli.CliException +import org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME + +abstract class CliAbstractProjectCommand( + cliOptions: CliBaseOptions, + private val projectDirs: List +) : CliCommand(cliOptions) { + + protected val normalizedProjectFiles: List by lazy { + if (projectDirs.isEmpty()) { + val projectFile = + cliOptions.normalizedWorkingDir.getProjectFile(cliOptions.normalizedRootDir) + ?: throw CliException( + "No project visible to the working directory. Ensure there is a PklProject file in the workspace, or provide an explicit project directory as an argument." + ) + return@lazy listOf(projectFile.normalize()) + } + projectDirs.map { dir -> + val projectFile = dir.resolve(PKL_PROJECT_FILENAME) + if (!Files.exists(projectFile)) { + throw CliException("Directory $dir does not contain a PklProject file.") + } + projectFile.normalize() + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliDownloadPackageCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliDownloadPackageCommand.kt new file mode 100644 index 00000000..68c2c367 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliDownloadPackageCommand.kt @@ -0,0 +1,59 @@ +/** + * 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.cli + +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliCommand +import org.pkl.commons.cli.CliException +import org.pkl.core.packages.PackageResolver +import org.pkl.core.packages.PackageUri + +class CliDownloadPackageCommand( + baseOptions: CliBaseOptions, + private val packageUris: List, + private val noTranstive: Boolean +) : CliCommand(baseOptions) { + + override fun doRun() { + if (moduleCacheDir == null) { + throw CliException("Cannot download packages because no cache directory is specified.") + } + val packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir) + val errors = mutableMapOf() + for (pkg in packageUris) { + try { + packageResolver.downloadPackage(pkg, pkg.checksums, noTranstive) + } catch (e: Throwable) { + errors[pkg] = e + } + } + when (errors.size) { + 0 -> return + 1 -> throw CliException(errors.values.single().message!!) + else -> + throw CliException( + buildString { + appendLine("Failed to download some packages.") + for ((uri, error) in errors) { + appendLine() + appendLine("Failed to download $uri because:") + appendLine("${error.message}") + } + } + ) + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt new file mode 100644 index 00000000..bcb7841d --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt @@ -0,0 +1,236 @@ +/** + * 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.cli + +import java.io.File +import java.io.Reader +import java.io.Writer +import java.net.URI +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import org.pkl.commons.cli.CliCommand +import org.pkl.commons.cli.CliException +import org.pkl.commons.createParentDirectories +import org.pkl.commons.currentWorkingDir +import org.pkl.commons.writeString +import org.pkl.core.EvaluatorBuilder +import org.pkl.core.ModuleSource +import org.pkl.core.PklException +import org.pkl.core.module.ModuleKeyFactories +import org.pkl.core.module.ModulePathResolver +import org.pkl.core.runtime.ModuleResolver +import org.pkl.core.runtime.VmException +import org.pkl.core.runtime.VmUtils +import org.pkl.core.util.IoUtils + +private data class OutputFile(val pathSpec: String, val moduleUri: URI) + +/** API equivalent of the Pkl command-line evaluator. */ +class CliEvaluator +@JvmOverloads +constructor( + private val options: CliEvaluatorOptions, + // use System.{in,out}() rather than System.console() + // because the latter returns null when output is sent through a unix pipe + private val consoleReader: Reader = System.`in`.reader(), + private val consoleWriter: Writer = System.out.writer(), +) : CliCommand(options.base) { + /** + * Output files for the modules to be evaluated. Returns `null` if `options.outputPath` is `null`. + * Multiple modules may be mapped to the same output file, in which case their outputs are + * concatenated with [CliEvaluatorOptions.moduleOutputSeparator]. + */ + @Suppress("MemberVisibilityCanBePrivate") + val outputFiles: Set? by lazy { + fileOutputPaths?.values?.mapTo(mutableSetOf(), Path::toFile) + } + + /** + * Output directories for the modules to be evaluated. Returns `null` if + * `options.multipleFileOutputPath` is `null`. + */ + @Suppress("MemberVisibilityCanBePrivate") + val outputDirectories: Set? by lazy { + directoryOutputPaths?.values?.mapTo(mutableSetOf(), Path::toFile) + } + + /** The file output path */ + val fileOutputPaths: Map? by lazy { + if (options.multipleFileOutputPath != null) return@lazy null + options.outputPath?.let { resolveOutputPaths(it) } + } + + private val directoryOutputPaths: Map? by lazy { + options.multipleFileOutputPath?.let { resolveOutputPaths(it) } + } + + /** + * Evaluates source modules according to [options]. + * + * If [CliEvaluatorOptions.outputPath] is set, each module's `output.text` is written to the + * module's [output file][outputFiles]. If [CliEvaluatorOptions.multipleFileOutputPath] is set, + * each module's `output.files` are written to the module's [output directory][outputDirectories]. + * Otherwise, each module's `output.text` is written to [consoleWriter] (which defaults to + * standard out). + * + * Throws [CliException] in case of an error. + */ + override fun doRun() { + val builder = evaluatorBuilder() + try { + if (options.multipleFileOutputPath != null) { + writeMultipleFileOutput(builder) + } else { + writeOutput(builder) + } + } finally { + ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories) + } + } + + private fun resolveOutputPaths(pathStr: String): Map { + val moduleUris = options.base.normalizedSourceModules + val workingDir = options.base.normalizedWorkingDir + // used just to resolve the `%{moduleName}` placeholder + val moduleResolver = ModuleResolver(moduleKeyFactories(ModulePathResolver.empty())) + + return moduleUris.associateWith { uri -> + val moduleDir: String? = + IoUtils.toPath(uri)?.let { workingDir.relativize(it.parent).toString().ifEmpty { "." } } + val moduleKey = + try { + moduleResolver.resolve(uri) + } catch (e: VmException) { + throw e.toPklException(stackFrameTransformer) + } + val substituted = + pathStr + .replace("%{moduleName}", IoUtils.inferModuleName(moduleKey)) + .replace("%{outputFormat}", options.outputFormat ?: "%{outputFormat}") + .replace("%{moduleDir}", moduleDir ?: "%{moduleDir}") + if (substituted.contains("%{moduleDir}")) { + throw PklException( + "Cannot substitute output path placeholder `%{moduleDir}` " + + "because module `$uri` does not have a file system path." + ) + } + val absolutePath = workingDir.resolve(substituted).normalize() + absolutePath + } + } + + /** Renders each module's `output.text`, writing it to the specified output file. */ + private fun writeOutput(builder: EvaluatorBuilder) { + val evaluator = builder.setOutputFormat(options.outputFormat).build() + evaluator.use { + val outputFiles = fileOutputPaths + if (outputFiles != null) { + // files that we've written non-empty output to + // YamlRenderer produces empty output if `isStream` is true and `output.value` is empty + // collection + val writtenFiles = mutableSetOf() + + for ((moduleUri, outputFile) in outputFiles) { + val moduleSource = toModuleSource(moduleUri, consoleReader) + val output = evaluator.evaluateExpressionString(moduleSource, options.expression) + outputFile.createParentDirectories() + if (!writtenFiles.contains(outputFile)) { + // write file even if output is empty to overwrite output from previous runs + outputFile.writeString(output) + if (output.isNotEmpty()) { + writtenFiles.add(outputFile) + } + } else { + if (output.isNotEmpty()) { + outputFile.writeString( + options.moduleOutputSeparator + IoUtils.getLineSeparator(), + Charsets.UTF_8, + StandardOpenOption.WRITE, + StandardOpenOption.APPEND + ) + outputFile.writeString( + output, + Charsets.UTF_8, + StandardOpenOption.WRITE, + StandardOpenOption.APPEND + ) + } + } + } + } else { + var outputWritten = false + for (moduleUri in options.base.normalizedSourceModules) { + val moduleSource = toModuleSource(moduleUri, consoleReader) + val output = evaluator.evaluateExpressionString(moduleSource, options.expression) + if (output.isNotEmpty()) { + if (outputWritten) consoleWriter.appendLine(options.moduleOutputSeparator) + consoleWriter.write(output) + consoleWriter.flush() + outputWritten = true + } + } + } + } + } + + private fun toModuleSource(uri: URI, reader: Reader) = + if (uri == VmUtils.REPL_TEXT_URI) ModuleSource.create(uri, reader.readText()) + else ModuleSource.uri(uri) + + /** + * Renders each module's `output.files`, writing each entry as a file into the specified output + * directory. + */ + private fun writeMultipleFileOutput(builder: EvaluatorBuilder) { + val outputDirs = directoryOutputPaths!! + val writtenFiles = mutableMapOf() + for ((moduleUri, outputDir) in outputDirs) { + val evaluator = builder.setOutputFormat(options.outputFormat).build() + if (outputDir.exists() && !outputDir.isDirectory()) { + throw CliException("Output path `$outputDir` exists and is not a directory.") + } + val moduleSource = toModuleSource(moduleUri, consoleReader) + val output = evaluator.evaluateOutputFiles(moduleSource) + for ((pathSpec, fileOutput) in output) { + val resolvedPath = outputDir.resolve(pathSpec).normalize() + val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath + if (!realPath.startsWith(outputDir)) { + throw CliException( + "Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$outputDir`." + ) + } + val previousOutput = writtenFiles[realPath] + if (previousOutput != null) { + throw CliException( + "Output file conflict: `output.files` entries `\"${previousOutput.pathSpec}\"` in module `${previousOutput.moduleUri}` and `\"$pathSpec\"` in module `$moduleUri` resolve to the same file path `$realPath`." + ) + } + if (realPath.isDirectory()) { + throw CliException( + "Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is a directory." + ) + } + writtenFiles[realPath] = OutputFile(pathSpec, moduleUri) + realPath.createParentDirectories() + realPath.writeString(fileOutput.text) + consoleWriter.write(currentWorkingDir.relativize(resolvedPath).toString() + "\n") + consoleWriter.flush() + } + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt new file mode 100644 index 00000000..1c624f93 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt @@ -0,0 +1,85 @@ +/** + * 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.cli + +import org.pkl.commons.cli.CliBaseOptions + +/** Configuration options for [CliEvaluator]. */ +data class CliEvaluatorOptions( + /** Base options shared between CLI commands. */ + val base: CliBaseOptions, + + /** + * The file path where the output file is placed. If multiple source modules are given, + * placeholders can be used to map them to different output files. If multiple modules are mapped + * to the same output file, their outputs are concatenated. Currently, the only available + * concatenation strategy is to separate outputs with `---`, as in a YAML stream. + * + * The following placeholders are supported: + * - `%{moduleDir}` The directory path of the module, relative to the working directory. Only + * available when evaluating file-based modules. + * - `%{moduleName}` The simple module name as inferred from the module URI. For hierarchical + * URIs, this is the last path segment without file extension. + * - `%{outputFormat}` The requested output format. Only available if `outputFormat` is non-null. + * + * If [CliBaseOptions.workingDir] corresponds to a file system path, relative output paths are + * resolved against that path. Otherwise, relative output paths are not allowed. + * + * If `null`, output is written to the console. + */ + val outputPath: String? = null, + + /** + * The output format to generate. + * + * The default output renderer for a module supports the following formats: + * - `"json"` + * - `"jsonnet"` + * - `"pcf"` (default) + * - `"plist"` + * - `"properties"` + * - `"textproto"` + * - `"xml"` + * - `"yaml"` + */ + val outputFormat: String? = null, + + /** The separator to use when multiple module outputs are written to the same location. */ + val moduleOutputSeparator: String = "---", + + /** + * The directory where a module's output files are placed. + * + * Setting this option causes Pkl to evaluate `output.files` instead of `output.text`, and write + * files using each entry's key as the file path relative to [multipleFileOutputPath], and each + * value's `text` property as the file's contents. + */ + val multipleFileOutputPath: String? = null, + + /** + * The expression to evaluate within the module. + * + * If set, the said expression is evaluated under the context of the enclosing module. + * + * If unset, the module's `output.text` property evaluated. + */ + val expression: String = "output.text", +) { + + companion object { + val defaults = CliEvaluatorOptions(CliBaseOptions()) + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt new file mode 100644 index 00000000..1d4db1b3 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt @@ -0,0 +1,90 @@ +/** + * 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.cli + +import java.io.Writer +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliException +import org.pkl.commons.cli.CliTestException +import org.pkl.commons.cli.CliTestOptions +import org.pkl.core.project.Project +import org.pkl.core.project.ProjectPackager +import org.pkl.core.util.ErrorMessages + +class CliProjectPackager( + baseOptions: CliBaseOptions, + projectDirs: List, + private val testOptions: CliTestOptions, + private val outputPath: String, + private val skipPublishCheck: Boolean, + private val consoleWriter: Writer = System.out.writer(), + private val errWriter: Writer = System.err.writer() +) : CliAbstractProjectCommand(baseOptions, projectDirs) { + + private fun runApiTests(project: Project) { + val apiTests = project.`package`!!.apiTests + if (apiTests.isEmpty()) return + val normalizeApiTests = apiTests.map { project.projectDir.resolve(it).toUri() } + val testRunner = + CliTestRunner( + cliOptions.copy(sourceModules = normalizeApiTests, projectDir = project.projectDir), + testOptions = testOptions, + consoleWriter = consoleWriter, + errWriter = errWriter, + ) + try { + testRunner.run() + } catch (e: CliTestException) { + throw CliException(ErrorMessages.create("packageTestsFailed", project.`package`!!.uri)) + } + } + + override fun doRun() { + val projects = buildList { + for (projectFile in normalizedProjectFiles) { + val project = loadProject(projectFile) + project.`package` + ?: throw CliException( + ErrorMessages.create("noPackageDefinedByProject", project.projectFileUri) + ) + runApiTests(project) + add(project) + } + } + // Require that all local projects are included + projects.forEach { proj -> + proj.dependencies.localDependencies.values.forEach { localDep -> + val projectDir = Path.of(localDep.projectFileUri).parent + if (projects.none { it.projectDir == projectDir }) { + throw CliException( + ErrorMessages.create("missingProjectInPackageCommand", proj.projectDir, projectDir) + ) + } + } + } + ProjectPackager( + projects, + cliOptions.normalizedWorkingDir, + outputPath, + stackFrameTransformer, + securityManager, + skipPublishCheck, + consoleWriter + ) + .createPackages() + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectResolver.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectResolver.kt new file mode 100644 index 00000000..69e14e51 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectResolver.kt @@ -0,0 +1,53 @@ +/** + * 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.cli + +import java.io.Writer +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.core.SecurityManagers +import org.pkl.core.module.ProjectDependenciesManager +import org.pkl.core.packages.PackageResolver +import org.pkl.core.project.ProjectDependenciesResolver + +class CliProjectResolver( + baseOptions: CliBaseOptions, + projectDirs: List, + private val consoleWriter: Writer = System.out.writer(), + private val errWriter: Writer = System.err.writer() +) : CliAbstractProjectCommand(baseOptions, projectDirs) { + override fun doRun() { + for (projectFile in normalizedProjectFiles) { + val project = loadProject(projectFile) + val packageResolver = + PackageResolver.getInstance( + SecurityManagers.standard( + allowedModules, + allowedResources, + SecurityManagers.defaultTrustLevels, + rootDir + ), + moduleCacheDir + ) + val dependencies = ProjectDependenciesResolver(project, packageResolver, errWriter).resolve() + val depsFile = + projectFile.parent.resolve(ProjectDependenciesManager.PKL_PROJECT_DEPS_FILENAME).toFile() + depsFile.outputStream().use { dependencies.writeTo(it) } + consoleWriter.appendLine(depsFile.toString()) + consoleWriter.flush() + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliRepl.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliRepl.kt new file mode 100644 index 00000000..95248fca --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliRepl.kt @@ -0,0 +1,72 @@ +/** + * 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.cli + +import org.pkl.cli.repl.Repl +import org.pkl.commons.cli.CliCommand +import org.pkl.core.Loggers +import org.pkl.core.SecurityManagers +import org.pkl.core.module.ModuleKeyFactories +import org.pkl.core.module.ModulePathResolver +import org.pkl.core.repl.ReplServer +import org.pkl.core.resource.ResourceReaders + +internal class CliRepl(private val options: CliEvaluatorOptions) : CliCommand(options.base) { + override fun doRun() { + ModulePathResolver(modulePath).use { modulePathResolver -> + // TODO: send options as command + val server = + ReplServer( + SecurityManagers.standard( + allowedModules, + allowedResources, + SecurityManagers.defaultTrustLevels, + rootDir + ), + Loggers.stdErr(), + listOf( + ModuleKeyFactories.standardLibrary, + ModuleKeyFactories.modulePath(modulePathResolver) + ) + + ModuleKeyFactories.fromServiceProviders() + + listOf( + ModuleKeyFactories.file, + ModuleKeyFactories.pkg, + ModuleKeyFactories.projectpackage, + ModuleKeyFactories.genericUrl + ), + listOf( + ResourceReaders.environmentVariable(), + ResourceReaders.externalProperty(), + ResourceReaders.modulePath(modulePathResolver), + ResourceReaders.file(), + ResourceReaders.http(), + ResourceReaders.https(), + ResourceReaders.pkg(), + ResourceReaders.projectpackage() + ), + environmentVariables, + externalProperties, + moduleCacheDir, + project?.dependencies, + options.outputFormat, + options.base.normalizedWorkingDir, + stackFrameTransformer + ) + Repl(options.base.normalizedWorkingDir, server).run() + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliServer.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliServer.kt new file mode 100644 index 00000000..3035802b --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliServer.kt @@ -0,0 +1,33 @@ +/** + * 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.cli + +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliCommand +import org.pkl.commons.cli.CliException +import org.pkl.server.MessageTransports +import org.pkl.server.ProtocolException +import org.pkl.server.Server + +class CliServer(options: CliBaseOptions) : CliCommand(options) { + override fun doRun() = + try { + val server = Server(MessageTransports.stream(System.`in`, System.out)) + server.use { it.start() } + } catch (e: ProtocolException) { + throw CliException(e.message!!) + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt new file mode 100644 index 00000000..bd08a77c --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt @@ -0,0 +1,104 @@ +/** + * 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.cli + +import java.io.Writer +import org.pkl.commons.cli.* +import org.pkl.core.EvaluatorBuilder +import org.pkl.core.ModuleSource.uri +import org.pkl.core.module.ModuleKeyFactories +import org.pkl.core.stdlib.test.report.JUnitReport +import org.pkl.core.stdlib.test.report.SimpleReport +import org.pkl.core.util.ErrorMessages + +class CliTestRunner +@JvmOverloads +constructor( + private val options: CliBaseOptions, + private val testOptions: CliTestOptions, + private val consoleWriter: Writer = System.out.writer(), + private val errWriter: Writer = System.err.writer() +) : CliCommand(options) { + + override fun doRun() { + val builder = evaluatorBuilder() + try { + evalTest(builder) + } finally { + ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories) + } + } + + private fun evalTest(builder: EvaluatorBuilder) { + val sources = + options.normalizedSourceModules.ifEmpty { project?.tests?.map { it.toUri() } } + ?: + // keep in sync with error message thrown by clikt + throw CliException( + """ + Usage: pkl test [OPTIONS] ... + + Error: Missing argument "" + """ + .trimIndent() + ) + + val evaluator = builder.build() + evaluator.use { + var failed = false + val moduleNames = mutableSetOf() + for (moduleUri in sources) { + try { + val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite) + if (!failed) { + failed = results.failed() + } + SimpleReport().report(results, consoleWriter) + consoleWriter.flush() + val junitDir = testOptions.junitDir + if (junitDir != null) { + junitDir.toFile().mkdirs() + val moduleName = "${results.moduleName}.xml" + if (moduleName in moduleNames) { + throw RuntimeException( + """ + Cannot generate JUnit report for $moduleUri. + A report with the same name was already generated. + + To fix, provide a different name for this module by adding a module header. + """ + .trimIndent() + ) + } + moduleNames += moduleName + JUnitReport().reportToPath(results, junitDir.resolve(moduleName)) + } + } catch (ex: Exception) { + errWriter.appendLine("Error evaluating module ${moduleUri.path}:") + errWriter.write(ex.message ?: "") + if (moduleUri != sources.last()) { + errWriter.appendLine() + } + errWriter.flush() + failed = true + } + } + if (failed) { + throw CliTestException(ErrorMessages.create("testsFailed")) + } + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt new file mode 100644 index 00000000..f75ee9e0 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt @@ -0,0 +1,55 @@ +/** + * 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. + */ +@file:JvmName("Main") + +package org.pkl.cli + +import com.github.ajalt.clikt.core.subcommands +import org.pkl.cli.commands.* +import org.pkl.commons.cli.CliMain +import org.pkl.commons.cli.cliMain +import org.pkl.core.Release + +/** Main method of the Pkl CLI (command-line evaluator and REPL). */ +internal fun main(args: Array) { + val version = Release.current().versionInfo() + val helpLink = "${Release.current().documentation().homepage()}pkl-cli/index.html#usage" + val commands = + arrayOf( + EvalCommand(helpLink), + ReplCommand(helpLink), + ServerCommand(helpLink), + TestCommand(helpLink), + ProjectCommand(helpLink), + DownloadPackageCommand(helpLink) + ) + val cmd = RootCommand("pkl", version, helpLink).subcommands(*commands) + cliMain { + if (CliMain.compat == "alpine") { + // Alpine's main thread has a prohibitively small stack size by default; + // https://github.com/oracle/graal/issues/3398 + var throwable: Throwable? = null + Thread(null, { cmd.main(args) }, "alpineMain", 10000000).apply { + setUncaughtExceptionHandler { _, t -> throwable = t } + start() + join() + } + throwable?.let { throw it } + } else { + cmd.main(args) + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/DownloadPackageCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/DownloadPackageCommand.kt new file mode 100644 index 00000000..e448854c --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/DownloadPackageCommand.kt @@ -0,0 +1,72 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.convert +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import org.pkl.cli.CliDownloadPackageCommand +import org.pkl.commons.cli.commands.BaseCommand +import org.pkl.commons.cli.commands.ProjectOptions +import org.pkl.commons.cli.commands.single +import org.pkl.core.packages.PackageUri + +class DownloadPackageCommand(helpLink: String) : + BaseCommand( + name = "download-package", + helpLink = helpLink, + help = + """ + Download package(s) + + This command downloads the specified packages to the cache directory. + If the package already exists in the cache directory, this command is a no-op. + + Examples: + ``` + # Download two packages + $ pkl download-package package://example.com/package1@1.0.0 package://example.com/package2@1.0.0 + ``` + """ + .trimIndent() + ) { + private val projectOptions by ProjectOptions() + + private val packageUris: List by + argument("", "The package URIs to download") + .convert { PackageUri(it) } + .multiple(required = true) + + private val noTransitive: Boolean by + option( + names = arrayOf("--no-transitive"), + help = "Skip downloading transitive dependencies of a package" + ) + .single() + .flag() + + override fun run() { + CliDownloadPackageCommand( + baseOptions.baseOptions(emptyList(), projectOptions), + packageUris, + noTransitive + ) + .run() + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt new file mode 100644 index 00000000..3fcb64ae --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt @@ -0,0 +1,88 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.validate +import org.pkl.cli.CliEvaluator +import org.pkl.cli.CliEvaluatorOptions +import org.pkl.commons.cli.commands.ModulesCommand +import org.pkl.commons.cli.commands.single + +class EvalCommand(helpLink: String) : + ModulesCommand( + name = "eval", + help = "Render pkl module(s)", + helpLink = helpLink, + ) { + private val outputPath: String? by + option( + names = arrayOf("-o", "--output-path"), + metavar = "", + help = "File path where the output file is placed." + ) + .single() + + private val moduleOutputSeparator: String by + option( + names = arrayOf("--module-output-separator"), + metavar = "", + help = + "Separator to use when multiple module outputs are written to the same file. (default: ---)" + ) + .single() + .default("---") + + private val expression: String? by + option( + names = arrayOf("-x", "--expression"), + metavar = "", + help = "Expression to be evaluated within the module." + ) + .single() + + private val multipleFileOutputPath: String? by + option( + names = arrayOf("-m", "--multiple-file-output-path"), + metavar = "", + help = "Directory where a module's multiple file output is placed." + ) + .single() + .validate { + if (outputPath != null || expression != null) { + fail("Option is mutually exclusive with -o, --output-path and -x, --expression.") + } + } + + // hidden option used by the native tests + private val testMode: Boolean by + option(names = arrayOf("--test-mode"), help = "Internal test mode", hidden = true).flag() + + override fun run() { + val options = + CliEvaluatorOptions( + base = baseOptions.baseOptions(modules, projectOptions, testMode = testMode), + outputPath = outputPath, + outputFormat = baseOptions.format, + moduleOutputSeparator = moduleOutputSeparator, + multipleFileOutputPath = multipleFileOutputPath, + expression = expression ?: CliEvaluatorOptions.defaults.expression + ) + CliEvaluator(options).run() + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt new file mode 100644 index 00000000..7ee9615e --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt @@ -0,0 +1,149 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.core.NoOpCliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.path +import java.nio.file.Path +import org.pkl.cli.CliProjectPackager +import org.pkl.cli.CliProjectResolver +import org.pkl.commons.cli.commands.BaseCommand +import org.pkl.commons.cli.commands.TestOptions +import org.pkl.commons.cli.commands.single + +class ProjectCommand(helpLink: String) : + NoOpCliktCommand( + name = "project", + help = "Run commands related to projects", + epilog = "For more information, visit $helpLink" + ) { + init { + subcommands(ResolveCommand(helpLink), PackageCommand(helpLink)) + } + + companion object { + class ResolveCommand(helpLink: String) : + BaseCommand( + name = "resolve", + helpLink = helpLink, + help = + """ + Resolve dependencies for project(s) + + This command takes the `dependencies` of `PklProject`s, and writes the + resolved versions to `PklProject.deps.json` files. + + Examples: + + ``` + # Search the current working directory for a project, and resolve its dependencies. + $ pkl project resolve + + # Resolve dependencies for all projects within the `packages/` directory. + $ pkl project resolve packages/*/ + ``` + """, + ) { + private val projectDirs: List by + argument("

", "The project directories to resolve dependencies for").path().multiple() + + override fun run() { + CliProjectResolver(baseOptions.baseOptions(emptyList()), projectDirs).run() + } + } + + private const val NEWLINE = '\u0085' + + class PackageCommand(helpLink: String) : + BaseCommand( + name = "package", + helpLink = helpLink, + help = + """ + Verify package(s), and prepare package artifacts to be published. + + This command runs a project's api tests, as defined by `apiTests` in `PklProject`. + Additionally, it verifies that all imports resolve to paths that are local to the project. + + Finally, this command writes the folowing artifacts into the output directory specified by the output path. + + - `name@version` - dependency metadata$NEWLINE + - `name@version.sha256` - dependency metadata's SHA-256 checksum$NEWLINE + - `name@version.zip` - package archive$NEWLINE + - `name@version.zip.sha256` - package archive's SHA-256 checksum + + The output path option accepts the following placeholders: + + - %{name}: The display name of the package$NEWLINE + - %{version}: The version of the package + + If a project has local project dependencies, the depended upon project directories must also + be included as arguments to this command. + + Examples: + + ``` + # Search the current working directory for a project, and package it. + $ pkl project package + + # Package all projects within the `packages/` directory. + $ pkl project package packages/*/ + ``` + """ + .trimIndent(), + ) { + private val testOptions by TestOptions() + + private val projectDirs: List by + argument("", "The project directories to package").path().multiple() + + private val outputPath: String by + option( + names = arrayOf("--output-path"), + help = "The directory to write artifacts to", + metavar = "" + ) + .single() + .default(".out/%{name}@%{version}") + + private val skipPublishCheck: Boolean by + option( + names = arrayOf("--skip-publish-check"), + help = "Skip checking if a package has already been published with different contents", + ) + .single() + .flag() + + override fun run() { + CliProjectPackager( + baseOptions.baseOptions(emptyList()), + projectDirs, + testOptions.cliTestOptions, + outputPath, + skipPublishCheck + ) + .run() + } + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ReplCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ReplCommand.kt new file mode 100644 index 00000000..eedb0563 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ReplCommand.kt @@ -0,0 +1,36 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import org.pkl.cli.CliEvaluatorOptions +import org.pkl.cli.CliRepl +import org.pkl.commons.cli.commands.BaseCommand +import org.pkl.commons.cli.commands.ProjectOptions + +class ReplCommand(helpLink: String) : + BaseCommand( + name = "repl", + help = "Start a REPL session", + helpLink = helpLink, + ) { + private val projectOptions by ProjectOptions() + + override fun run() { + val options = CliEvaluatorOptions(base = baseOptions.baseOptions(emptyList(), projectOptions)) + CliRepl(options).run() + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/RootCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/RootCommand.kt new file mode 100644 index 00000000..286a3c5a --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/RootCommand.kt @@ -0,0 +1,39 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.core.NoOpCliktCommand +import com.github.ajalt.clikt.core.context +import com.github.ajalt.clikt.parameters.options.versionOption + +class RootCommand(name: String, version: String, helpLink: String) : + NoOpCliktCommand( + name = name, + printHelpOnEmptyArgs = true, + epilog = "For more information, visit $helpLink", + ) { + init { + versionOption(version, names = setOf("-v", "--version"), message = { it }) + + context { + correctionSuggestor = { given, possible -> + if (!given.startsWith("-")) { + registeredSubcommands().map { it.commandName } + } else possible + } + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ServerCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ServerCommand.kt new file mode 100644 index 00000000..01d7cb23 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ServerCommand.kt @@ -0,0 +1,32 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.core.CliktCommand +import org.pkl.cli.CliServer +import org.pkl.commons.cli.CliBaseOptions + +class ServerCommand(helpLink: String) : + CliktCommand( + name = "server", + help = "Run as a server that communicates over standard input/output", + epilog = "For more information, visit $helpLink" + ) { + + override fun run() { + CliServer(CliBaseOptions()).run() + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt new file mode 100644 index 00000000..cd0ae557 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt @@ -0,0 +1,46 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.convert +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import java.net.URI +import org.pkl.cli.CliTestRunner +import org.pkl.commons.cli.commands.BaseCommand +import org.pkl.commons.cli.commands.ProjectOptions +import org.pkl.commons.cli.commands.TestOptions + +class TestCommand(helpLink: String) : + BaseCommand(name = "test", help = "Run tests within the given module(s)", helpLink = helpLink) { + val modules: List by + argument(name = "", help = "Module paths or URIs to evaluate.") + .convert { parseModuleName(it) } + .multiple() + + private val projectOptions by ProjectOptions() + + private val testOptions by TestOptions() + + override fun run() { + CliTestRunner( + options = baseOptions.baseOptions(modules, projectOptions), + testOptions = testOptions.cliTestOptions + ) + .run() + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/repl/Repl.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/Repl.kt new file mode 100644 index 00000000..6a16aaed --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/Repl.kt @@ -0,0 +1,219 @@ +/** + * 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.cli.repl + +import java.io.IOException +import java.net.URI +import java.nio.file.Path +import kotlin.io.path.deleteIfExists +import org.fusesource.jansi.Ansi +import org.jline.reader.EndOfFileException +import org.jline.reader.LineReader.Option +import org.jline.reader.LineReaderBuilder +import org.jline.reader.UserInterruptException +import org.jline.reader.impl.completer.AggregateCompleter +import org.jline.reader.impl.history.DefaultHistory +import org.jline.terminal.TerminalBuilder +import org.jline.utils.InfoCmp +import org.pkl.core.repl.ReplRequest +import org.pkl.core.repl.ReplResponse +import org.pkl.core.repl.ReplServer +import org.pkl.core.util.IoUtils + +internal class Repl(workingDir: Path, private val server: ReplServer) { + private val terminal = TerminalBuilder.builder().apply { jansi(true) }.build() + private val history = DefaultHistory() + private val reader = + LineReaderBuilder.builder() + .apply { + history(history) + terminal(terminal) + completer(AggregateCompleter(CommandCompleter, FileCompleter(workingDir))) + option(Option.DISABLE_EVENT_EXPANSION, true) + variable( + org.jline.reader.LineReader.HISTORY_FILE, + (IoUtils.getPklHomeDir().resolve("repl-history")) + ) + } + .build() + + private var continuation = false + private var quit = false + private var nextRequestId = 0 + + fun run() { + // JLine 2 history file is incompatible with JLine 3 + IoUtils.getPklHomeDir().resolve("repl-history.bin").deleteIfExists() + + println(ReplMessages.welcome) + println() + + var inputBuffer = "" + + try { + while (!quit) { + val line = + try { + if (continuation) { + nextRequestId -= 1 + reader.readLine(" ".repeat("pkl$nextRequestId> ".length)) + } else { + reader.readLine("pkl$nextRequestId> ") + } + } catch (e: UserInterruptException) { + ":quit" + } catch (e: EndOfFileException) { + ":quit" + } + + val input = line.trim() + if (input.isEmpty()) continue + + if (continuation) { + inputBuffer = (inputBuffer + "\n" + input).trim() + continuation = false + } else { + inputBuffer = input + } + + if (inputBuffer.startsWith(":")) { + executeCommand(inputBuffer) + } else { + evaluate(inputBuffer) + } + } + } finally { + try { + history.save() + } catch (ignored: IOException) {} + try { + terminal.close() + } catch (ignored: IOException) {} + } + } + + private fun executeCommand(inputBuffer: String) { + val candidates = getMatchingCommands(inputBuffer) + when { + candidates.isEmpty() -> { + println("Unknown command: `${inputBuffer.drop(1)}`") + } + candidates.size > 1 -> { + print("Which of the following did you mean? ") + println(candidates.joinToString(separator = " ") { "`:${it.type}`" }) + } + else -> { + doExecuteCommand(candidates.single()) + } + } + } + + private fun doExecuteCommand(command: ParsedCommand) { + when (command.type) { + Command.Clear -> clear() + Command.Examples -> examples() + Command.Force -> force(command) + Command.Help -> help() + Command.Load -> load(command) + Command.Quit -> quit() + Command.Reset -> reset() + } + } + + private fun clear() { + terminal.puts(InfoCmp.Capability.clear_screen) + terminal.flush() + } + + private fun examples() { + println(ReplMessages.examples) + } + + private fun help() { + println(ReplMessages.help) + } + + private fun quit() { + quit = true + } + + private fun reset() { + server.handleRequest(ReplRequest.Reset(nextRequestId())) + clear() + nextRequestId = 0 + } + + private fun evaluate(inputBuffer: String) { + handleEvalRequest(ReplRequest.Eval(nextRequestId(), inputBuffer, false, false)) + } + + private fun loadModule(uri: URI) { + handleEvalRequest(ReplRequest.Load(nextRequestId(), uri)) + } + + private fun force(command: ParsedCommand) { + handleEvalRequest(ReplRequest.Eval(nextRequestId(), command.arg, false, true)) + } + + private fun load(command: ParsedCommand) { + loadModule(IoUtils.toUri(command.arg)) + } + + private fun handleEvalRequest(request: ReplRequest) { + val responses = server.handleRequest(request) + + for (response in responses) { + when (response) { + is ReplResponse.EvalSuccess -> { + println(response.result) + } + is ReplResponse.EvalError -> { + println(response.message) + } + is ReplResponse.InternalError -> { + throw response.cause + } + is ReplResponse.IncompleteInput -> { + assert(responses.size == 1) + continuation = true + } + else -> throw IllegalStateException("Unexpected response: $response") + } + } + } + + private fun nextRequestId(): String = "pkl$nextRequestId".apply { nextRequestId += 1 } + + private fun print(msg: String) { + terminal.writer().print(highlight(msg)) + } + + private fun println(msg: String = "") { + terminal.writer().println(highlight(msg)) + } + + private fun highlight(str: String): String { + val ansi = Ansi.ansi() + var normal = true + for (part in str.split("`", "```")) { + ansi.a(part) + normal = !normal + if (!normal) ansi.bold() else ansi.boldOff() + } + ansi.reset() + return ansi.toString() + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCommands.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCommands.kt new file mode 100644 index 00000000..927147cf --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCommands.kt @@ -0,0 +1,45 @@ +/** + * 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.cli.repl + +private val cmdRegex = Regex(":(\\p{Alpha}*)(\\p{Space}*)(.*)", RegexOption.DOT_MATCHES_ALL) + +internal fun getMatchingCommands(input: String): List { + val match = cmdRegex.matchEntire(input) ?: return listOf() + val (cmd, ws, arg) = match.destructured + return Command.values() + .filter { it.toString().lowercase().startsWith(cmd) } + .map { ParsedCommand(it, cmd, ws, arg) } +} + +internal data class ParsedCommand( + val type: Command, + val cmd: String, + val ws: String, + val arg: String +) + +internal enum class Command { + Clear, + Examples, + Force, + Help, + Load, + Quit, + Reset; + + override fun toString() = name.lowercase() +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCompleters.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCompleters.kt new file mode 100644 index 00000000..f0fd2396 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplCompleters.kt @@ -0,0 +1,182 @@ +/** + * 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.cli.repl + +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import org.jline.reader.Candidate +import org.jline.reader.Completer +import org.jline.reader.LineReader +import org.jline.reader.ParsedLine +import org.jline.terminal.Terminal +import org.jline.utils.AttributedStringBuilder +import org.jline.utils.OSUtils +import org.jline.utils.StyleResolver + +/** + * Originally copied from: + * https://github.com/jline/jline3/blob/jline-parent-3.21.0/builtins/src/main/java/org/jline/builtins/Completers.java + * + * Reasons for copying this class instead of adding jline-builtins dependency: + * - Adding the dependency breaks native-image build (at least when using build-time initialization, + * might work with some config). + * - Completers.FileNameCompleter is the only class we currently use. + */ +internal abstract class JLineFileNameCompleter : Completer { + override fun complete( + reader: LineReader, + commandLine: ParsedLine, + candidates: MutableList + ) { + val buffer = commandLine.word().substring(0, commandLine.wordCursor()) + val current: Path + val curBuf: String + val sep = getSeparator(reader.isSet(LineReader.Option.USE_FORWARD_SLASH)) + val lastSep = buffer.lastIndexOf(sep) + try { + if (lastSep >= 0) { + curBuf = buffer.substring(0, lastSep + 1) + current = + if (curBuf.startsWith("~")) { + if (curBuf.startsWith("~$sep")) { + userHome.resolve(curBuf.substring(2)) + } else { + userHome.parent.resolve(curBuf.substring(1)) + } + } else { + userDir.resolve(curBuf) + } + } else { + curBuf = "" + current = userDir + } + try { + Files.newDirectoryStream(current) { accept(it) } + .use { directory -> + directory.forEach { path -> + val value = curBuf + path.fileName.toString() + if (Files.isDirectory(path)) { + candidates.add( + Candidate( + value + if (reader.isSet(LineReader.Option.AUTO_PARAM_SLASH)) sep else "", + getDisplay(reader.terminal, path, resolver, sep), + null, + null, + if (reader.isSet(LineReader.Option.AUTO_REMOVE_SLASH)) sep else null, + null, + false + ) + ) + } else { + candidates.add( + Candidate( + value, + getDisplay(reader.terminal, path, resolver, sep), + null, + null, + null, + null, + true + ) + ) + } + } + } + } catch (ignored: IOException) {} + } catch (ignored: Exception) {} + } + + protected open fun accept(path: Path): Boolean { + return try { + !Files.isHidden(path) + } catch (e: IOException) { + false + } + } + + protected open val userDir: Path + get() = Path.of(System.getProperty("user.dir")) + + private val userHome: Path + get() = Path.of(System.getProperty("user.home")) + + private fun getSeparator(useForwardSlash: Boolean): String { + return if (useForwardSlash) "/" else userDir.fileSystem.separator + } + + private fun getDisplay( + terminal: Terminal, + path: Path, + resolver: StyleResolver, + separator: String + ): String { + val builder = AttributedStringBuilder() + val name = path.fileName.toString() + val index = name.lastIndexOf(".") + val type = if (index != -1) ".*" + name.substring(index) else null + if (Files.isSymbolicLink(path)) { + builder.styled(resolver.resolve(".ln"), name).append("@") + } else if (Files.isDirectory(path)) { + builder.styled(resolver.resolve(".di"), name).append(separator) + } else if (Files.isExecutable(path) && !OSUtils.IS_WINDOWS) { + builder.styled(resolver.resolve(".ex"), name).append("*") + } else if (type != null && resolver.resolve(type).style != 0L) { + builder.styled(resolver.resolve(type), name) + } else if (Files.isRegularFile(path)) { + builder.styled(resolver.resolve(".fi"), name) + } else { + builder.append(name) + } + return builder.toAnsi(terminal) + } + + companion object { + private val resolver = StyleResolver { name -> + when (name) { + // imitate org.jline.builtins.Styles.DEFAULT_LS_COLORS + "di" -> "1;91" + "ex" -> "1;92" + "ln" -> "1;96" + "fi" -> null + else -> null + } + } + } +} + +internal class FileCompleter(override val userDir: Path) : JLineFileNameCompleter() { + override fun complete( + reader: LineReader, + commandLine: ParsedLine, + candidates: MutableList + ) { + val loadCmd = + getMatchingCommands(commandLine.line()).find { it.type == Command.Load && it.ws.isNotEmpty() } + if (loadCmd != null) { + super.complete(reader, commandLine, candidates) + } + } +} + +internal object CommandCompleter : Completer { + private val commandCandidates: List = + Command.values().map { Candidate(":" + it.toString().lowercase()) } + + override fun complete(reader: LineReader, line: ParsedLine, candidates: MutableList) { + if (line.wordIndex() == 0) candidates.addAll(commandCandidates) + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplMessages.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplMessages.kt new file mode 100644 index 00000000..4bf1461a --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/ReplMessages.kt @@ -0,0 +1,104 @@ +/** + * 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.cli.repl + +import org.pkl.core.Release + +internal object ReplMessages { + val welcome = + """ + Welcome to Pkl ${Release.current().version()}. + Type an expression to have it evaluated. + Type `:help` or `:examples` for more information. + """ + .trimIndent() + + val help = + """ + `` Evaluate and print the result. `1 + 3` + ` = ` Evaluate and assign the result to property . `msg = "howdy"` + `:clear` Clear the screen. + `:examples` Show code examples (use copy and paste to run them). + `:force ` Force eager evaluation of a value. + `:help` Show this help. + `:load ` Load from local file system. `:load path/to/config.pkl` + `:quit` Quit this program. + `:reset` Reset the environment to its initial state. + + Tips: + * Commands can be abbreviated. `:h` + * Commands can be completed. `:` + * File paths can be completed. `:load ` + * Expressions can be completed. `"hello".re` + * Multiple declarations and expressions can be evaluated at once. `a = 1; b = a + 2` + * Incomplete input will be continued on the next line. + * Multi-line programs can be copy-pasted into the REPL. + + """ + .trimIndent() + + val examples: String = + """ + Expressions: + `2 + 3 * 4` + + Strings: + `"Hello, " + "World!"` + + Properties: + `timeout = 5.min; timeout` + + Objects: + ```pigeon { + name = "Pigeon" + fullName = "\(name) Bird" + age = 42 + address { + street = "Landers St." + } + } + pigeon.fullName + + hobbies { + "Swimming" + "Dancing" + "Surfing" + } + hobbies[1] + + prices { + ["Apple"] = 1.5 + ["Orange"] = 5 + ["Banana"] = 2 + } + prices["Banana"]``` + + Inheritance: + ```parrot = (pigeon) { + name = "Parrot" + age = 41 + } + :force parrot``` + + For more examples, see the Language Reference${if (isMacOs()) " (Command+Double-click the link below)" else ""}: + + ${Release.current().documentation().homepage()}language-reference/ + + """ + .trimIndent() + + private fun isMacOs() = System.getProperty("os.name").equals("Mac OS X", ignoreCase = true) +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/repl/package-info.java b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/package-info.java new file mode 100644 index 00000000..12bb3c6b --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/repl/package-info.java @@ -0,0 +1,34 @@ +/** + * This package contains source code from: + * + *

https://github.com/jline/jline3 + * + *

Original license: + * + *

Copyright (c) 2002-2018, the original author or authors. All rights reserved. + * + *

https://opensource.org/licenses/BSD-3-Clause + * + *

Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + *

Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + *

Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + *

Neither the name of JLine nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written permission. + * + *

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.pkl.cli.repl; diff --git a/pkl-cli/src/test/files/projects/project1/PklProject b/pkl-cli/src/test/files/projects/project1/PklProject new file mode 100644 index 00000000..94e31e70 --- /dev/null +++ b/pkl-cli/src/test/files/projects/project1/PklProject @@ -0,0 +1,7 @@ +amends "pkl:Project" + +dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliDownloadPackageCommandTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliDownloadPackageCommandTest.kt new file mode 100644 index 00000000..97b7a44c --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliDownloadPackageCommandTest.kt @@ -0,0 +1,229 @@ +/** + * 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.cli + +import java.nio.file.Path +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.test.FileTestUtils +import org.pkl.commons.test.PackageServer +import org.pkl.core.packages.PackageUri + +class CliDownloadPackageCommandTest { + companion object { + @BeforeAll + @JvmStatic + fun beforeAll() { + PackageServer.ensureStarted() + } + } + + @Test + fun `download packages`(@TempDir tempDir: Path) { + val cmd = + CliDownloadPackageCommand( + baseOptions = + CliBaseOptions( + moduleCacheDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + packageUris = + listOf( + PackageUri("package://localhost:12110/birds@0.5.0"), + PackageUri("package://localhost:12110/fruit@1.0.5"), + PackageUri("package://localhost:12110/fruit@1.1.0") + ), + noTranstive = true + ) + cmd.run() + assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.zip")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.json")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.1.0/fruit@1.1.0.zip")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.1.0/fruit@1.1.0.json")).exists() + } + + @Test + fun `download packages with cache dir set by project`(@TempDir tempDir: Path) { + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + evaluatorSettings { + moduleCacheDir = ".my-cache" + } + """ + .trimIndent() + ) + + val cmd = + CliDownloadPackageCommand( + baseOptions = + CliBaseOptions( + workingDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + packageUris = listOf(PackageUri("package://localhost:12110/birds@0.5.0")), + noTranstive = true + ) + cmd.run() + assertThat(tempDir.resolve(".my-cache/package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip")) + .exists() + assertThat(tempDir.resolve(".my-cache/package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json")) + .exists() + } + + @Test + fun `download package while specifying checksum`(@TempDir tempDir: Path) { + val cmd = + CliDownloadPackageCommand( + baseOptions = + CliBaseOptions( + moduleCacheDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + packageUris = + listOf( + PackageUri( + "package://localhost:12110/birds@0.5.0::sha256:3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + ), + ), + noTranstive = true + ) + cmd.run() + assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json")).exists() + } + + @Test + fun `download package with invalid checksum`(@TempDir tempDir: Path) { + val cmd = + CliDownloadPackageCommand( + baseOptions = + CliBaseOptions( + moduleCacheDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + packageUris = + listOf( + PackageUri("package://localhost:12110/birds@0.5.0::sha256:intentionallyBogusChecksum"), + ), + noTranstive = true + ) + assertThatCode { cmd.run() } + .hasMessage( + """ + Cannot download package `package://localhost:12110/birds@0.5.0` because the computed checksum for package metadata does not match the expected checksum. + + Computed checksum: "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + Expected checksum: "intentionallyBogusChecksum" + Asset URL: "https://localhost:12110/birds@0.5.0" + """ + .trimIndent() + ) + } + + @Test + fun `disabling cacheing is an error`(@TempDir tempDir: Path) { + val cmd = + CliDownloadPackageCommand( + baseOptions = CliBaseOptions(workingDir = tempDir, noCache = true), + packageUris = listOf(PackageUri("package://localhost:12110/birds@0.5.0")), + noTranstive = true + ) + assertThatCode { cmd.run() } + .hasMessage("Cannot download packages because no cache directory is specified.") + } + + @Test + fun `download packages with bad checksum`(@TempDir tempDir: Path) { + val cmd = + CliDownloadPackageCommand( + baseOptions = + CliBaseOptions( + moduleCacheDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + packageUris = listOf(PackageUri("package://localhost:12110/badChecksum@1.0.0")), + noTranstive = true + ) + assertThatCode { cmd.run() } + .hasMessageStartingWith( + "Cannot download package `package://localhost:12110/badChecksum@1.0.0` because the computed checksum does not match the expected checksum." + ) + } + + @Test + fun `download multiple failing packages`(@TempDir tempDir: Path) { + val cmd = + CliDownloadPackageCommand( + baseOptions = + CliBaseOptions( + moduleCacheDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + packageUris = + listOf( + PackageUri("package://localhost:12110/badChecksum@1.0.0"), + PackageUri("package://bogus.domain/notAPackage@1.0.0") + ), + noTranstive = true + ) + assertThatCode { cmd.run() } + .hasMessage( + """ + Failed to download some packages. + + Failed to download package://localhost:12110/badChecksum@1.0.0 because: + Cannot download package `package://localhost:12110/badChecksum@1.0.0` because the computed checksum does not match the expected checksum. + + Computed checksum: "0ec8a501e974802d0b71b8d58141e1e6eaa10bc2033e18200be3a978823d98aa" + Expected checksum: "intentionally bogus checksum" + Asset URL: "https://localhost:12110/badChecksum@1.0.0/badChecksum@1.0.0.zip" + + Failed to download package://bogus.domain/notAPackage@1.0.0 because: + Exception when making request `GET https://bogus.domain/notAPackage@1.0.0`: + bogus.domain + + """ + .trimIndent() + ) + } + + @Test + fun `download package, including transitive dependencies`(@TempDir tempDir: Path) { + CliDownloadPackageCommand( + baseOptions = + CliBaseOptions( + moduleCacheDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + packageUris = listOf(PackageUri("package://localhost:12110/birds@0.5.0")), + noTranstive = false + ) + .run() + assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.zip")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/birds@0.5.0/birds@0.5.0.json")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.zip")).exists() + assertThat(tempDir.resolve("package-1/localhost:12110/fruit@1.0.5/fruit@1.0.5.json")).exists() + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt new file mode 100644 index 00000000..29932214 --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt @@ -0,0 +1,1217 @@ +/** + * 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.cli + +import java.io.StringReader +import java.io.StringWriter +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import kotlin.io.path.createDirectories +import kotlin.io.path.listDirectoryEntries +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.pkl.commons.* +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliException +import org.pkl.commons.cli.commands.BaseOptions +import org.pkl.commons.test.FileTestUtils +import org.pkl.commons.test.PackageServer +import org.pkl.core.OutputFormat +import org.pkl.core.util.IoUtils + +class CliEvaluatorTest { + companion object { + const val defaultContents = """ +person { + name = "pigeon" + age = 20 + 10 +} + """ + } + + // use manually constructed temp dir instead of @TempDir to work around + // https://forums.developer.apple.com/thread/118358 + private val tempDir: Path = run { + val baseDir = FileTestUtils.rootProjectDir.resolve("pkl-cli/build/tmp/CliEvaluatorTest") + baseDir.createDirectories() + Files.createTempDirectory(baseDir, null) + } + + @AfterEach + fun afterEach() { + tempDir.deleteRecursively() + } + + @Test + fun `generate Pcf`() { + val sourceFiles = listOf(writePklFile("test.pkl")) + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFormat = "pcf", + ) + ) + + assertThat(outputFiles).hasSize(1) + checkOutputFile(outputFiles[0], "test.pcf", """ +person { + name = "pigeon" + age = 30 +} + """) + } + + @Test + fun `generate JSON`() { + val sourceFiles = listOf(writePklFile("test.pkl")) + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFormat = "json", + ) + ) + + assertThat(outputFiles).hasSize(1) + checkOutputFile( + outputFiles[0], + "test.json", + """ +{ + "person": { + "name": "pigeon", + "age": 30 + } +} + """ + ) + } + + @Test + fun `generate YAML`() { + val sourceFiles = listOf(writePklFile("test.pkl")) + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFormat = "yaml", + ) + ) + + assertThat(outputFiles).hasSize(1) + checkOutputFile(outputFiles[0], "test.yaml", """ +person: + name: pigeon + age: 30 + """) + } + + @Test + fun `generate plist`() { + val sourceFiles = listOf(writePklFile("test.pkl")) + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFormat = "plist", + ) + ) + + assertThat(outputFiles).hasSize(1) + @Suppress("HttpUrlsUsage") + checkOutputFile( + outputFiles[0], + "test.plist", + """ + + + + + person + + name + pigeon + age + 30 + + + + """ + ) + } + + @Test + fun `generate XML`() { + val sourceFiles = listOf(writePklFile("test.pkl")) + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFormat = "xml", + ) + ) + + assertThat(outputFiles).hasSize(1) + checkOutputFile( + outputFiles[0], + "test.xml", + """ + + + + pigeon + 30 + + + """ + ) + } + + @Test + fun `unknown output format`() { + val sourceFiles = listOf(writePklFile("test.pkl")) + + val e = + assertThrows { + evalToFiles( + CliEvaluatorOptions(CliBaseOptions(sourceModules = sourceFiles), outputFormat = "unknown") + ) + } + + assertThat(e).hasMessageContaining("Unknown output format: `unknown`. ") + } + + @Test + fun `generate multiple files`() { + val sourceFiles = + listOf( + writePklFile("file1.pkl", "x = 1 + 1"), + writePklFile("file2.pkl", "x = 2 + 2"), + writePklFile("file3.pkl", "x = 3 + 3") + ) + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFormat = "pcf", + ) + ) + + assertThat(outputFiles).hasSize(3) + checkOutputFile(outputFiles[0], "file1.pcf", "x = 2") + checkOutputFile(outputFiles[1], "file2.pcf", "x = 4") + checkOutputFile(outputFiles[2], "file3.pcf", "x = 6") + } + + @Test + fun `module path module as source module`() { + val dir = tempDir.resolve("foo").resolve("bar").createDirectories() + dir.resolve("test.pkl").writeString(defaultContents) + // check relative imports too + dir + .resolve("test2.pkl") + .writeString( + """ + amends "test.pkl" + + person { + name = "barn owl" + } + """ + .trimIndent() + ) + + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions( + sourceModules = + listOf(URI("modulepath:/foo/bar/test.pkl"), URI("modulepath:/foo/bar/test2.pkl")), + modulePath = listOf(tempDir) + ), + outputFormat = "pcf", + outputPath = "$tempDir/%{moduleName}.%{outputFormat}" + ) + ) + + assertThat(outputFiles).hasSize(2) + checkOutputFile(outputFiles[0], "test.pcf", """ +person { + name = "pigeon" + age = 30 +} + """) + checkOutputFile( + outputFiles[1], + "test2.pcf", + """ +person { + name = "barn owl" + age = 30 +} + """ + ) + } + + @Test + fun `external properties`() { + val sourceFiles = + listOf( + writePklFile( + "test.pkl", + """ +person { + name = read("prop:name") + age = read("prop:age").toInt() +} + """ + ) + ) + + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions( + sourceModules = sourceFiles, + externalProperties = mapOf("name" to "pigeon", "age" to "30") + ), + outputFormat = "pcf", + ) + ) + + assertThat(outputFiles).hasSize(1) + checkOutputFile(outputFiles[0], "test.pcf", """ +person { + name = "pigeon" + age = 30 +} + """) + } + + @Test + fun `custom working directory given as absolute path`() { + customWorkingDirectory(relativePath = false) + } + + @Test + fun `custom working directory given as relative path (the norm when using cli)`() { + customWorkingDirectory(relativePath = true) + } + + private fun customWorkingDirectory(relativePath: Boolean) { + val dir = tempDir.resolve("foo").resolve("bar").createDirectories() + val file = dir.resolve("test.pkl").writeString(defaultContents) + + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions( + sourceModules = listOf(file.toUri()), + workingDir = + if (relativePath) IoUtils.getCurrentWorkingDir().relativize(dir.parent) + else dir.parent + ), + outputFormat = "pcf", + outputPath = "baz/%{moduleName}.pcf" + ) + ) + + assertThat(outputFiles).hasSize(1) + assertThat(outputFiles[0].normalize()).isEqualTo(dir.parent.resolve("baz/test.pcf")) + checkOutputFile(outputFiles[0], "test.pcf", """ +person { + name = "pigeon" + age = 30 +} + """) + } + + @Test + fun `source module with relative path`() { + val dir = tempDir.resolve("foo").createDirectories() + dir.resolve("test.pkl").writeString(defaultContents) + + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(URI("foo/test.pkl")), workingDir = tempDir), + outputFormat = "pcf" + ) + ) + + assertThat(outputFiles).hasSize(1) + checkOutputFile(outputFiles[0], "test.pcf", """ +person { + name = "pigeon" + age = 30 +} + """) + } + + @Test + fun `module path element with relative path`() { + val libDir = tempDir.resolve("lib").resolve("foo").createDirectories() + libDir.resolve("someLib.pkl").writeString("x = 1") + + val pklScript = + writePklFile("test.pkl", """ +import "modulepath:/foo/someLib.pkl" +result = someLib.x + """) + + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions( + sourceModules = listOf(pklScript), + workingDir = tempDir, + modulePath = listOf("lib".toPath()) + ), + outputFormat = "pcf" + ) + ) + + assertThat(outputFiles).hasSize(1) + checkOutputFile(outputFiles[0], "test.pcf", "result = 1") + } + + @Test + fun `moduleDir is relative to workingDir even if not descendant`() { + val contents = "foo = 42" + val file = writePklFile("some/nested/structure.pkl", contents) + val workingDir = tempDir.resolve("another/structure").createDirectories() + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(file), workingDir = workingDir), + outputPath = "%{moduleDir}/result.pcf", + outputFormat = "pcf" + ) + ) + assertThat(outputFiles).hasSize(1) + assertThat(outputFiles[0]).isEqualTo(tempDir.resolve("some/nested/result.pcf")) + checkOutputFile(outputFiles[0], "result.pcf", contents) + } + + @Test + fun `moduleDir is relative to workingDir even through symlinks`() { + val contents = "foo = 42" + val realWorkingDir = tempDir.resolve("workingDir").createDirectories() + val symlinkToTempDir = Files.createSymbolicLink(tempDir.resolve("symlinkToTempDir"), tempDir) + val workingDir = symlinkToTempDir.resolve("workingDir") + val file = realWorkingDir.resolve("test.pkl").writeString(contents).toUri() + val outputFiles = + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(file), workingDir = workingDir), + outputFormat = "pcf" + ) + ) + assertThat(outputFiles).hasSize(1) + assertThat(outputFiles[0].toString()).doesNotContain("symlinkToTempDir") + checkOutputFile(outputFiles[0], "test.pcf", contents) + } + + @Test + fun `take input from stdin`() { + val stdin = StringReader(defaultContents) + val stdout = StringWriter() + val evaluator = + CliEvaluator( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(URI("repl:text"))), + outputFormat = "pcf" + ), + stdin, + stdout + ) + evaluator.run() + assertThat(stdout.toString().trim()).isEqualTo(defaultContents.replace("20 + 10", "30").trim()) + } + + @Test + fun `write output to console`() { + val module1 = writePklFile("mod1.pkl", "x = 21 + 21") + val module2 = writePklFile("mod2.pkl", "y = 11 + 11") + + val output = + evalToConsole( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(module1, module2)), + ) + ) + + assertThat(output).isEqualTo("x = 42\n---\ny = 22\n") + } + + @Test + fun `evaluation timeout`() { + val sourceFiles = + listOf( + writePklFile( + "test.pkl", + """ + function fib(n) = if (n < 2) 0 else fib(n - 1) + fib(n - 2) + x = fib(100) + """ + ) + ) + + val e = + assertThrows { + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles, timeout = Duration.ofMillis(100)), + outputFormat = "pcf" + ) + ) + } + assertThat(e.message).contains("timed out") + } + + @Test + fun `cannot import module located outside root dir`() { + val sourceFiles = listOf(writePklFile("test.pkl", """ + amends "/non/existing.pkl" + """)) + + val e = + assertThrows { + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles, rootDir = tempDir), + ) + ) + } + + assertThat(e.message).contains("Refusing to load module `file:///non/existing.pkl`") + } + + @Test + fun `concatenate file outputs`() { + val sourceFiles = + listOf( + writePklFile("test1.pkl", "x = 1"), + writePklFile("test2.pkl", "x = 2"), + writePklFile("test3.pkl", "x = 3") + ) + + val outputFile = tempDir.resolve("output.yaml") + + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFile.toString(), + "yaml" + ) + ) + + checkOutputFile(outputFile, "output.yaml", "x: 1\n---\nx: 2\n---\nx: 3") + } + + @Test + fun `concatenate file outputs - some empty YAML streams`() { + val sourceFiles = + listOf( + writePklFile( + "test0.pkl", + "output { value = List(); renderer = new YamlRenderer { isStream = true } }" + ), + writePklFile("test1.pkl", "x = 1"), + writePklFile( + "test2.pkl", + "output { value = List(); renderer = new YamlRenderer { isStream = true } }" + ), + writePklFile("test3.pkl", "x = 3"), + writePklFile( + "test4.pkl", + "output { value = List(); renderer = new YamlRenderer { isStream = true } }" + ) + ) + + val outputFile = tempDir.resolve("output.yaml") + + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFile.toString(), + "yaml" + ) + ) + + checkOutputFile(outputFile, "output.yaml", "x: 1\n---\nx: 3") + } + + @Test + fun `concatenate module outputs with custom separator`() { + val sourceFiles = + listOf( + writePklFile("test1.pkl", "x = 1"), + writePklFile("test2.pkl", "x = 2"), + writePklFile("test3.pkl", "x = 3") + ) + + val outputFile = tempDir.resolve("output.pcf") + + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFile.toString(), + outputFormat = "pcf", + moduleOutputSeparator = "// my module separator" + ) + ) + + checkOutputFile( + outputFile, + "output.pcf", + """ + x = 1 + // my module separator + x = 2 + // my module separator + x = 3 + """ + .trimIndent() + ) + } + + @Test + fun `concatenate module outputs with empty custom separator`() { + val sourceFiles = + listOf( + writePklFile("test1.pkl", "x = 1"), + writePklFile("test2.pkl", "y = 2"), + writePklFile("test3.pkl", "z = 3") + ) + + val outputFile = tempDir.resolve("output.pcf") + + evalToFiles( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + outputFile.toString(), + outputFormat = "pcf", + moduleOutputSeparator = "" + ) + ) + + checkOutputFile( + outputFile, + "output.pcf", + """ + x = 1 + + y = 2 + + z = 3 + """ + .trimIndent() + ) + } + + @Test + fun `concatenate console outputs`() { + val sourceFiles = + listOf( + writePklFile("test1.pkl", "x = 1"), + writePklFile("test2.pkl", "x = 2"), + writePklFile("test3.pkl", "x = 3") + ) + + val output = + evalToConsole(CliEvaluatorOptions(CliBaseOptions(sourceModules = sourceFiles), null, "yaml")) + + assertThat(output).isEqualTo("x: 1\n---\nx: 2\n---\nx: 3\n") + } + + @Test + fun `concatenate console outputs - some empty YAML streams`() { + val sourceFiles = + listOf( + writePklFile( + "test0.pkl", + "output { value = List(); renderer = new YamlRenderer { isStream = true } }" + ), + writePklFile("test1.pkl", "x = 1"), + writePklFile( + "test2.pkl", + "output { value = List(); renderer = new YamlRenderer { isStream = true } }" + ), + writePklFile("test3.pkl", "x = 3"), + writePklFile( + "test4.pkl", + "output { value = List(); renderer = new YamlRenderer { isStream = true } }" + ) + ) + + val output = + evalToConsole(CliEvaluatorOptions(CliBaseOptions(sourceModules = sourceFiles), null, "yaml")) + + assertThat(output).isEqualTo("x: 1\n---\nx: 3\n") + } + + // prototext can't render `Dynamic`. + @EnumSource(names = ["TEXTPROTO"], mode = EnumSource.Mode.EXCLUDE) + @ParameterizedTest(name = "{0} console output ends with newline") + fun `console output ends with newline`(outputFormat: OutputFormat) { + val sourceFiles = listOf(writePklFile("test0.pkl", "foo = 0\nbar=\"Baz\"")) + val output = + evalToConsole( + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceFiles), + null, + outputFormat.toString() + ) + ) + assertThat(output).endsWith("\n") + } + + @Test + fun `multiple file output writes multiple files to the provided directory`() { + val contents = + """ + output { + files { + ["foo.pcf"] { + value = new Dynamic { + ["bar"] = "baz" + } + } + ["bar/baz.pcf"] { + value = new Dynamic { + ["baz"] = "biz" + } + } + ["buz.txt"] { + text = "buz" + } + } + } + """ + .trimIndent() + val sourceFile = writePklFile("test.pkl", contents) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(sourceFile), workingDir = tempDir), + outputPath = "my-outputs", + multipleFileOutputPath = ".my-output/", + ) + val evaluator = CliEvaluator(options) + evaluator.run() + checkOutputFile( + tempDir.resolve(".my-output/foo.pcf"), + "foo.pcf", + """ + ["bar"] = "baz" + """ + .trimIndent() + ) + checkOutputFile( + tempDir.resolve(".my-output/bar/baz.pcf"), + "baz.pcf", + """ + ["baz"] = "biz" + """ + .trimIndent() + ) + checkOutputFile(tempDir.resolve(".my-output/buz.txt"), "buz.txt", "buz") + } + + @Test + fun `multiple file output writes multiple modules to the output path`() { + val sourceModules = + listOf( + writePklFile( + "test0.pkl", + """ + output { + files { + ["foo.pcf"] { + value = new Dynamic { + ["bar"] = "baz" + } + } + } + } + """ + .trimIndent(), + ), + writePklFile( + "test1.pkl", + """ + output { + files { + ["bar.pcf"] { + value = new Dynamic { + ["bar"] = "baz" + } + } + } + } + """ + .trimIndent(), + ) + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceModules, workingDir = tempDir), + multipleFileOutputPath = ".", + ) + CliEvaluator(options).run() + assertThat(tempDir.resolve("foo.pcf")).isRegularFile.hasFileName("foo.pcf") + assertThat(tempDir.resolve("bar.pcf")).isRegularFile.hasFileName("bar.pcf") + } + + @Test + fun `multiple file output throws in case of conflict`() { + val sourceModules = + listOf( + writePklFile( + "bar.pkl", + """ + output { + files { + ["foo.pcf"] { + text = "myBar" + } + } + } + """ + .trimIndent() + ), + writePklFile( + "foo.pkl", + """ + output { + files { + ["foo.pcf"] { + text = "myFoo" + } + } + } + """ + .trimIndent() + ), + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = sourceModules, workingDir = tempDir), + multipleFileOutputPath = ".", + ) + assertThrows { CliEvaluator(options).run() } + } + + @Test + fun `multiple file output writes nothing if output files is null`() { + val moduleUri = + writePklFile( + "test.pkl", + "", + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + multipleFileOutputPath = ".output", + ) + val output = evalToConsole(options) + assertThat(output).isEqualTo("") + assertThat(tempDir.listDirectoryEntries()).hasSize(1) + } + + @Test + fun `multiple file output throws if files are written outside the base path`() { + val moduleUri = + writePklFile( + "test.pkl", + """ + output { + files { + ["../foo.txt"] { + text = "bar" + } + } + } + """ + .trimIndent() + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + multipleFileOutputPath = ".output" + ) + assertThatCode { evalToConsole(options) } + .hasMessageStartingWith("Output file conflict:") + .hasMessageContaining("which is outside output directory") + } + + @Test + fun `multiple file output throws if file path is a directory`() { + tempDir.resolve(".output/myDir").createDirectories() + val moduleUris = + listOf( + writePklFile( + "test1.pkl", + """ + output { + files { + ["."] { text = "bar" } + } + } + """ + .trimIndent() + ), + writePklFile( + "test2.pkl", + """ + output { + files { + ["myDir"] { text = "bar" } + } + } + """ + .trimIndent() + ) + ) + for (moduleUri in moduleUris) { + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + multipleFileOutputPath = ".output" + ) + assertThatCode { evalToConsole(options) } + .hasMessageStartingWith("Output file conflict:") + .hasMessageContaining("which is a directory") + } + } + + @Test + fun `multiple file output throws on conflicting files`() { + val moduleUris = + listOf( + writePklFile( + "test1.pkl", + """ + output { + files { + ["foo.txt"] { text = "bar" } + } + } + """ + .trimIndent() + ), + writePklFile( + "test2.pkl", + """ + output { + files { + ["foo.txt"] { text = "bar" } + } + } + """ + .trimIndent() + ) + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = moduleUris, workingDir = tempDir), + multipleFileOutputPath = ".output" + ) + assertThatCode { evalToConsole(options) } + .hasMessageContaining("Output file conflict:") + .hasMessageContaining("resolve to the same file path") + } + + @Test + fun `multi-output throws on conflicting files within the same module`() { + val moduleUri = + writePklFile( + "test.pkl", + """ + output { + files { + ["foo.txt"] { text = "bar" } + ["./foo.txt"] { text = "bar" } + } + } + """ + .trimIndent() + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + multipleFileOutputPath = ".output" + ) + assertThatCode { evalToConsole(options) } + .hasMessageContaining("Output file conflict:") + .hasMessageContaining("resolve to the same file path") + } + + @Test + fun `evaluate output expression`() { + val moduleUri = + writePklFile( + "test.pkl", + """ + foo { + bar = 1 + } + """ + .trimIndent() + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + expression = "foo", + ) + val buffer = StringWriter() + CliEvaluator(options, consoleWriter = buffer).run() + assertThat(buffer.toString()) + .isEqualTo( + """ + new Dynamic { bar = 1 } + """ + .trimIndent() + ) + } + + @Test + fun `evaluate output expression - custom toString()`() { + val moduleUri = + writePklFile( + "test.pkl", + """ + class Person { + name: String + + function toString() = "Person(\(name))" + } + person: Person = new { name = "Frodo" } + """ + .trimIndent() + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + expression = "person", + ) + val buffer = StringWriter() + CliEvaluator(options, consoleWriter = buffer).run() + assertThat(buffer.toString()).isEqualTo("Person(Frodo)") + } + + @Test + fun `evaluate output expression - nested structure`() { + val moduleUri = + writePklFile( + "test.pkl", + """ + person { + friend { name = "Bilbo" } + } + """ + .trimIndent() + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + expression = "person", + ) + val buffer = StringWriter() + CliEvaluator(options, consoleWriter = buffer).run() + assertThat(buffer.toString()).isEqualTo("new Dynamic { friend { name = \"Bilbo\" } }") + } + + @Test + fun `skip PklProject file`() { + val moduleUri = + writePklFile( + "test.pkl", + """ + res = 1 + """ + .trimIndent() + ) + writePklFile( + "PklProject", + """ + amends "pkl:Project" + + package = throw("invalid project package") + """ + .trimIndent() + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir, noProject = true), + ) + val buffer = StringWriter() + CliEvaluator(options, consoleWriter = buffer).run() + assertThat(buffer.toString()).isEqualTo("res = 1\n") + } + + @Test + fun `settings from PklProject file`() { + + val moduleUri = + writePklFile( + "test.pkl", + """ + res = read*("env:**") + """ + .trimIndent() + ) + writePklFile( + "PklProject", + // language=Pkl + """ + amends "pkl:Project" + + evaluatorSettings { + env { + ["foo"] = "foo" + ["bar"] = "bar" + } + } + """ + .trimIndent() + ) + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir), + ) + val buffer = StringWriter() + CliEvaluator(options, consoleWriter = buffer).run() + assertThat(buffer.toString()) + .isEqualTo( + """ + res { + ["env:bar"] = "bar" + ["env:foo"] = "foo" + } + + """ + .trimIndent() + ) + } + + @Test + fun `setting noCache will skip writing to the cache dir`() { + PackageServer.ensureStarted() + val moduleUri = + writePklFile( + "test.pkl", + """ + import "package://localhost:12110/birds@0.5.0#/catalog/Swallow.pkl" + + res = Swallow + """ + .trimIndent() + ) + val buffer = StringWriter() + val options = + CliEvaluatorOptions( + CliBaseOptions( + sourceModules = listOf(moduleUri), + workingDir = tempDir, + moduleCacheDir = tempDir, + noCache = true, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + ) + CliEvaluator(options, consoleWriter = buffer).run() + assertThat(buffer.toString()) + .isEqualTo( + """ + res { + name = "Swallow" + favoriteFruit { + name = "Apple" + } + } + + """ + .trimIndent() + ) + assertThat(tempDir.resolve("package-1")).doesNotExist() + } + + @Test + fun `not including the self signed certificate will result in a error`() { + PackageServer.ensureStarted() + val moduleUri = + writePklFile( + "test.pkl", + """ + import "package://localhost:12110/birds@0.5.0#/catalog/Swallow.pkl" + + res = Swallow + """ + .trimIndent() + ) + val buffer = StringWriter() + val options = + CliEvaluatorOptions( + CliBaseOptions( + sourceModules = listOf(moduleUri), + workingDir = tempDir, + moduleCacheDir = tempDir, + noCache = true, + // ensure we override any previously set root cert to the default buundle. + caCertificates = listOf(BaseOptions.Companion.includedCARootCerts()) + ), + ) + val err = assertThrows { CliEvaluator(options, consoleWriter = buffer).run() } + assertThat(err.message).contains("unable to find valid certification path to requested target") + } + + private fun writePklFile(fileName: String, contents: String = defaultContents): URI { + tempDir.resolve(fileName).createParentDirectories() + return tempDir.resolve(fileName).writeString(contents).toUri() + } + + private fun evalToFiles(options: CliEvaluatorOptions): List { + val evaluator = + CliEvaluator( + options.copy( + outputPath = options.outputPath ?: "%{moduleDir}/%{moduleName}.%{outputFormat}" + ) + ) + + evaluator.run() + return evaluator.fileOutputPaths!!.values.toList() + } + + private fun evalToConsole(options: CliEvaluatorOptions): String { + val reader = StringReader("") + val writer = StringWriter() + CliEvaluator(options, reader, writer).run() + return writer.toString() + } + + private fun checkOutputFile(file: Path, name: String, contents: String) { + assertThat(file).isRegularFile.hasFileName(name) + assertThat(file.readString().trim()).isEqualTo(contents.trim()) + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt new file mode 100644 index 00000000..d81867e5 --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt @@ -0,0 +1,123 @@ +/** + * 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.cli + +import com.github.ajalt.clikt.core.BadParameterValue +import com.github.ajalt.clikt.core.subcommands +import java.nio.file.Path +import kotlin.io.path.createDirectory +import kotlin.io.path.createSymbolicLinkPointingTo +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.AssertionsForClassTypes.assertThatCode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.pkl.cli.commands.EvalCommand +import org.pkl.cli.commands.RootCommand +import org.pkl.commons.writeString + +class CliMainTest { + + private val evalCmd = EvalCommand("") + private val cmd = RootCommand("pkl", "pkl version 1", "").subcommands(evalCmd) + + @Test + fun `duplicate CLI option produces meaningful errror message`(@TempDir tempDir: Path) { + val inputFile = tempDir.resolve("test.pkl").writeString("").toString() + + assertThatCode { + cmd.parse(arrayOf("eval", "--output-path", "path1", "--output-path", "path2", inputFile)) + } + .hasMessage("Invalid value for \"--output-path\": Option cannot be repeated") + + assertThatCode { + cmd.parse(arrayOf("eval", "-o", "path1", "--output-path", "path2", inputFile)) + } + .hasMessage("Invalid value for \"--output-path\": Option cannot be repeated") + } + + @Test + fun `eval requires at least one file`() { + assertThatCode { cmd.parse(arrayOf("eval")) }.hasMessage("""Missing argument """"") + } + + @Test + fun `output to symlinked directory works`(@TempDir tempDir: Path) { + val code = + """ + x = 3 + + output { + value = x + renderer = new JsonRenderer {} + } + """ + .trimIndent() + val inputFile = tempDir.resolve("test.pkl").writeString(code).toString() + val outputFile = makeSymdir(tempDir, "out", "linkOut").resolve("test.pkl").toString() + + assertThatCode { cmd.parse(arrayOf("eval", inputFile, "-o", outputFile)) } + .doesNotThrowAnyException() + } + + @Test + fun `cannot have multiple output with -o or -x`(@TempDir tempDir: Path) { + val testIn = makeInput(tempDir) + val testOut = tempDir.resolve("test").toString() + val error = + """Invalid value for "--multiple-file-output-path": Option is mutually exclusive with -o, --output-path and -x, --expression.""" + + assertThatCode { cmd.parse(arrayOf("eval", "-m", testOut, "-x", "x", testIn)) } + .hasMessage(error) + + assertThatCode { cmd.parse(arrayOf("eval", "-m", testOut, "-o", "/tmp/test", testIn)) } + .hasMessage(error) + } + + @Test + fun `showing version works`() { + assertThatCode { cmd.parse(arrayOf("--version")) }.hasMessage("pkl version 1") + } + + @Test + fun `file paths get parsed into URIs`(@TempDir tempDir: Path) { + cmd.parse(arrayOf("eval", makeInput(tempDir, "my file.txt"))) + + val modules = evalCmd.baseOptions.baseOptions(evalCmd.modules).normalizedSourceModules + assertThat(modules).hasSize(1) + assertThat(modules[0].path).endsWith("my file.txt") + } + + @Test + fun `invalid URIs are not accepted`() { + val ex = assertThrows { cmd.parse(arrayOf("eval", "file:my file.txt")) } + + assertThat(ex.message).contains("URI `file:my file.txt` has invalid syntax") + } + + private fun makeInput(tempDir: Path, fileName: String = "test.pkl"): String { + val code = "x = 1" + return tempDir.resolve(fileName).writeString(code).toString() + } + + private fun makeSymdir(baseDir: Path, name: String, linkName: String): Path { + val dir = baseDir.resolve(name) + val link = baseDir.resolve(linkName) + dir.createDirectory() + link.createSymbolicLinkPointingTo(dir) + return link + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt new file mode 100644 index 00000000..f885cc7f --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt @@ -0,0 +1,959 @@ +/** + * 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.cli + +import java.io.StringWriter +import java.net.URI +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.util.stream.Collectors +import kotlin.io.path.createDirectories +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliException +import org.pkl.commons.cli.CliTestOptions +import org.pkl.commons.readString +import org.pkl.commons.test.FileTestUtils +import org.pkl.commons.test.PackageServer +import org.pkl.commons.writeString +import org.pkl.core.runtime.CertificateUtils + +class CliProjectPackagerTest { + @Test + fun `missing PklProject when inferring a project dir`(@TempDir tempDir: Path) { + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + ) + val err = assertThrows { packager.run() } + assertThat(err).hasMessageStartingWith("No project visible to the working directory.") + } + + @Test + fun `missing PklProject when explict dir is provided`(@TempDir tempDir: Path) { + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + ) + val err = assertThrows { packager.run() } + assertThat(err).hasMessageStartingWith("Directory $tempDir does not contain a PklProject file.") + } + + @Test + fun `PklProject missing package section`(@TempDir tempDir: Path) { + tempDir + .resolve("PklProject") + .writeString( + """ + amends "pkl:Project" + """ + .trimIndent() + ) + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + ) + val err = assertThrows { packager.run() } + assertThat(err) + .hasMessageStartingWith("No package was declared in project `${tempDir.toUri()}PklProject`.") + } + + @Test + fun `failing apiTests`(@TempDir tempDir: Path) { + tempDir.writeFile( + "myTest.pkl", + """ + amends "pkl:test" + + facts { + ["1 == 2"] { + 1 == 2 + } + } + """ + .trimIndent() + ) + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + apiTests { "myTest.pkl" } + } + """ + .trimIndent() + ) + val buffer = StringWriter() + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = buffer + ) + val err = assertThrows { packager.run() } + assertThat(err).hasMessageContaining("because its API tests are failing") + assertThat(buffer.toString()).contains("1 == 2") + } + + @Test + fun `passing apiTests`(@TempDir tempDir: Path) { + tempDir + .resolve("myTest.pkl") + .writeString( + """ + amends "pkl:test" + + facts { + ["1 == 1"] { + 1 == 1 + } + } + """ + .trimIndent() + ) + tempDir + .resolve("PklProject") + .writeString( + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + apiTests { "myTest.pkl" } + } + """ + .trimIndent() + ) + val buffer = StringWriter() + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = buffer + ) + packager.run() + } + + @Test + fun `apiTests that import dependencies`(@TempDir tempDir: Path) { + val cacheDir = tempDir.resolve("cache") + val projectDir = tempDir.resolve("myProject").createDirectories() + PackageServer.populateCacheDir(cacheDir) + projectDir + .resolve("myTest.pkl") + .writeString( + """ + amends "pkl:test" + import "@birds/Bird.pkl" + + examples { + ["Bird"] { + new Bird { name = "Finch"; favoriteFruit { name = "Tangerine" } } + } + } + """ + .trimIndent() + ) + projectDir + .resolve("PklProject") + .writeString( + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + apiTests { "myTest.pkl" } + } + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + } + """ + .trimIndent() + ) + projectDir + .resolve("PklProject.deps.json") + .writeString( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:12110/fruit@1.0.5", + "checksums": { + "sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5" + } + } + } + } + """ + .trimIndent() + ) + val buffer = StringWriter() + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = projectDir, moduleCacheDir = cacheDir), + listOf(projectDir), + CliTestOptions(), + ".out", + skipPublishCheck = true, + consoleWriter = buffer + ) + packager.run() + } + + @Test + fun `generate package`(@TempDir tempDir: Path) { + val fooPkl = + tempDir.writeFile( + "a/b/foo.pkl", + """ + module foo + + name: String + """ + .trimIndent() + ) + + val fooTxt = + tempDir.writeFile( + "c/d/foo.txt", + """ + foo + bar + baz + """ + .trimIndent() + ) + + tempDir + .resolve("PklProject") + .writeString( + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + packager.run() + val expectedMetadata = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0") + val expectedMetadataChecksum = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.sha256") + val expectedArchive = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.zip") + val expectedArchiveChecksum = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.zip.sha256") + assertThat(expectedMetadata).exists() + assertThat(expectedMetadata) + .hasContent( + """ + { + "name": "mypackage", + "packageUri": "package://example.com/mypackage@1.0.0", + "version": "1.0.0", + "packageZipUrl": "https://foo.com", + "packageZipChecksums": { + "sha256": "7f515fbc4b229ba171fac78c7c3f2c2e68e2afebae8cfba042b12733943a2813" + }, + "dependencies": {}, + "authors": [] + } + """ + .trimIndent() + ) + assertThat(expectedArchive).exists() + assertThat(expectedArchive.zipFilePaths()) + .hasSameElementsAs(listOf("/", "/c", "/c/d", "/c/d/foo.txt", "/a", "/a/b", "/a/b/foo.pkl")) + assertThat(expectedMetadataChecksum) + .hasContent("203ef387f217a3caee7f19819ef2b50926929269241cb7b3a29d95237b2c7f4b") + assertThat(expectedArchiveChecksum) + .hasContent("7f515fbc4b229ba171fac78c7c3f2c2e68e2afebae8cfba042b12733943a2813") + FileSystems.newFileSystem(URI("jar:" + expectedArchive.toUri()), mutableMapOf()) + .use { fs -> + assertThat(fs.getPath("a/b/foo.pkl")).hasSameTextualContentAs(fooPkl) + assertThat(fs.getPath("c/d/foo.txt")).hasSameTextualContentAs(fooTxt) + } + } + + @Test + fun `generate package with excludes`(@TempDir tempDir: Path) { + tempDir.apply { + writeEmptyFile("a/b/c/d.bin") + writeEmptyFile("input/foo/bar.txt") + writeEmptyFile("z.bin") + writeEmptyFile("main.pkl") + writeEmptyFile("main.test.pkl") + writeEmptyFile("child/main.pkl") + writeEmptyFile("child/main.test.pkl") + } + + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + exclude { + "*.bin" + "child/main.pkl" + "*.test.pkl" + } + } + """ + .trimIndent() + ) + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + .run() + val expectedArchive = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0.zip") + assertThat(expectedArchive.zipFilePaths()) + .hasSameElementsAs( + listOf( + "/", + "/a", + "/a/b", + "/a/b/c", + "/child", + "/input", + "/input/foo", + "/input/foo/bar.txt", + "/main.pkl", + ) + ) + } + + @Test + fun `generate packages with local dependencies`(@TempDir tempDir: Path) { + val projectDir = tempDir.resolve("project") + val project2Dir = tempDir.resolve("project2") + projectDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + ["project2"] = import("../project2/PklProject") + } + """ + .trimIndent() + ) + projectDir.writeFile( + "PklProject.deps.json", + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/project2@5": { + "type": "local", + "uri": "projectpackage://localhost:12110/project2@5.0.0", + "path": "../project2" + } + } + } + """ + .trimIndent() + ) + + project2Dir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "project2" + baseUri = "package://localhost:12110/project2" + version = "5.0.0" + packageZipUrl = "https://foo.com/project2.zip" + } + """ + .trimIndent() + ) + project2Dir.writeFile( + "PklProject.deps.json", + """ + { + "schemaVersion": 1, + "resolvedDependencies": {} + } + """ + .trimIndent() + ) + + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(projectDir, project2Dir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + .run() + val expectedMetadata = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0") + assertThat(expectedMetadata).exists() + assertThat(expectedMetadata) + .hasContent( + """ + { + "name": "mypackage", + "packageUri": "package://example.com/mypackage@1.0.0", + "version": "1.0.0", + "packageZipUrl": "https://foo.com", + "packageZipChecksums": { + "sha256": "7d08a65078e0bfc382c16fe1bb94546ab9a11e6f551087f362a4515ca98102fc" + }, + "dependencies": { + "birds": { + "uri": "package://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "project2": { + "uri": "package://localhost:12110/project2@5.0.0", + "checksums": { + "sha256": "ddebb806e5b218ebb1a2baa14ad206b46e7a0c1585fa9863a486c75592bc8b16" + } + } + }, + "authors": [] + } + """ + .trimIndent() + ) + val project2Metadata = tempDir.resolve(".out/project2@5.0.0/project2@5.0.0") + assertThat(project2Metadata).exists() + assertThat(project2Metadata.readString()) + .isEqualTo( + """ + { + "name": "project2", + "packageUri": "package://localhost:12110/project2@5.0.0", + "version": "5.0.0", + "packageZipUrl": "https://foo.com/project2.zip", + "packageZipChecksums": { + "sha256": "7d08a65078e0bfc382c16fe1bb94546ab9a11e6f551087f362a4515ca98102fc" + }, + "dependencies": {}, + "authors": [] + } + """ + .trimIndent() + ) + } + + @Test + fun `generate package with local dependencies fails if local dep is not included for packaging`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("project") + val project2Dir = tempDir.resolve("project2") + projectDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + ["project2"] = import("../project2/PklProject") + } + """ + .trimIndent() + ) + projectDir.writeFile( + "PklProject.deps.json", + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/project2@5": { + "type": "local", + "uri": "projectpackage://localhost:12110/project2@5.0.0", + "path": "../project2" + } + } + } + """ + .trimIndent() + ) + + project2Dir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "project2" + baseUri = "package://localhost:12110/project2" + version = "5.0.0" + packageZipUrl = "https://foo.com/project2.zip" + } + """ + .trimIndent() + ) + project2Dir.writeFile( + "PklProject.deps.json", + """ + { + "schemaVersion": 1, + "resolvedDependencies": {} + } + """ + .trimIndent() + ) + assertThatCode { + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(projectDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + .run() + } + .hasMessageContaining("which is not included for packaging") + } + + @Test + fun `import path verification -- relative path outside project dir`(@TempDir tempDir: Path) { + tempDir.writeFile( + "main.pkl", + """ + import "../foo.pkl" + + res = foo + """ + .trimIndent() + ) + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val e = + assertThrows { + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + .run() + } + assertThat(e.message) + .startsWith( + """ + –– Pkl Error –– + Path `../foo.pkl` includes path segments that are outside the project root directory. + + 1 | import "../foo.pkl" + ^^^^^^^^^^^^ + """ + .trimIndent() + ) + } + + @Test + fun `import path verification -- absolute import from root dir`(@TempDir tempDir: Path) { + tempDir.writeFile( + "main.pkl", + """ + import "$tempDir/foo.pkl" + + res = foo + """ + .trimIndent() + ) + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val e = + assertThrows { + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + .run() + } + assertThat(e.message) + .startsWith( + """ + –– Pkl Error –– + Path `$tempDir/foo.pkl` includes path segments that are outside the project root directory. + """ + .trimIndent() + ) + } + + @Test + fun `import path verification -- absolute read from root dir`(@TempDir tempDir: Path) { + tempDir.writeFile( + "main.pkl", + """ + res = read("$tempDir/foo.pkl") + """ + .trimIndent() + ) + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val e = + assertThrows { + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + .run() + } + assertThat(e.message) + .startsWith( + """ + –– Pkl Error –– + Path `$tempDir/foo.pkl` includes path segments that are outside the project root directory. + """ + .trimIndent() + ) + } + + @Test + fun `import path verification -- passing`(@TempDir tempDir: Path) { + tempDir.writeFile( + "foo/bar.pkl", + """ + import "baz.pkl" + """ + .trimIndent() + ) + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + .run() + } + + @Test + fun `multiple projects`(@TempDir tempDir: Path) { + tempDir.writeFile("project1/main.pkl", "res = 1") + tempDir.writeFile( + "project1/PklProject", + """ + amends "pkl:Project" + + package { + name = "project1" + version = "1.0.0" + baseUri = "package://example.com/project1" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + tempDir.writeFile("project2/main2.pkl", "res = 2") + tempDir.writeFile( + "project2/PklProject", + """ + amends "pkl:Project" + + package { + name = "project2" + version = "2.0.0" + baseUri = "package://example.com/project2" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val out = StringWriter() + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir.resolve("project1"), tempDir.resolve("project2")), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = out + ) + .run() + assertThat(out.toString()) + .isEqualTo( + """ + .out/project1@1.0.0/project1@1.0.0.zip + .out/project1@1.0.0/project1@1.0.0.zip.sha256 + .out/project1@1.0.0/project1@1.0.0 + .out/project1@1.0.0/project1@1.0.0.sha256 + .out/project2@2.0.0/project2@2.0.0.zip + .out/project2@2.0.0/project2@2.0.0.zip.sha256 + .out/project2@2.0.0/project2@2.0.0 + .out/project2@2.0.0/project2@2.0.0.sha256 + + """ + .trimIndent() + ) + assertThat(tempDir.resolve(".out/project1@1.0.0/project1@1.0.0.zip").zipFilePaths()) + .hasSameElementsAs(listOf("/", "/main.pkl")) + assertThat(tempDir.resolve(".out/project2@2.0.0/project2@2.0.0.zip").zipFilePaths()) + .hasSameElementsAs(listOf("/", "/main2.pkl")) + } + + @Test + fun `publish checks`(@TempDir tempDir: Path) { + PackageServer.ensureStarted() + CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate)) + tempDir.writeFile("project/main.pkl", "res = 1") + tempDir.writeFile( + "project/PklProject", + // intentionally conflict with localhost:12110/birds@0.5.0 from our test fixtures + """ + amends "pkl:Project" + + package { + name = "birds" + version = "0.5.0" + baseUri = "package://localhost:12110/birds" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val e = + assertThrows { + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir.resolve("project")), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = false, + consoleWriter = StringWriter() + ) + .run() + } + assertThat(e) + .hasMessageStartingWith( + """ + Package `package://localhost:12110/birds@0.5.0` was already published with different contents. + + Computed checksum: 7324e17214b6dcda63ebfb57d5a29b077af785c13bed0dc22b5138628a3f8d8f + Published checksum: 3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118 + """ + .trimIndent() + ) + } + + @Test + fun `publish check when package is not yet published`(@TempDir tempDir: Path) { + PackageServer.ensureStarted() + CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate)) + tempDir.writeFile("project/main.pkl", "res = 1") + tempDir.writeFile( + "project/PklProject", + """ + amends "pkl:Project" + + package { + name = "mangos" + version = "1.0.0" + baseUri = "package://localhost:12110/mangos" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val out = StringWriter() + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir.resolve("project")), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = false, + consoleWriter = out + ) + .run() + assertThat(out.toString()) + .isEqualTo( + """ + .out/mangos@1.0.0/mangos@1.0.0.zip + .out/mangos@1.0.0/mangos@1.0.0.zip.sha256 + .out/mangos@1.0.0/mangos@1.0.0 + .out/mangos@1.0.0/mangos@1.0.0.sha256 + + """ + .trimIndent() + ) + } + + private fun Path.zipFilePaths(): List { + return FileSystems.newFileSystem(URI("jar:${toUri()}"), emptyMap()).use { fs -> + Files.walk(fs.getPath("/")).map { it.toString() }.collect(Collectors.toList()) + } + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectResolverTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectResolverTest.kt new file mode 100644 index 00000000..068d84ba --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectResolverTest.kt @@ -0,0 +1,439 @@ +/** + * 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.cli + +import java.io.StringWriter +import java.nio.file.Path +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliException +import org.pkl.commons.test.FileTestUtils +import org.pkl.commons.test.PackageServer + +class CliProjectResolverTest { + companion object { + @BeforeAll + @JvmStatic + fun beforeAll() { + PackageServer.ensureStarted() + } + } + + @Test + fun `missing PklProject when inferring a project dir`(@TempDir tempDir: Path) { + val packager = + CliProjectResolver( + CliBaseOptions(workingDir = tempDir), + emptyList(), + consoleWriter = StringWriter(), + errWriter = StringWriter() + ) + val err = assertThrows { packager.run() } + assertThat(err).hasMessageStartingWith("No project visible to the working directory.") + } + + @Test + fun `missing PklProject when explict dir is provided`(@TempDir tempDir: Path) { + val packager = + CliProjectResolver( + CliBaseOptions(), + listOf(tempDir), + consoleWriter = StringWriter(), + errWriter = StringWriter() + ) + val err = assertThrows { packager.run() } + assertThat(err).hasMessageStartingWith("Directory $tempDir does not contain a PklProject file.") + } + + @Test + fun `basic project`(@TempDir tempDir: Path) { + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + } + """ + .trimIndent() + ) + CliProjectResolver( + CliBaseOptions( + workingDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + listOf(tempDir), + consoleWriter = StringWriter(), + errWriter = StringWriter() + ) + .run() + val expectedOutput = tempDir.resolve("PklProject.deps.json") + assertThat(expectedOutput) + .hasContent( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:12110/fruit@1.0.5", + "checksums": { + "sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5" + } + } + } + } + """ + .trimIndent() + ) + } + + @Test + fun `basic project, inferred from working dir`(@TempDir tempDir: Path) { + tempDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + } + """ + .trimIndent() + ) + CliProjectResolver( + CliBaseOptions( + workingDir = tempDir, + caCertificates = listOf(FileTestUtils.selfSignedCertificate) + ), + emptyList(), + consoleWriter = StringWriter(), + errWriter = StringWriter() + ) + .run() + val expectedOutput = tempDir.resolve("PklProject.deps.json") + assertThat(expectedOutput) + .hasContent( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:12110/fruit@1.0.5", + "checksums": { + "sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5" + } + } + } + } + """ + .trimIndent() + ) + } + + @Test + fun `local dependencies`(@TempDir tempDir: Path) { + val projectDir = tempDir.resolve("theproject") + projectDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + ["project2"] = import("../project2/PklProject") + } + """ + .trimIndent() + ) + projectDir.writeFile( + "../project2/PklProject", + """ + amends "pkl:Project" + + package { + name = "project2" + baseUri = "package://localhost:12110/package2" + version = "5.0.0" + packageZipUrl = "https://foo.com/package2.zip" + } + + dependencies { + ["fruit"] { + uri = "package://localhost:12110/fruit@1.0.5" + } + ["project3"] = import("../project3/PklProject") + } + """ + .trimIndent() + ) + + projectDir.writeFile( + "../project3/PklProject", + """ + amends "pkl:Project" + + package { + name = "project3" + baseUri = "package://localhost:12110/package3" + version = "5.0.0" + packageZipUrl = "https://foo.com/package3.zip" + } + + dependencies { + ["fruit"] { + uri = "package://localhost:12110/fruit@1.1.0" + } + } + """ + .trimIndent() + ) + CliProjectResolver( + CliBaseOptions(caCertificates = listOf(FileTestUtils.selfSignedCertificate)), + listOf(projectDir), + consoleWriter = StringWriter(), + errWriter = StringWriter() + ) + .run() + val expectedOutput = projectDir.resolve("PklProject.deps.json") + assertThat(expectedOutput) + .hasContent( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:12110/fruit@1.1.0", + "checksums": { + "sha256": "98ad9fc407a79dc3fd5595e7a29c3803ade0a6957c18ec94b8a1624360b24f01" + } + }, + "package://localhost:12110/package2@5": { + "type": "local", + "uri": "projectpackage://localhost:12110/package2@5.0.0", + "path": "../project2" + }, + "package://localhost:12110/package3@5": { + "type": "local", + "uri": "projectpackage://localhost:12110/package3@5.0.0", + "path": "../project3" + } + } + } + """ + .trimIndent() + ) + } + + @Test + fun `local dependency overridden by remote dependency`(@TempDir tempDir: Path) { + val projectDir = tempDir.resolve("theproject") + projectDir.writeFile( + "PklProject", + """ + amends "pkl:Project" + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + ["fruit"] = import("../fruit/PklProject") + } + """ + .trimIndent() + ) + projectDir.writeFile( + "../fruit/PklProject", + """ + amends "pkl:Project" + + package { + name = "fruit" + baseUri = "package://localhost:12110/fruit" + version = "1.0.0" + packageZipUrl = "https://foo.com/fruit.zip" + } + """ + .trimIndent() + ) + val consoleOut = StringWriter() + val errOut = StringWriter() + CliProjectResolver( + CliBaseOptions(caCertificates = listOf(FileTestUtils.selfSignedCertificate)), + listOf(projectDir), + consoleWriter = consoleOut, + errWriter = errOut + ) + .run() + val expectedOutput = projectDir.resolve("PklProject.deps.json") + assertThat(expectedOutput) + .hasContent( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:12110/fruit@1.0.5", + "checksums": { + "sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5" + } + } + } + } + """ + .trimIndent() + ) + assertThat(errOut.toString()) + .isEqualTo( + "WARN: local dependency `package://localhost:12110/fruit@1.0.0` was overridden to remote dependency `package://localhost:12110/fruit@1.0.5`.\n" + ) + } + + @Test + fun `resolving multiple projects`(@TempDir tempDir: Path) { + tempDir.writeFile( + "project1/PklProject", + """ + amends "pkl:Project" + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + } + """ + .trimIndent() + ) + + tempDir.writeFile( + "project2/PklProject", + """ + amends "pkl:Project" + + dependencies { + ["fruit"] { + uri = "package://localhost:12110/fruit@1.1.0" + } + } + """ + .trimIndent() + ) + + val consoleOut = StringWriter() + val errOut = StringWriter() + CliProjectResolver( + CliBaseOptions(caCertificates = listOf(FileTestUtils.selfSignedCertificate)), + listOf(tempDir.resolve("project1"), tempDir.resolve("project2")), + consoleWriter = consoleOut, + errWriter = errOut + ) + .run() + assertThat(consoleOut.toString()) + .isEqualTo( + """ + $tempDir/project1/PklProject.deps.json + $tempDir/project2/PklProject.deps.json + + """ + .trimIndent() + ) + assertThat(tempDir.resolve("project1/PklProject.deps.json")) + .hasContent( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:12110/birds@0.5.0", + "checksums": { + "sha256": "3f19ab9fcee2f44f93a75a09e531db278c6d2cd25206836c8c2c4071cd7d3118" + } + }, + "package://localhost:12110/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:12110/fruit@1.0.5", + "checksums": { + "sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5" + } + } + } + } + """ + .trimIndent() + ) + assertThat(tempDir.resolve("project2/PklProject.deps.json")) + .hasContent( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:12110/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:12110/fruit@1.1.0", + "checksums": { + "sha256": "98ad9fc407a79dc3fd5595e7a29c3803ade0a6957c18ec94b8a1624360b24f01" + } + } + } + } + """ + .trimIndent() + ) + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt new file mode 100644 index 00000000..79435e93 --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt @@ -0,0 +1,208 @@ +/** + * 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.cli + +import com.github.ajalt.clikt.core.MissingArgument +import com.github.ajalt.clikt.core.subcommands +import java.io.StringWriter +import java.net.URI +import java.nio.file.Path +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.pkl.cli.commands.EvalCommand +import org.pkl.cli.commands.RootCommand +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.CliException +import org.pkl.commons.cli.CliTestOptions +import org.pkl.commons.readString +import org.pkl.commons.toUri +import org.pkl.commons.writeString +import org.pkl.core.Release + +class CliTestRunnerTest { + + @Test + fun `CliTestRunner succeed test`(@TempDir tempDir: Path) { + val code = + """ + amends "pkl:test" + + facts { + ["succeed"] { + 8 == 8 + 3 == 3 + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(code).toString() + val out = StringWriter() + val err = StringWriter() + val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val testOpts = CliTestOptions() + val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) + runner.run() + + assertThat(out.toString().stripFileAndLines(tempDir)) + .isEqualTo( + """ + module test + succeed ✅ + + """ + .trimIndent() + ) + assertThat(err.toString()).isEqualTo("") + } + + @Test + fun `CliTestRunner fail test`(@TempDir tempDir: Path) { + val code = + """ + amends "pkl:test" + + facts { + ["fail"] { + 4 == 9 + "foo" == "bar" + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(code).toString() + val out = StringWriter() + val err = StringWriter() + val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val testOpts = CliTestOptions() + val runner = CliTestRunner(opts, testOpts, consoleWriter = out, errWriter = err) + assertThatCode { runner.run() }.hasMessage("Tests failed.") + + assertThat(out.toString().stripFileAndLines(tempDir)) + .isEqualTo( + """ + module test + fail ❌ + 4 == 9 ❌ + "foo" == "bar" ❌ + + """ + .trimIndent() + ) + assertThat(err.toString()).isEqualTo("") + } + + @Test + fun `CliTestRunner JUnit reports`(@TempDir tempDir: Path) { + val code = + """ + amends "pkl:test" + + facts { + ["foo"] { + 9 == trace(9) + "foo" == "foo" + } + ["fail"] { + 5 == 9 + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(code).toString() + val opts = CliBaseOptions(sourceModules = listOf(input.toUri()), settings = URI("pkl:settings")) + val testOpts = CliTestOptions(junitDir = tempDir) + val runner = CliTestRunner(opts, testOpts) + assertThatCode { runner.run() }.hasMessageContaining("failed") + + val junitReport = tempDir.resolve("test.xml").readString().stripFileAndLines(tempDir) + assertThat(junitReport) + .isEqualTo( + """ + + + + + 5 == 9 ❌ + + + + + """ + .trimIndent() + ) + } + + @Test + fun `CliTestRunner duplicated JUnit reports`(@TempDir tempDir: Path) { + val foo = + """ + module foo + + amends "pkl:test" + + facts { + ["foo"] { + 1 == 1 + } + } + """ + .trimIndent() + + val bar = + """ + module foo + + amends "pkl:test" + + facts { + ["foo"] { + 1 == 1 + } + } + """ + .trimIndent() + val input = tempDir.resolve("test.pkl").writeString(foo).toString() + val input2 = tempDir.resolve("test.pkl").writeString(bar).toString() + val opts = + CliBaseOptions( + sourceModules = listOf(input.toUri(), input2.toUri()), + settings = URI("pkl:settings") + ) + val testOpts = CliTestOptions(junitDir = tempDir) + val runner = CliTestRunner(opts, testOpts) + assertThatCode { runner.run() }.hasMessageContaining("failed") + } + + @Test + fun `no source modules specified has same message as pkl eval`() { + val e1 = assertThrows { CliTestRunner(CliBaseOptions(), CliTestOptions()).run() } + val e2 = + assertThrows { + val rootCommand = + RootCommand("pkl", Release.current().versionInfo(), "").subcommands(EvalCommand("")) + rootCommand.parse(listOf("eval")) + } + assertThat(e1).hasMessageContaining("Missing argument \"\"") + assertThat(e1.message!!.replace("test", "eval")).isEqualTo(e2.helpMessage()) + } + + private fun String.stripFileAndLines(tmpDir: Path) = + replace(tmpDir.toUri().toString(), "/tempDir/").replace(Regex(""" \(.*, line \d+\)"""), "") +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt new file mode 100644 index 00000000..cb15c67a --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt @@ -0,0 +1,64 @@ +/** + * 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.cli.repl + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.pkl.commons.toPath +import org.pkl.core.Loggers +import org.pkl.core.SecurityManagers +import org.pkl.core.StackFrameTransformers +import org.pkl.core.module.ModuleKeyFactories +import org.pkl.core.repl.ReplRequest +import org.pkl.core.repl.ReplResponse +import org.pkl.core.repl.ReplServer + +class ReplMessagesTest { + private val server = + ReplServer( + SecurityManagers.defaultManager, + Loggers.stdErr(), + listOf(ModuleKeyFactories.standardLibrary), + listOf(), + mapOf(), + mapOf(), + null, + null, + null, + "/".toPath(), + StackFrameTransformers.defaultTransformer + ) + + @Test + fun `run examples`() { + val examples = ReplMessages.examples + var startIndex = examples.indexOf("```") + while (startIndex != -1) { + val endIndex = examples.indexOf("```", startIndex + 3) + assertThat(endIndex).isNotEqualTo(-1) + val text = + examples + .substring(startIndex + 3, endIndex) + .lines() + .filterNot { it.contains(":force") } + .joinToString("\n") + val responses = server.handleRequest(ReplRequest.Eval("1", text, true, true)) + assertThat(responses.size).isBetween(1, 9) + assertThat(responses).hasOnlyElementsOfType(ReplResponse.EvalSuccess::class.java) + startIndex = examples.indexOf("```", endIndex + 3) + } + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/testExtensions.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/testExtensions.kt new file mode 100644 index 00000000..100a1f3a --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/testExtensions.kt @@ -0,0 +1,30 @@ +/** + * 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.cli + +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import org.pkl.commons.createParentDirectories +import org.pkl.commons.writeString + +fun Path.writeFile(fileName: String, contents: String): Path { + return resolve(fileName).apply { + createParentDirectories() + writeString(contents, StandardCharsets.UTF_8) + } +} + +fun Path.writeEmptyFile(fileName: String): Path = writeFile(fileName, "") diff --git a/pkl-codegen-java/gradle.lockfile b/pkl-codegen-java/gradle.lockfile new file mode 100644 index 00000000..9bf6cba9 --- /dev/null +++ b/pkl-codegen-java/gradle.lockfile @@ -0,0 +1,39 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.github.ajalt.clikt:clikt-jvm:3.5.1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt:3.5.1=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.squareup:javapoet:1.13.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,runtimeClasspath,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.14=testRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-codegen-java/pkl-codegen-java.gradle.kts b/pkl-codegen-java/pkl-codegen-java.gradle.kts new file mode 100644 index 00000000..20b0e309 --- /dev/null +++ b/pkl-codegen-java/pkl-codegen-java.gradle.kts @@ -0,0 +1,44 @@ +plugins { + pklAllProjects + pklKotlinLibrary + pklPublishLibrary +} + +dependencies { + // CliJavaCodeGeneratorOptions exposes CliBaseOptions + api(project(":pkl-commons-cli")) + + implementation(project(":pkl-commons")) + implementation(project(":pkl-core")) + implementation(libs.javaPoet) + + testImplementation(project(":pkl-config-java")) + testImplementation(project(":pkl-commons-test")) +} + +// with `org.gradle.parallel=true` and without the line below, `test` strangely runs into: +// java.lang.NoClassDefFoundError: Lorg/pkl/config/java/ConfigEvaluator; +// perhaps somehow related to InMemoryJavaCompiler? +tasks.test { + mustRunAfter(":pkl-config-java:testFatJar") +} + +tasks.jar { + manifest { + attributes += mapOf("Main-Class" to "org.pkl.codegen.java.Main") + } +} + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-codegen-java") + description.set(""" + Java source code generator that generates corresponding Java classes for Pkl classes, + simplifying consumption of Pkl configuration as statically typed Java objects. + """.trimIndent()) + } + } + } +} diff --git a/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/CliJavaCodeGenerator.kt b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/CliJavaCodeGenerator.kt new file mode 100644 index 00000000..1e0ef2f6 --- /dev/null +++ b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/CliJavaCodeGenerator.kt @@ -0,0 +1,55 @@ +/** + * 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.codegen.java + +import java.io.IOException +import org.pkl.commons.cli.CliCommand +import org.pkl.commons.cli.CliException +import org.pkl.commons.createParentDirectories +import org.pkl.commons.writeString +import org.pkl.core.ModuleSource +import org.pkl.core.module.ModuleKeyFactories + +/** API for the Java code generator CLI. */ +class CliJavaCodeGenerator(private val options: CliJavaCodeGeneratorOptions) : + CliCommand(options.base) { + + override fun doRun() { + val builder = evaluatorBuilder() + try { + builder.build().use { evaluator -> + for (moduleUri in options.base.normalizedSourceModules) { + val schema = evaluator.evaluateSchema(ModuleSource.uri(moduleUri)) + val codeGenerator = JavaCodeGenerator(schema, options.toJavaCodegenOptions()) + try { + for ((fileName, fileContents) in codeGenerator.output) { + val outputFile = options.outputDir.resolve(fileName) + try { + outputFile.createParentDirectories().writeString(fileContents) + } catch (e: IOException) { + throw CliException("I/O error writing file `$outputFile`.\nCause: ${e.message}") + } + } + } catch (e: JavaCodeGeneratorException) { + throw CliException(e.message!!) + } + } + } + } finally { + ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories) + } + } +} diff --git a/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/CliJavaCodeGeneratorOptions.kt b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/CliJavaCodeGeneratorOptions.kt new file mode 100644 index 00000000..aaa3a92c --- /dev/null +++ b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/CliJavaCodeGeneratorOptions.kt @@ -0,0 +1,69 @@ +/** + * 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.codegen.java + +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions + +/** Configuration options for [CliJavaCodeGenerator]. */ +data class CliJavaCodeGeneratorOptions( + /** Base options shared between CLI commands. */ + val base: CliBaseOptions, + + /** The directory where generated source code is placed. */ + val outputDir: Path, + + /** The characters to use for indenting generated source code. */ + val indent: String = " ", + + /** + * Whether to generate public getter methods and private/protected fields instead of public + * fields. + */ + val generateGetters: Boolean = false, + + /** Whether to generate Javadoc based on doc comments for Pkl modules, classes, and properties. */ + val generateJavadoc: Boolean = false, + + /** Whether to generate config classes for use with Spring Boot. */ + val generateSpringBootConfig: Boolean = false, + + /** + * Fully qualified name of the annotation to use on constructor parameters. If this options is not + * set, [org.pkl.config.java.mapper.Named] will be used. + */ + val paramsAnnotation: String? = null, + + /** + * Fully qualified name of the annotation to use on non-null properties. If this option is not + * set, [org.pkl.config.java.mapper.NonNull] will be used. + */ + val nonNullAnnotation: String? = null, + + /** Whether to make generated classes implement [java.io.Serializable] */ + val implementSerializable: Boolean = false +) { + fun toJavaCodegenOptions() = + JavaCodegenOptions( + indent, + generateGetters, + generateJavadoc, + generateSpringBootConfig, + paramsAnnotation, + nonNullAnnotation, + implementSerializable + ) +} diff --git a/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/JavaCodeGenerator.kt b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/JavaCodeGenerator.kt new file mode 100644 index 00000000..872fafa8 --- /dev/null +++ b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/JavaCodeGenerator.kt @@ -0,0 +1,933 @@ +/** + * 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.codegen.java + +import com.squareup.javapoet.* +import java.io.StringWriter +import java.lang.Deprecated +import java.net.URI +import java.util.* +import java.util.regex.Pattern +import javax.lang.model.element.Modifier +import kotlin.AssertionError +import kotlin.Boolean +import kotlin.Int +import kotlin.Long +import kotlin.RuntimeException +import kotlin.String +import kotlin.Suppress +import kotlin.Unit +import kotlin.apply +import kotlin.let +import kotlin.takeIf +import kotlin.to +import org.pkl.core.* +import org.pkl.core.util.CodeGeneratorUtils + +class JavaCodeGeneratorException(message: String) : RuntimeException(message) + +data class JavaCodegenOptions( + /** The characters to use for indenting generated Java code. */ + val indent: String = " ", + + /** + * Whether to generate public getter methods and protected final fields instead of public final + * fields. + */ + val generateGetters: Boolean = false, + + /** Whether to generate Javadoc based on doc comments for Pkl modules, classes, and properties. */ + val generateJavadoc: Boolean = false, + + /** Whether to generate config classes for use with Spring Boot. */ + val generateSpringBootConfig: Boolean = false, + + /** + * Fully qualified name of the annotation to use on constructor parameters. If this options is not + * set, [org.pkl.config.java.mapper.Named] will be used. + */ + val paramsAnnotation: String? = null, + + /** + * Fully qualified name of the annotation to use on non-null properties. If this option is not + * set, [org.pkl.config.java.mapper.NonNull] will be used. + */ + val nonNullAnnotation: String? = null, + + /** Whether to make generated classes implement [java.io.Serializable] */ + val implementSerializable: Boolean = false +) + +/** Entrypoint for the Java code generator API. */ +class JavaCodeGenerator( + private val schema: ModuleSchema, + private val codegenOptions: JavaCodegenOptions +) { + + companion object { + private val STRING = ClassName.get(String::class.java) + private val DURATION = ClassName.get(Duration::class.java) + private val DURATION_UNIT = ClassName.get(DurationUnit::class.java) + private val DATA_SIZE = ClassName.get(DataSize::class.java) + private val DATASIZE_UNIT = ClassName.get(DataSizeUnit::class.java) + private val PAIR = ClassName.get(Pair::class.java) + private val COLLECTION = ClassName.get(Collection::class.java) + private val LIST = ClassName.get(List::class.java) + private val SET = ClassName.get(Set::class.java) + private val MAP = ClassName.get(Map::class.java) + private val PMODULE = ClassName.get(PModule::class.java) + private val PCLASS = ClassName.get(PClass::class.java) + private val PATTERN = ClassName.get(Pattern::class.java) + private val URI = ClassName.get(URI::class.java) + private val VERSION = ClassName.get(Version::class.java) + + private const val PROPERTY_PREFIX: String = "org.pkl.config.java.mapper." + + private fun toClassName(fqn: String): ClassName { + val index = fqn.lastIndexOf(".") + if (index == -1) { + throw JavaCodeGeneratorException( + """ + Annotation `$fqn` is not a valid Java class. + The name of the annotation should be the canonical Java name of the class, for example, `com.example.Foo`. + """ + .trimIndent() + ) + } + val packageName = fqn.substring(0, index) + val classParts = fqn.substring(index + 1).split('$') + return if (classParts.size == 1) { + ClassName.get(packageName, classParts.first()) + } else { + ClassName.get(packageName, classParts.first(), *classParts.drop(1).toTypedArray()) + } + } + } + + val output: Map + get() { + return mapOf(javaFileName to javaFile, propertyFileName to propertiesFile) + } + + private val propertyFileName: String + get() = "resources/META-INF/org/pkl/config/java/mapper/classes/${schema.moduleName}.properties" + + private val propertiesFile: String + get() { + val props = Properties() + props["$PROPERTY_PREFIX${schema.moduleClass.qualifiedName}"] = + schema.moduleClass.toJavaPoetName().reflectionName() + for (pClass in schema.classes.values) { + props["$PROPERTY_PREFIX${pClass.qualifiedName}"] = pClass.toJavaPoetName().reflectionName() + } + return StringWriter() + .apply { props.store(this, "Java mappings for Pkl module `${schema.moduleName}`") } + .toString() + } + + private val nonNullAnnotation: AnnotationSpec + get() { + val annotation = codegenOptions.nonNullAnnotation + val className = + if (annotation == null) { + ClassName.get("org.pkl.config.java.mapper", "NonNull") + } else { + toClassName(annotation) + } + return AnnotationSpec.builder(className).build() + } + + val javaFileName: String + get() = relativeOutputPathFor(schema.moduleName) + + val javaFile: String + get() { + if (schema.moduleUri.scheme == "pkl") { + throw JavaCodeGeneratorException( + "Cannot generate Java code for a Pkl standard library module (`${schema.moduleUri}`)." + ) + } + + val pModuleClass = schema.moduleClass + val moduleClass = generateTypeSpec(pModuleClass, schema) + + for (pClass in schema.classes.values) { + moduleClass.addType(generateTypeSpec(pClass, schema).build()) + } + + for (typeAlias in schema.typeAliases.values) { + val stringLiterals = mutableSetOf() + if (CodeGeneratorUtils.isRepresentableAsEnum(typeAlias.aliasedType, stringLiterals)) { + moduleClass.addType(generateEnumTypeSpec(typeAlias, stringLiterals).build()) + } + } + // generate static append method for module classes w/o parent class; reuse in subclasses and + // nested classes + if (pModuleClass.superclass!!.info == PClassInfo.Module) { + val modifier = + if (pModuleClass.isOpen || pModuleClass.isAbstract) Modifier.PROTECTED + else Modifier.PRIVATE + moduleClass.addMethod(appendPropertyMethod().addModifiers(modifier).build()) + } + + val moduleName = schema.moduleName + val index = moduleName.lastIndexOf(".") + val packageName = if (index == -1) "" else moduleName.substring(0, index) + + return JavaFile.builder(packageName, moduleClass.build()) + .indent(codegenOptions.indent) + .build() + .toString() + } + + private fun relativeOutputPathFor(moduleName: String): String { + val moduleNameParts = moduleName.split(".") + val dirPath = moduleNameParts.dropLast(1).joinToString("/") + val fileName = moduleNameParts.last().replaceFirstChar { it.titlecaseChar() } + return if (dirPath.isEmpty()) { + "java/$fileName.java" + } else { + "java/$dirPath/$fileName.java" + } + } + + @Suppress("NAME_SHADOWING") + private fun generateTypeSpec(pClass: PClass, schema: ModuleSchema): TypeSpec.Builder { + + val isModuleClass = pClass == schema.moduleClass + val javaPoetClassName = pClass.toJavaPoetName() + val superclass = + pClass.superclass?.takeIf { it.info != PClassInfo.Typed && it.info != PClassInfo.Module } + val superProperties = + superclass?.let { renameIfReservedWord(it.allProperties) }?.filterValues { !it.isHidden } + ?: mapOf() + val properties = renameIfReservedWord(pClass.properties).filterValues { !it.isHidden } + val allProperties = superProperties + properties + + fun addCtorParameter( + builder: MethodSpec.Builder, + propJavaName: String, + property: PClass.Property + ) { + builder.addParameter( + ParameterSpec.builder(property.type.toJavaPoetName(), propJavaName) + .addAnnotation( + AnnotationSpec.builder(namedAnnotationName) + .addMember("value", "\$S", property.simpleName) + .build() + ) + .build() + ) + } + + fun generateConstructor(): MethodSpec { + val builder = + MethodSpec.constructorBuilder() + // choose most restrictive access modifier possible + .addModifiers( + when { + pClass.isAbstract -> Modifier.PROTECTED + allProperties.isNotEmpty() -> Modifier.PUBLIC // if `false`, has no state + pClass.isOpen -> Modifier.PROTECTED + else -> Modifier.PRIVATE + } + ) + + if (superProperties.isNotEmpty()) { + for ((name, property) in superProperties) { + if (properties.containsKey(name)) continue + addCtorParameter(builder, name, property) + } + // $W inserts space or newline (automatic line wrapping) + val callArgsStr = superProperties.keys.joinToString(",\$W") + // use kotlin interpolation rather than javapoet $L interpolation + // as otherwise the $W won't get processed + builder.addStatement("super($callArgsStr)") + } + + for ((name, property) in properties) { + addCtorParameter(builder, name, property) + builder.addStatement("this.\$N = \$N", name, name) + } + + return builder.build() + } + + fun generateEqualsMethod(): MethodSpec { + val builder = + MethodSpec.methodBuilder("equals") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override::class.java) + .addParameter(Object::class.java, "obj") + .returns(Boolean::class.java) + .addStatement("if (this == obj) return true") + .addStatement("if (obj == null) return false") + // generating this.getClass() instead of class literal avoids a SpotBugs warning + .addStatement("if (this.getClass() != obj.getClass()) return false") + .addStatement("\$T other = (\$T) obj", javaPoetClassName, javaPoetClassName) + + for ((propertyName, property) in allProperties) { + val accessor = + if ((property.type as? PType.Class)?.pClass?.info == PClassInfo.Regex) "\$N.pattern()" + else "\$N" + builder.addStatement( + "if (!\$T.equals(this.$accessor, other.$accessor)) return false", + Objects::class.java, + propertyName, + propertyName + ) + } + + builder.addStatement("return true") + return builder.build() + } + + fun generateHashCodeMethod(): MethodSpec { + val builder = + MethodSpec.methodBuilder("hashCode") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override::class.java) + .returns(Int::class.java) + .addStatement("int result = 1") + + for (propertyName in allProperties.keys) { + builder.addStatement( + "result = 31 * result + \$T.hashCode(this.\$N)", + Objects::class.java, + propertyName + ) + } + + builder.addStatement("return result") + return builder.build() + } + + fun generateToStringMethod(): MethodSpec { + val builder = + MethodSpec.methodBuilder("toString") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override::class.java) + .returns(String::class.java) + + var builderSize = 50 + val appendBuilder = CodeBlock.builder() + for (propertyName in allProperties.keys) { + builderSize += 50 + appendBuilder.addStatement( + "appendProperty(builder, \$S, this.\$N)", + propertyName, + propertyName + ) + } + + builder + .addStatement( + "\$T builder = new \$T(\$L)", + StringBuilder::class.java, + StringBuilder::class.java, + builderSize + ) + .addStatement("builder.append(\$T.class.getSimpleName()).append(\" {\")", javaPoetClassName) + .addCode(appendBuilder.build()) + // not using $S here because it generates `"\n" + "{"` + // with a line break in the generated code after `+` + .addStatement("builder.append(\"\\n}\")") + .addStatement("return builder.toString()") + + return builder.build() + } + + // do the minimum work necessary to avoid (most) java compile errors + // generating idiomatic Javadoc would require parsing doc comments, converting member links, + // etc. + fun renderAsJavadoc(docComment: String): String { + val escaped = docComment.replace("*/", "*/") + return if (escaped[escaped.length - 1] != '\n') escaped + '\n' else escaped + } + + fun generateDeprecation( + annotations: Collection, + hasJavadoc: Boolean, + addAnnotation: (Class<*>) -> Unit, + addJavadoc: (String) -> Unit + ) { + annotations + .firstOrNull { it.classInfo == PClassInfo.Deprecated } + ?.let { deprecation -> + addAnnotation(Deprecated::class.java) + if (codegenOptions.generateJavadoc) { + (deprecation["message"] as String?)?.let { + if (hasJavadoc) { + addJavadoc("\n") + } + addJavadoc(renderAsJavadoc("@deprecated $it")) + } + } + } + } + + fun generateField(propertyName: String, property: PClass.Property): FieldSpec { + val builder = FieldSpec.builder(property.type.toJavaPoetName(), propertyName) + + val docComment = property.docComment + val hasJavadoc = + docComment != null && codegenOptions.generateJavadoc && !codegenOptions.generateGetters + if (hasJavadoc) { + builder.addJavadoc(renderAsJavadoc(docComment!!)) + } + + if (codegenOptions.generateGetters) { + builder.addModifiers( + if (pClass.isAbstract || pClass.isOpen) Modifier.PROTECTED else Modifier.PRIVATE + ) + } else { + generateDeprecation( + property.annotations, + hasJavadoc, + { builder.addAnnotation(it) }, + { builder.addJavadoc(it) } + ) + builder.addModifiers(Modifier.PUBLIC) + } + builder.addModifiers(Modifier.FINAL) + + return builder.build() + } + + @Suppress("DuplicatedCode") + fun generateGetter( + propertyName: String, + property: PClass.Property, + isOverridden: Boolean + ): MethodSpec { + val propertyType = property.type + val isBooleanProperty = + propertyType is PType.Class && propertyType.pClass.info == PClassInfo.Boolean + val methodName = + (if (isBooleanProperty) "is" else "get") + + // can use original name here (property.name rather than propertyName) + // because getter name cannot possibly conflict with reserved words + property.simpleName.replaceFirstChar { it.titlecaseChar() } + + val builder = + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC) + .returns(propertyType.toJavaPoetName()) + .addStatement("return \$N", propertyName) + if (isOverridden) { + builder.addAnnotation(Override::class.java) + } + + val docComment = property.docComment + val hasJavadoc = docComment != null && codegenOptions.generateJavadoc + if (hasJavadoc) { + builder.addJavadoc(renderAsJavadoc(docComment!!)) + } + + generateDeprecation( + property.annotations, + hasJavadoc, + { builder.addAnnotation(it) }, + { builder.addJavadoc(it) } + ) + + return builder.build() + } + + fun generateWithMethod(propertyName: String, property: PClass.Property): MethodSpec { + val methodName = "with" + property.simpleName.replaceFirstChar { it.titlecaseChar() } + + val methodBuilder = + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC) + .addParameter(property.type.toJavaPoetName(), propertyName) + .returns(javaPoetClassName) + + generateDeprecation( + property.annotations, + false, + { methodBuilder.addAnnotation(it) }, + { methodBuilder.addJavadoc(it) } + ) + + val codeBuilder = CodeBlock.builder() + codeBuilder.add("return new \$T(", javaPoetClassName) + var firstProperty = true + for (name in superProperties.keys) { + if (name in properties) continue + if (firstProperty) { + firstProperty = false + codeBuilder.add("\$N", name) + } else { + codeBuilder.add(", \$N", name) + } + } + for (name in properties.keys) { + if (firstProperty) { + firstProperty = false + codeBuilder.add("\$N", name) + } else { + codeBuilder.add(", \$N", name) + } + } + codeBuilder.add(");\n") + + methodBuilder.addCode(codeBuilder.build()) + return methodBuilder.build() + } + + fun generateSpringBootAnnotations(builder: TypeSpec.Builder) { + builder.addAnnotation( + ClassName.get("org.springframework.boot.context.properties", "ConstructorBinding") + ) + + if (isModuleClass) { + builder.addAnnotation( + ClassName.get("org.springframework.boot.context.properties", "ConfigurationProperties") + ) + } else { + // not very efficient to repeat computing module property base types for every class + val modulePropertiesWithMatchingType = + schema.moduleClass.allProperties.values.filter { property -> + var propertyType = property.type + while (propertyType is PType.Constrained || propertyType is PType.Nullable) { + if (propertyType is PType.Constrained) { + propertyType = propertyType.baseType + } else if (propertyType is PType.Nullable) { + propertyType = propertyType.baseType + } + } + propertyType is PType.Class && propertyType.pClass == pClass + } + if (modulePropertiesWithMatchingType.size == 1) { + // exactly one module property has this type -> make it available for direct injection + // (potential improvement: make type available for direct injection if it occurs exactly + // once in property tree) + builder.addAnnotation( + AnnotationSpec.builder( + ClassName.get( + "org.springframework.boot.context.properties", + "ConfigurationProperties" + ) + ) + // use "value" instead of "prefix" to entice JavaPoet to generate a single-line + // annotation + // that can easily be filtered out by JavaCodeGeneratorTest.`spring boot config` + .addMember("value", "\$S", modulePropertiesWithMatchingType.first().simpleName) + .build() + ) + } + } + } + + @Suppress("DuplicatedCode") + fun generateClass(): TypeSpec.Builder { + val builder = + TypeSpec.classBuilder(javaPoetClassName.simpleName()).addModifiers(Modifier.PUBLIC) + + if (codegenOptions.implementSerializable && !isModuleClass) { + builder.addSuperinterface(java.io.Serializable::class.java) + builder.addField(generateSerialVersionUIDField()) + } + + val docComment = pClass.docComment + val hasJavadoc = docComment != null && codegenOptions.generateJavadoc + if (hasJavadoc) { + builder.addJavadoc(renderAsJavadoc(docComment!!)) + } + + generateDeprecation( + pClass.annotations, + hasJavadoc, + { builder.addAnnotation(it) }, + { builder.addJavadoc(it) } + ) + + if (!isModuleClass) { + builder.addModifiers(Modifier.STATIC) + } + + if (pClass.isAbstract) { + builder.addModifiers(Modifier.ABSTRACT) + } else if (!pClass.isOpen) { + builder.addModifiers(Modifier.FINAL) + } + + if (codegenOptions.generateSpringBootConfig) { + generateSpringBootAnnotations(builder) + } + + builder.addMethod(generateConstructor()) + + superclass?.let { builder.superclass(it.toJavaPoetName()) } + + // generate fields, plus getter methods and either setters or `with` methods in alternating + // order + // `with` methods also need to be generated for superclass properties so that return type is + // self type + for ((name, property) in allProperties) { + if (name in properties) { + builder.addField(generateField(name, property)) + if (codegenOptions.generateGetters) { + val isOverridden = name in superProperties + builder.addMethod(generateGetter(name, property, isOverridden)) + } + } + if (!pClass.isAbstract) { + builder.addMethod(generateWithMethod(name, property)) + } + } + + if (properties.isNotEmpty()) { + builder + .addMethod(generateEqualsMethod()) + .addMethod(generateHashCodeMethod()) + .addMethod(generateToStringMethod()) + } + + return builder + } + + return generateClass() + } + + private fun generateSerialVersionUIDField(): FieldSpec { + return FieldSpec.builder(Long::class.java, "serialVersionUID", Modifier.PRIVATE) + .addModifiers(Modifier.STATIC, Modifier.FINAL) + .initializer("0L") + .build() + } + + private fun generateEnumTypeSpec( + typeAlias: TypeAlias, + stringLiterals: Set + ): TypeSpec.Builder { + val enumConstantToPklNames = + stringLiterals + .groupingBy { literal -> + CodeGeneratorUtils.toEnumConstantName(literal) + ?: throw JavaCodeGeneratorException( + "Cannot generate Java enum class for Pkl type alias `${typeAlias.displayName}` " + + "because string literal type \"$literal\" cannot be converted to a valid enum constant name." + ) + } + .reduce { enumConstantName, firstLiteral, secondLiteral -> + throw JavaCodeGeneratorException( + "Cannot generate Java enum class for Pkl type alias `${typeAlias.displayName}` " + + "because string literal types \"$firstLiteral\" and \"$secondLiteral\" " + + "would both be converted to enum constant name `$enumConstantName`." + ) + } + + val builder = + TypeSpec.enumBuilder(typeAlias.simpleName) + .addModifiers(Modifier.PUBLIC) + .addField(String::class.java, "value", Modifier.PRIVATE) + .addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(String::class.java, "value") + .addStatement("this.value = value") + .build() + ) + .addMethod( + MethodSpec.methodBuilder("toString") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override::class.java) + .returns(String::class.java) + .addStatement("return this.value") + .build() + ) + + for ((enumConstantName, pklName) in enumConstantToPklNames) { + builder.addEnumConstant( + enumConstantName, + TypeSpec.anonymousClassBuilder("\$S", pklName).build() + ) + } + + return builder + } + + private val namedAnnotationName = + if (codegenOptions.paramsAnnotation != null) { + toClassName(codegenOptions.paramsAnnotation) + } else { + ClassName.get("org.pkl.config.java.mapper", "Named") + } + + private fun appendPropertyMethod() = + MethodSpec.methodBuilder("appendProperty") + .addModifiers(Modifier.STATIC) + .addParameter(StringBuilder::class.java, "builder") + .addParameter(String::class.java, "name") + .addParameter(Object::class.java, "value") + .addStatement("builder.append(\"\\n \").append(name).append(\" = \")") + .addStatement( + "\$T lines = \$T.toString(value).split(\"\\n\")", + ArrayTypeName.of(String::class.java), + Objects::class.java + ) + .addStatement("builder.append(lines[0])") + .beginControlFlow("for (int i = 1; i < lines.length; i++)") + .addStatement("builder.append(\"\\n \").append(lines[i])") + .endControlFlow() + + private fun PClass.toJavaPoetName(): ClassName { + val index = moduleName.lastIndexOf(".") + val packageName = if (index == -1) "" else moduleName.substring(0, index) + val moduleClassName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() } + return if (isModuleClass) { + ClassName.get(packageName, moduleClassName) + } else { + ClassName.get(packageName, moduleClassName, simpleName) + } + } + + // generated type is a nested enum class + private fun TypeAlias.toJavaPoetName(): ClassName { + val index = moduleName.lastIndexOf(".") + val packageName = if (index == -1) "" else moduleName.substring(0, index) + val moduleClassName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() } + return ClassName.get(packageName, moduleClassName, simpleName) + } + + /** Generate `List` if `Foo` is `abstract` or `open`, to allow subclassing. */ + private fun PType.toJavaPoetTypeArgumentName(): TypeName { + val baseName = toJavaPoetName(nullable = false, boxed = true) + return if (this is PType.Class && (pClass.isAbstract || pClass.isOpen)) { + WildcardTypeName.subtypeOf(baseName) + } else { + baseName + } + } + + private fun PType.toJavaPoetName(nullable: Boolean = false, boxed: Boolean = false): TypeName = + when (this) { + PType.UNKNOWN -> TypeName.OBJECT.nullableIf(nullable) + PType.NOTHING -> TypeName.VOID + is PType.StringLiteral -> STRING.nullableIf(nullable) + is PType.Class -> { + // if in doubt, spell it out + when (val classInfo = pClass.info) { + PClassInfo.Any -> TypeName.OBJECT + PClassInfo.Typed, + PClassInfo.Dynamic -> TypeName.OBJECT.nullableIf(nullable) + PClassInfo.Boolean -> TypeName.BOOLEAN.boxIf(boxed).nullableIf(nullable) + PClassInfo.String -> STRING.nullableIf(nullable) + // seems more useful to generate `double` than `java.lang.Number` + PClassInfo.Number -> TypeName.DOUBLE.boxIf(boxed).nullableIf(nullable) + PClassInfo.Int -> TypeName.LONG.boxIf(boxed).nullableIf(nullable) + PClassInfo.Float -> TypeName.DOUBLE.boxIf(boxed).nullableIf(nullable) + PClassInfo.Duration -> DURATION.nullableIf(nullable) + PClassInfo.DataSize -> DATA_SIZE.nullableIf(nullable) + PClassInfo.Pair -> + ParameterizedTypeName.get( + PAIR, + if (typeArguments.isEmpty()) { + TypeName.OBJECT + } else { + typeArguments[0].toJavaPoetTypeArgumentName() + }, + if (typeArguments.isEmpty()) { + TypeName.OBJECT + } else { + typeArguments[1].toJavaPoetTypeArgumentName() + } + ) + .nullableIf(nullable) + PClassInfo.Collection -> + ParameterizedTypeName.get( + COLLECTION, + if (typeArguments.isEmpty()) { + TypeName.OBJECT + } else { + typeArguments[0].toJavaPoetTypeArgumentName() + } + ) + .nullableIf(nullable) + PClassInfo.List, + PClassInfo.Listing -> { + ParameterizedTypeName.get( + LIST, + if (typeArguments.isEmpty()) { + TypeName.OBJECT + } else { + typeArguments[0].toJavaPoetTypeArgumentName() + } + ) + .nullableIf(nullable) + } + PClassInfo.Set -> + ParameterizedTypeName.get( + SET, + if (typeArguments.isEmpty()) { + TypeName.OBJECT + } else { + typeArguments[0].toJavaPoetTypeArgumentName() + } + ) + .nullableIf(nullable) + PClassInfo.Map, + PClassInfo.Mapping -> + ParameterizedTypeName.get( + MAP, + if (typeArguments.isEmpty()) { + TypeName.OBJECT + } else { + typeArguments[0].toJavaPoetTypeArgumentName() + }, + if (typeArguments.isEmpty()) { + TypeName.OBJECT + } else { + typeArguments[1].toJavaPoetTypeArgumentName() + } + ) + .nullableIf(nullable) + PClassInfo.Module -> PMODULE.nullableIf(nullable) + PClassInfo.Class -> PCLASS.nullableIf(nullable) + PClassInfo.Regex -> PATTERN.nullableIf(nullable) + PClassInfo.Version -> VERSION.nullableIf(nullable) + else -> + when { + !classInfo.isStandardLibraryClass -> pClass.toJavaPoetName().nullableIf(nullable) + else -> + throw JavaCodeGeneratorException( + "Standard library class `${pClass.qualifiedName}` is not supported by Java code generator. " + + "If you think this is an omission, please let us know." + ) + } + } + } + is PType.Nullable -> baseType.toJavaPoetName(nullable = true, boxed = true) + is PType.Constrained -> baseType.toJavaPoetName(nullable = nullable, boxed = boxed) + is PType.Alias -> + when (typeAlias.qualifiedName) { + "pkl.base#NonNull" -> TypeName.OBJECT.nullableIf(nullable) + "pkl.base#Int8" -> TypeName.BYTE.boxIf(boxed).nullableIf(nullable) + "pkl.base#Int16", + "pkl.base#UInt8" -> TypeName.SHORT.boxIf(boxed).nullableIf(nullable) + "pkl.base#Int32", + "pkl.base#UInt16" -> TypeName.INT.boxIf(boxed).nullableIf(nullable) + "pkl.base#UInt", + "pkl.base#UInt32" -> TypeName.LONG.boxIf(boxed).nullableIf(nullable) + "pkl.base#DurationUnit" -> DURATION_UNIT.nullableIf(nullable) + "pkl.base#DataSizeUnit" -> DATASIZE_UNIT.nullableIf(nullable) + "pkl.base#Uri" -> URI.nullableIf(nullable) + else -> { + if (CodeGeneratorUtils.isRepresentableAsEnum(aliasedType, null)) { + if (typeAlias.isStandardLibraryMember) { + throw JavaCodeGeneratorException( + "Standard library typealias `${typeAlias.qualifiedName}` is not supported by Java code generator. " + + "If you think this is an omission, please let us know." + ) + } else { + // reference generated enum class + typeAlias.toJavaPoetName().nullableIf(nullable) + } + } else { + // inline type alias + aliasedType.toJavaPoetName(nullable) + } + } + } + is PType.Function -> + throw JavaCodeGeneratorException( + "Pkl function types are not supported by the Java code generator." + ) + is PType.Union -> + if (CodeGeneratorUtils.isRepresentableAsString(this)) STRING.nullableIf(nullable) + else + throw JavaCodeGeneratorException( + "Pkl union types are not supported by the Java code generator." + ) + else -> + // should never encounter PType.TypeVariableNode because it can only occur in stdlib classes + throw AssertionError("Encountered unexpected PType subclass: $this") + } + + private fun TypeName.nullableIf(isNullable: Boolean): TypeName = + if (isPrimitive && isNullable) box() + else if (isPrimitive || isNullable) this else annotated(nonNullAnnotation) + + private fun TypeName.boxIf(shouldBox: Boolean): TypeName = if (shouldBox) box() else this + + private fun renameIfReservedWord(map: Map): Map { + return map.mapKeys { (key, _) -> + if (key in javaReservedWords) { + generateSequence("_$key") { "_$it" }.first { it !in map.keys } + } else key + } + } +} + +internal val javaReservedWords = + setOf( + "_", // java 9+ + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "double", + "do", + "else", + "enum", + "extends", + "false", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "null", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "true", + "try", + "void", + "volatile", + "while" + ) diff --git a/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/Main.kt b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/Main.kt new file mode 100644 index 00000000..11b4441d --- /dev/null +++ b/pkl-codegen-java/src/main/kotlin/org/pkl/codegen/java/Main.kt @@ -0,0 +1,126 @@ +/** + * 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. + */ +@file:JvmName("Main") + +package org.pkl.codegen.java + +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.path +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.cliMain +import org.pkl.commons.cli.commands.ModulesCommand +import org.pkl.commons.toPath +import org.pkl.core.Release + +/** Main method for the Java code generator CLI. */ +internal fun main(args: Array) { + cliMain { PklJavaCodegenCommand().main(args) } +} + +class PklJavaCodegenCommand : + ModulesCommand( + name = "pkl-codegen-java", + helpLink = Release.current().documentation().homepage(), + ) { + + private val defaults = CliJavaCodeGeneratorOptions(CliBaseOptions(), "".toPath()) + + private val outputDir: Path by + option( + names = arrayOf("-o", "--output-dir"), + metavar = "", + help = "The directory where generated source code is placed." + ) + .path() + .default(defaults.outputDir) + + private val indent: String by + option( + names = arrayOf("--indent"), + metavar = "", + help = "The characters to use for indenting generated source code." + ) + .default(defaults.indent) + + private val generateGetters: Boolean by + option( + names = arrayOf("--generate-getters"), + help = + "Whether to generate public getter methods and " + + "private final fields instead of public final fields." + ) + .flag() + + private val generateJavadoc: Boolean by + option( + names = arrayOf("--generate-javadoc"), + help = + "Whether to generate Javadoc based on doc comments " + + "for Pkl modules, classes, and properties." + ) + .flag() + + private val generateSpringboot: Boolean by + option( + names = arrayOf("--generate-spring-boot"), + help = "Whether to generate config classes for use with Spring boot." + ) + .flag() + + private val paramsAnnotation: String? by + option( + names = arrayOf("--params-annotation"), + help = "Fully qualified name of the annotation to use on constructor parameters." + ) + + private val nonNullAnnotation: String? by + option( + names = arrayOf("--non-null-annotation"), + help = + """ + Fully qualified named of the annotation class to use for non-null types. + This annotation is required to have `java.lang.annotation.ElementType.TYPE_USE` as a `@Target` + or it may generate code that does not compile. + """ + .trimIndent() + ) + + private val implementSerializable: Boolean by + option( + names = arrayOf("--implement-serializable"), + help = "Whether to make generated classes implement java.io.Serializable." + ) + .flag() + + override fun run() { + val options = + CliJavaCodeGeneratorOptions( + base = baseOptions.baseOptions(modules, projectOptions), + outputDir = outputDir, + indent = indent, + generateGetters = generateGetters, + generateJavadoc = generateJavadoc, + generateSpringBootConfig = generateSpringboot, + paramsAnnotation = paramsAnnotation, + nonNullAnnotation = nonNullAnnotation, + implementSerializable = implementSerializable + ) + CliJavaCodeGenerator(options).run() + } +} diff --git a/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/CliJavaCodeGeneratorTest.kt b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/CliJavaCodeGeneratorTest.kt new file mode 100644 index 00000000..2c949482 --- /dev/null +++ b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/CliJavaCodeGeneratorTest.kt @@ -0,0 +1,184 @@ +/** + * 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.codegen.java + +import java.nio.file.Path +import kotlin.io.path.listDirectoryEntries +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.readString + +class CliJavaCodeGeneratorTest { + + private val dollar = "$" + + @Test + fun `module inheritance`(@TempDir tempDir: Path) { + val module1 = + PklModule( + "org.mod1", + """ + open module org.mod1 + + pigeon: Person + + class Person { + name: String + age: Int + } + """ + ) + + val module2 = + PklModule( + "org.mod2", + """ + module org.mod2 + + extends "mod1.pkl" + + parrot: Person + """ + ) + + val module1File = module1.writeToDisk(tempDir.resolve("org/mod1.pkl")) + val module2File = module2.writeToDisk(tempDir.resolve("org/mod2.pkl")) + val outputDir = tempDir.resolve("output") + + val generator = + CliJavaCodeGenerator( + CliJavaCodeGeneratorOptions( + CliBaseOptions(listOf(module1File.toUri(), module2File.toUri())), + outputDir + ) + ) + + generator.run() + + val javaDir = outputDir.resolve("java") + + val moduleJavaFiles = javaDir.resolve("org").listDirectoryEntries() + assertThat(moduleJavaFiles.map { it.fileName.toString() }) + .containsExactlyInAnyOrder("Mod1.java", "Mod2.java") + + val module1JavaFile = javaDir.resolve("org/Mod1.java") + assertContains( + """ + |public class Mod1 { + | public final @NonNull Person pigeon; + """, + module1JavaFile.readString() + ) + + val module2JavaFile = javaDir.resolve("org/Mod2.java") + assertContains( + """ + |public final class Mod2 extends Mod1 { + | public final Mod1. @NonNull Person parrot; + """, + module2JavaFile.readString() + ) + val resourcesDir = outputDir.resolve("resources/META-INF/org/pkl/config/java/mapper/classes/") + + val module1PropertiesFile = resourcesDir.resolve("org.mod1.properties") + + assertContains( + """ + org.pkl.config.java.mapper.org.mod1\#Person=org.Mod1${dollar}Person + org.pkl.config.java.mapper.org.mod1\#ModuleClass=org.Mod1 + """ + .trimIndent(), + module1PropertiesFile.readString() + ) + + val module2PropertiesFile = resourcesDir.resolve("org.mod2.properties") + + assertContains( + """ + org.pkl.config.java.mapper.org.mod2\#ModuleClass=org.Mod2 + """ + .trimIndent(), + module2PropertiesFile.readString() + ) + } + + @Test + fun `class name clashes`(@TempDir tempDir: Path) { + val module1 = + PklModule( + "org.mod1", + """ + module org.mod1 + + class Person { + name: String + } + """ + ) + + val module2 = + PklModule( + "org.mod2", + """ + module org.mod2 + + import "mod1.pkl" + + person1: mod1.Person + person2: Person + + class Person { + age: Int + } + """ + ) + + val module1PklFile = module1.writeToDisk(tempDir.resolve("org/mod1.pkl")) + val module2PklFile = module2.writeToDisk(tempDir.resolve("org/mod2.pkl")) + val outputDir = tempDir.resolve("output") + + val generator = + CliJavaCodeGenerator( + CliJavaCodeGeneratorOptions( + CliBaseOptions(listOf(module1PklFile.toUri(), module2PklFile.toUri())), + outputDir + ) + ) + + generator.run() + + val module2JavaFile = outputDir.resolve("java/org/Mod2.java") + assertContains( + """ + |public final class Mod2 { + | public final Mod1. @NonNull Person person1; + | + | public final @NonNull Person person2; + """, + module2JavaFile.readString() + ) + } + + private fun assertContains(part: String, code: String) { + val trimmedPart = part.trim().trimMargin() + if (!code.contains(trimmedPart)) { + // check for equality to get better error output (ide diff dialog) + assertThat(code).isEqualTo(trimmedPart) + } + } +} diff --git a/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/InMemoryJavaCompiler.kt b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/InMemoryJavaCompiler.kt new file mode 100644 index 00000000..25c019c9 --- /dev/null +++ b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/InMemoryJavaCompiler.kt @@ -0,0 +1,93 @@ +/** + * 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.codegen.java + +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.net.URI +import javax.tools.* + +class CompilationFailedException(msg: String) : RuntimeException(msg) + +object InMemoryJavaCompiler { + fun compile(sourceFiles: Map): Map> { + val compiler = ToolProvider.getSystemJavaCompiler() + val diagnosticsCollector = DiagnosticCollector() + val fileManager = + InMemoryFileManager(compiler.getStandardFileManager(diagnosticsCollector, null, null)) + val sourceObjects = + sourceFiles.map { (filename, contents) -> ReadableSourceFileObject(filename, contents) } + val task = compiler.getTask(null, fileManager, diagnosticsCollector, null, null, sourceObjects) + val result = task.call() + if (!result) { + throw CompilationFailedException( + buildString { + appendLine("Compilation failed. Error(s):") + for (diagnostic in diagnosticsCollector.diagnostics) { + appendLine(diagnostic.getMessage(null)) + } + } + ) + } + val loader = ClassFileObjectLoader(fileManager.outputFiles) + return fileManager.outputFiles.mapValues { loader.loadClass(it.key) } + } +} + +private class ClassFileObjectLoader(val fileObjects: Map) : + ClassLoader(ClassFileObjectLoader::class.java.classLoader) { + + override fun findClass(name: String): Class<*> { + val obj = fileObjects[name] + if (obj == null || obj.kind != JavaFileObject.Kind.CLASS) { + throw ClassNotFoundException(name) + } + val array = obj.out.toByteArray() + return defineClass(name, array, 0, array.size) + } +} + +private class ReadableSourceFileObject(path: String, private val contents: String) : + SimpleJavaFileObject(URI(path), JavaFileObject.Kind.SOURCE) { + + override fun openInputStream(): InputStream = contents.byteInputStream() + + override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence = contents +} + +private class WritableBinaryFileObject(className: String, kind: JavaFileObject.Kind) : + SimpleJavaFileObject(URI("/${className.replace(".", "/")}.${kind.extension}"), kind) { + val out = ByteArrayOutputStream() + + override fun openOutputStream(): OutputStream = out +} + +private class InMemoryFileManager(delegate: JavaFileManager) : + ForwardingJavaFileManager(delegate) { + + val outputFiles = mutableMapOf() + + override fun getJavaFileForOutput( + location: JavaFileManager.Location, + className: String, + kind: JavaFileObject.Kind, + sibling: FileObject + ): JavaFileObject { + + return WritableBinaryFileObject(className, kind).also { outputFiles[className] = it } + } +} diff --git a/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/JavaCodeGeneratorTest.kt b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/JavaCodeGeneratorTest.kt new file mode 100644 index 00000000..42bec7b6 --- /dev/null +++ b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/JavaCodeGeneratorTest.kt @@ -0,0 +1,1900 @@ +/** + * 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.codegen.java + +import java.io.* +import java.nio.file.Path +import java.util.function.Consumer +import java.util.regex.Pattern +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.pkl.core.* +import org.pkl.core.ModuleSource.path +import org.pkl.core.ModuleSource.text +import org.pkl.core.util.IoUtils + +class JavaCodeGeneratorTest { + companion object { + private val simpleClass by lazy { + compileJavaCode( + generateJavaCode( + """ + module my.mod + + class Simple { + str: String + list: List + } + """ + ) + ) + .getValue("my.Mod\$Simple") + } + + private val propertyTypesSources by lazy { + generateJavaCode( + """ + module my.mod + + class PropertyTypes { + boolean: Boolean + int: Int + float: Float + string: String + duration: Duration + durationUnit: DurationUnit + dataSize: DataSize + dataSizeUnit: DataSizeUnit + nullable: String? + nullable2: String? + pair: Pair + pair2: Pair + coll: Collection + coll2: Collection + list: List + list2: List + set: Set + set2: Set + map: Map + map2: Map + container: Mapping + container2: Mapping + other: Other + regex: Regex + any: Any + nonNull: NonNull + enum: Direction + } + + class Other { + name: String + } + + typealias Direction = "north"|"east"|"south"|"west" + """ + ) + } + + private val propertyTypesClasses by lazy { compileJavaCode(propertyTypesSources) } + + private fun generateJavaCode( + pklCode: String, + generateGetters: Boolean = false, + generateJavadoc: Boolean = false, + generateSpringBootConfig: Boolean = false, + nonNullAnnotation: String? = null, + implementSerializable: Boolean = false + ): String { + val module = Evaluator.preconfigured().evaluateSchema(text(pklCode)) + val generator = + JavaCodeGenerator( + module, + JavaCodegenOptions( + generateGetters = generateGetters, + generateJavadoc = generateJavadoc, + generateSpringBootConfig = generateSpringBootConfig, + nonNullAnnotation = nonNullAnnotation, + implementSerializable = implementSerializable, + ) + ) + return generator.javaFile + } + + private fun compileJavaCode(javaCode: String): Map> { + return InMemoryJavaCompiler.compile(mapOf("/org/Mod.java" to javaCode)) + } + + private fun assertCompilesSuccessfully(sourceText: String) { + compileJavaCode(sourceText) + } + } + + @Test + fun testEquals() { + val ctor = simpleClass.constructors.first() + val instance1 = ctor.newInstance("foo", listOf(1, 2, 3)) + val instance2 = ctor.newInstance("foo", listOf(1, 2, 3)) + val instance3 = ctor.newInstance("foo", listOf(1, 3, 2)) + val instance4 = ctor.newInstance("bar", listOf(1, 2, 3)) + + assertThat(instance1).isEqualTo(instance1) + assertThat(instance1).isEqualTo(instance2) + assertThat(instance2).isEqualTo(instance1) + + assertThat(instance3).isNotEqualTo(instance1) + assertThat(instance4).isNotEqualTo(instance1) + } + + @Test + fun testHashCode() { + val ctor = simpleClass.constructors.first() + val instance1 = ctor.newInstance("foo", listOf(1, 2, 3)) + val instance2 = ctor.newInstance("foo", listOf(1, 2, 3)) + val instance3 = ctor.newInstance("foo", listOf(1, 3, 2)) + val instance4 = ctor.newInstance("bar", listOf(1, 2, 3)) + + assertThat(instance1.hashCode()).isEqualTo(instance1.hashCode()) + assertThat(instance1.hashCode()).isEqualTo(instance2.hashCode()) + + assertThat(instance3.hashCode()).isNotEqualTo(instance1.hashCode()) + assertThat(instance4.hashCode()).isNotEqualTo(instance1.hashCode()) + } + + @Test + fun testToString() { + val (_, propertyTypes) = instantiateOtherAndPropertyTypes() + + assertEqualTo( + """ + PropertyTypes { + _boolean = true + _int = 42 + _float = 42.3 + string = string + duration = 5.min + durationUnit = min + dataSize = 3.gb + dataSizeUnit = gb + nullable = idea + nullable2 = null + pair = Pair(1, 2) + pair2 = Pair(pigeon, Other { + name = pigeon + }) + coll = [1, 2, 3] + coll2 = [Other { + name = pigeon + }, Other { + name = pigeon + }] + list = [1, 2, 3] + list2 = [Other { + name = pigeon + }, Other { + name = pigeon + }] + set = [1, 2, 3] + set2 = [Other { + name = pigeon + }] + map = {1=one, 2=two} + map2 = {one=Other { + name = pigeon + }, two=Other { + name = pigeon + }} + container = {1=one, 2=two} + container2 = {one=Other { + name = pigeon + }, two=Other { + name = pigeon + }} + other = Other { + name = pigeon + } + regex = (i?)\w* + any = Other { + name = pigeon + } + nonNull = Other { + name = pigeon + } + _enum = north + } + """, + propertyTypes.toString() + ) + } + + @Test + fun `deprecated property with message`() { + val javaCode = + generateJavaCode( + """ + class ClassWithDeprecatedProperty { + @Deprecated { message = "property deprecation message" } + deprecatedProperty: Int = 1337 + } + """ + .trimIndent(), + generateJavadoc = true + ) + val expectedPropertyDef = + """ + | public static final class ClassWithDeprecatedProperty { + | /** + | * @deprecated property deprecation message + | */ + | @Deprecated + | public final long deprecatedProperty; + """ + .trimMargin() + val expectedWithMethodDef = + """ + | /** + | * @deprecated property deprecation message + | */ + | @Deprecated + | public ClassWithDeprecatedProperty withDeprecatedProperty(long deprecatedProperty) { + | return new ClassWithDeprecatedProperty(deprecatedProperty); + | } + """ + .trimMargin() + assertThat(javaCode).contains(expectedPropertyDef).contains(expectedWithMethodDef) + } + + @Test + fun `deprecated property's getter with message`() { + val javaCode = + generateJavaCode( + """ + class ClassWithDeprecatedProperty { + @Deprecated { message = "property deprecation message" } + deprecatedProperty: Int = 1337 + } + """ + .trimIndent(), + generateGetters = true, + generateJavadoc = true + ) + val expectedPropertyDef = + """ + | public static final class ClassWithDeprecatedProperty { + | private final long deprecatedProperty; + """ + .trimMargin() + val expectedGetterDef = + """ + | /** + | * @deprecated property deprecation message + | */ + | @Deprecated + | public long getDeprecatedProperty() { + | return deprecatedProperty; + | } + """ + .trimMargin() + val expectedWithMethodDef = + """ + | /** + | * @deprecated property deprecation message + | */ + | @Deprecated + | public ClassWithDeprecatedProperty withDeprecatedProperty(long deprecatedProperty) { + | return new ClassWithDeprecatedProperty(deprecatedProperty); + | } + """ + .trimMargin() + assertThat(javaCode) + .contains(expectedPropertyDef) + .contains(expectedGetterDef) + .contains(expectedWithMethodDef) + } + + @Test + fun `deprecated class with message`() { + val javaCode = + generateJavaCode( + """ + @Deprecated { message = "class deprecation message" } + class DeprecatedClass { + propertyOfDeprecatedClass: Int = 42 + } + """ + .trimIndent(), + generateJavadoc = true + ) + val expected = + """ + | /** + | * @deprecated class deprecation message + | */ + | @Deprecated + | public static final class DeprecatedClass { + """ + .trimMargin() + assertThat(javaCode).contains(expected) + } + + @Test + fun `deprecated module class with message`() { + for (generateJavadoc in listOf(false, true)) { + val javaCode = + generateJavaCode( + """ + @Deprecated{ message = "module class deprecation message" } + module DeprecatedModule + + propertyInDeprecatedModuleClass : Int = 42 + """ + .trimIndent(), + generateJavadoc = generateJavadoc + ) + val expectedJavadoc = + if (!generateJavadoc) "" + else + """ + |/** + | * @deprecated module class deprecation message + | */ + """ + .trimMargin() + val expected = + """ + |@Deprecated + |public final class DeprecatedModule { + """ + .trimMargin() + assertThat(javaCode).contains("$expectedJavadoc\n$expected") + } + } + + @Test + fun `deprecated property`() { + for (generateDoc in listOf(false, true)) { + val javaCode = + generateJavaCode( + """ + class ClassWithDeprecatedProperty { + @Deprecated + deprecatedProperty: Int = 1337 + } + """ + .trimIndent(), + generateJavadoc = generateDoc + ) // no message, so no Javadoc, regardless of flag + val expectedPropertyDef = + """ + | public static final class ClassWithDeprecatedProperty { + | @Deprecated + | public final long deprecatedProperty; + """ + .trimMargin() + val expectedWithMethodDef = + """ + | @Deprecated + | public ClassWithDeprecatedProperty withDeprecatedProperty(long deprecatedProperty) { + | return new ClassWithDeprecatedProperty(deprecatedProperty); + | } + """ + .trimMargin() + assertThat(javaCode) + .contains(expectedPropertyDef) + .contains(expectedWithMethodDef) + .doesNotContain("* @deprecated") + } + } + + @Test + fun `deprecated property's getter`() { + val javaCode = + generateJavaCode( + """ + class ClassWithDeprecatedProperty { + @Deprecated + deprecatedProperty: Int = 1337 + } + """ + .trimIndent(), + true + ) + val expectedPropertyDef = + """ + | public static final class ClassWithDeprecatedProperty { + | private final long deprecatedProperty; + """ + .trimMargin() + val expectedGetterDef = + """ + | @Deprecated + | public long getDeprecatedProperty() { + | return deprecatedProperty; + | } + """ + .trimMargin() + val expectedWithMethodDef = + """ + | @Deprecated + | public ClassWithDeprecatedProperty withDeprecatedProperty(long deprecatedProperty) { + | return new ClassWithDeprecatedProperty(deprecatedProperty); + | } + """ + .trimMargin() + assertThat(javaCode) + .contains(expectedPropertyDef) + .contains(expectedGetterDef) + .contains(expectedWithMethodDef) + .doesNotContain("* @deprecated") + } + + @Test + fun `deprecated class`() { + val javaCode = + generateJavaCode( + """ + @Deprecated + class DeprecatedClass { + propertyOfDeprecatedClass: Int = 42 + } + """ + .trimIndent() + ) + val expected = + """ + | @Deprecated + | public static final class DeprecatedClass { + """ + .trimMargin() + assertThat(javaCode).contains(expected).doesNotContain("* @deprecated") + } + + @Test + fun `deprecated module class`() { + val javaCode = + generateJavaCode( + """ + @Deprecated + module DeprecatedModule + + propertyInDeprecatedModuleClass : Int = 42 + """ + .trimIndent() + ) + val expected = + """ + |@Deprecated + |public final class DeprecatedModule { + """ + .trimMargin() + assertThat(javaCode).contains(expected).doesNotContain("* @deprecated") + } + + @Test + fun `deprecation with message and doc comment on the same property`() { + val javaCode = + generateJavaCode( + """ + /// Documenting deprecatedProperty + @Deprecated { message = "property is deprecated" } + deprecatedProperty: Int + """ + .trimIndent(), + generateJavadoc = true + ) + val expected = + """ + | /** + | * Documenting deprecatedProperty + | * + | * @deprecated property is deprecated + | */ + | @Deprecated + | public final long deprecatedProperty; + """ + .trimMargin() + assertThat(javaCode).contains(expected) + } + + @Test + fun properties() { + val (other, propertyTypes) = instantiateOtherAndPropertyTypes() + + assertThat(readProperty(other, "name")).isEqualTo("pigeon") + assertThat(readProperty(propertyTypes, "_boolean")).isEqualTo(true) + assertThat(readProperty(propertyTypes, "_int")).isEqualTo(42L) + assertThat(readProperty(propertyTypes, "_float")).isEqualTo(42.3) + assertThat(readProperty(propertyTypes, "string")).isEqualTo("string") + assertThat(readProperty(propertyTypes, "duration")) + .isEqualTo(Duration(5.0, DurationUnit.MINUTES)) + assertThat(readProperty(propertyTypes, "dataSize")) + .isEqualTo(DataSize(3.0, DataSizeUnit.GIGABYTES)) + assertThat(readProperty(propertyTypes, "nullable")).isEqualTo("idea") + assertThat(readProperty(propertyTypes, "nullable2")).isEqualTo(null as String?) + assertThat(readProperty(propertyTypes, "list")).isEqualTo(listOf(1, 2, 3)) + assertThat(readProperty(propertyTypes, "list2")).isEqualTo(listOf(other, other)) + assertThat(readProperty(propertyTypes, "set")).isEqualTo(setOf(1, 2, 3)) + assertThat(readProperty(propertyTypes, "set2")).isEqualTo(setOf(other)) + assertThat(readProperty(propertyTypes, "map")).isEqualTo(mapOf(1 to "one", 2 to "two")) + assertThat(readProperty(propertyTypes, "map2")).isEqualTo(mapOf("one" to other, "two" to other)) + assertThat(readProperty(propertyTypes, "container")).isEqualTo(mapOf(1 to "one", 2 to "two")) + assertThat(readProperty(propertyTypes, "container2")) + .isEqualTo(mapOf("one" to other, "two" to other)) + assertThat(readProperty(propertyTypes, "other")).isEqualTo(other) + assertThat(readProperty(propertyTypes, "regex")).isInstanceOf(Pattern::class.java) + assertThat(readProperty(propertyTypes, "any")).isEqualTo(other) + assertThat(readProperty(propertyTypes, "nonNull")).isEqualTo(other) + } + + private fun readProperty(obj: Any, property: String): Any? = + obj::class.java.getField(property).get(obj) + + @Test + fun `properties 2`() { + assertEqualTo( + IoUtils.readClassPathResourceAsString(javaClass, "PropertyTypes.jva"), + propertyTypesSources + ) + } + + @Test + fun `enum constant names`() { + val cases = + listOf( + "camelCasedName" to "CAMEL_CASED_NAME", + "hyphenated-name" to "HYPHENATED_NAME", + "EnQuad\u2000EmSpace\u2003IdeographicSpace\u3000" to "EN_QUAD_EM_SPACE_IDEOGRAPHIC_SPACE_", + "ᾊᾨ" to "ᾊᾨ", + "0-digit" to "_0_DIGIT", + "digit-1" to "DIGIT_1", + "42" to "_42", + "àœü" to "ÀŒÜ", + "日本-つくば" to "日本_つくば" + ) + val javaCode = + generateJavaCode( + """ + module my.mod + typealias MyTypeAlias = ${cases.joinToString(" | ") { "\"${it.first}\"" }} + """ + .trimIndent() + ) + val javaClass = compileJavaCode(javaCode).getValue("my.Mod\$MyTypeAlias") + + assertThat(javaClass.enumConstants.size) + .isEqualTo(cases.size) // make sure zip doesn't drop cases + + assertAll( + "generated enum constants have correct names", + javaClass.declaredFields.zip(cases) { field, (_, kotlinName) -> + { + assertThat(field.name).isEqualTo(kotlinName) + Unit + } + } + ) + + assertAll( + "toString() returns Pkl name", + javaClass.enumConstants.zip(cases) { enumConstant, (pklName, _) -> + { + assertThat(enumConstant.toString()).isEqualTo(pklName) + Unit + } + } + ) + } + + @Test + fun `conflicting enum constant names`() { + val pklCode = + """ + module my.mod + typealias MyTypeAlias = "foo-bar" | "foo bar" + """ + .trimIndent() + + val exception = assertThrows { generateJavaCode(pklCode) } + assertThat(exception) + .hasMessageContainingAll("both be converted to enum constant name", "FOO_BAR") + } + + @Test + fun `empty enum constant name`() { + val pklCode = + """ + module my.mod + typealias MyTypeAlias = "foo" | "" | "bar" + """ + .trimIndent() + + val exception = assertThrows { generateJavaCode(pklCode) } + assertThat(exception).hasMessageContaining("cannot be converted") + } + + @Test + fun `inconvertible enum constant name`() { + val pklCode = + """ + module my.mod + typealias MyTypeAlias = "foo" | "✅" | "bar" + """ + .trimIndent() + + val exception = assertThrows { generateJavaCode(pklCode) } + assertThat(exception).hasMessageContainingAll("✅", "cannot be converted") + } + + @Test + fun `recursive types`() { + val javaCode = + generateJavaCode( + """ + module my.mod + + class Foo { + other: Int + bar: Bar + } + class Bar { + foo: Foo + other: String + } + """ + ) + + assertContains( + """ + | public static final class Foo { + | public final long other; + | + | public final @NonNull Bar bar; + | + | public Foo(@Named("other") long other, @Named("bar") @NonNull Bar bar) { + | this.other = other; + | this.bar = bar; + | } + """, + javaCode + ) + + assertContains( + """ + | public static final class Bar { + | public final @NonNull Foo foo; + | + | public final @NonNull String other; + | + | public Bar(@Named("foo") @NonNull Foo foo, @Named("other") @NonNull String other) { + | this.foo = foo; + | this.other = other; + | } + """, + javaCode + ) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun inheritance() { + val javaCode = + generateJavaCode( + """ + module my.mod + + abstract class Foo { + one: Int + } + open class None extends Foo {} + open class Bar extends None { + two: String? + } + class Baz extends Bar { + three: Duration + } + """, + generateGetters = true + ) + + assertContains( + """ + | public abstract static class Foo { + | protected final long one; + | + | protected Foo(@Named("one") long one) { + | this.one = one; + | } + """, + javaCode + ) + + assertContains( + """ + | public static class None extends Foo { + | public None(@Named("one") long one) { + | super(one); + | } + """, + javaCode + ) + + assertContains( + """ + | public static class Bar extends None { + | protected final String two; + | + | public Bar(@Named("one") long one, @Named("two") String two) { + | super(one); + | this.two = two; + | } + """, + javaCode + ) + + assertThat(javaCode) + .isEqualTo(IoUtils.readClassPathResourceAsString(javaClass, "Inheritance.jva")) + // + // assertEqualTo( + // IoUtils.readClassPathResourceAsString(javaClass, "Inheritance.jva"), + // javaCode + // ) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `reserved words`() { + val props = javaReservedWords.joinToString("\n") { "`$it`: Int" } + + val fooClass = + compileJavaCode( + generateJavaCode( + """ + module my.mod + + class Foo { + $props + } + """ + ) + ) + .getValue("my.Mod\$Foo") + + assertThat(fooClass.declaredFields).allSatisfy(Consumer { it.name.startsWith("_") }) + } + + @Test + fun getters() { + val javaCode = + generateJavaCode( + """ + module my.mod + + class GenerateGetters { + urgent: Boolean = true + url: String = "https://apple.com" + diskSize: DataSize = 4.mb + ETA: Duration = 3.s + package: String + } + """, + generateGetters = true + ) + + assertEqualTo(IoUtils.readClassPathResourceAsString(javaClass, "GenerateGetters.jva"), javaCode) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `'with' methods`() { + val javaCode = + generateJavaCode( + """ + module my.mod + + abstract class Foo { + x: Int + } + class Bar extends Foo { + y: String + } + """ + ) + + assertContains( + """ + | public Bar withX(long x) { + | return new Bar(x, y); + | } + """, + javaCode + ) + + assertContains( + """ + | public Bar withY(@NonNull String y) { + | return new Bar(x, y); + | } + """, + javaCode + ) + + assertThat(javaCode).doesNotContain("public Foo withX") // because `Foo` is abstract + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `module class`() { + val javaCode = + generateJavaCode(""" + module my.mod + + pigeon: String + parrot: String + """) + + assertContains( + """ + |public final class Mod { + | public final @NonNull String pigeon; + | + | public final @NonNull String parrot; + """, + javaCode + ) + } + + @Test + fun `hidden properties`() { + val javaCode = + generateJavaCode( + """ + hidden pigeon1: String + parrot1: String + + class Persons { + hidden pigeon2: String + parrot2: String + } + """ + ) + + assertThat(javaCode) + .doesNotContain("final String pigeon1") + .contains("final @NonNull String parrot1") + .doesNotContain("final String pigeon2") + .contains("final @NonNull String parrot2") + } + + @Test + fun javadoc() { + val javaCode = + generateJavaCode( + """ + /// module comment. + /// *emphasized* `code`. + module my.mod + + /// module property comment. + /// *emphasized* `code`. + pigeon: Person + + /// class comment. + /// *emphasized* `code`. + class Person { + /// class property comment. + /// *emphasized* `code`. + name: String + } + """, + generateJavadoc = true + ) + + assertEqualTo(IoUtils.readClassPathResourceAsString(javaClass, "Javadoc.jva"), javaCode) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `javadoc 2`() { + val javaCode = + generateJavaCode( + """ + module my.mod + + /// module property comment. + /// can contain /* and */ characters. + pigeon: Person + + class Person { + /// class property comment. + /// can contain /* and */ characters. + name: String + } + """, + generateGetters = true, + generateJavadoc = true + ) + + assertContains( + """ + | /** + | * module property comment. + | * can contain /* and */ characters. + | */ + | public @NonNull Person getPigeon() { + """, + javaCode + ) + + assertContains( + """ + | /** + | * class property comment. + | * can contain /* and */ characters. + | */ + | public @NonNull String getName() { + """, + javaCode + ) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `pkl_base type aliases`() { + val javaCode = + generateJavaCode( + """ + module mod + + uint8: UInt8 + uint16: UInt16 + uint32: UInt32 + uint: UInt + int8: Int8 + int16: Int16 + int32: Int32 + uri: Uri + + pair: Pair + list: List + set: Set + map: Map + listing: Listing + mapping: Mapping + nullable: UInt16? + + class Foo { + uint8: UInt8 + uint16: UInt16 + uint32: UInt32 + uint: UInt + int8: Int8 + int16: Int16 + int32: Int32 + uri: Uri + list: List + } + """ + ) + + assertContains( + """ + |public final class Mod { + | public final short uint8; + | + | public final int uint16; + | + | public final long uint32; + | + | public final long uint; + | + | public final byte int8; + | + | public final short int16; + | + | public final int int32; + | + | public final @NonNull URI uri; + """, + javaCode + ) + + assertContains( + """ + | public final @NonNull Pair<@NonNull Short, @NonNull Integer> pair; + | + | public final @NonNull List<@NonNull Long> list; + | + | public final @NonNull Set<@NonNull Long> set; + | + | public final @NonNull Map<@NonNull Byte, @NonNull Short> map; + | + | public final @NonNull List<@NonNull Integer> listing; + | + | public final @NonNull Map<@NonNull URI, @NonNull Short> mapping; + | + | public final Integer nullable; + """, + javaCode + ) + + assertContains( + """ + | public static final class Foo { + | public final short uint8; + | + | public final int uint16; + | + | public final long uint32; + | + | public final long uint; + | + | public final byte int8; + | + | public final short int16; + | + | public final int int32; + | + | public final @NonNull URI uri; + | + | public final @NonNull List<@NonNull Long> list; + """, + javaCode + ) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `nullable properties`() { + var javaCode = + generateJavaCode( + """ + module mod + + foo: String + """, + nonNullAnnotation = "com.example.Annotations\$NonNull" + ) + + assertContains("import com.example.Annotations;", javaCode) + assertContains("public final @Annotations.NonNull String foo;", javaCode) + + javaCode = + generateJavaCode( + """ + module mod + + foo: Int + bar: Int? + baz: Any + qux: String + foo2: List? + bar2: List + baz2: List + qux2: List + """ + ) + + assertContains( + """ + |public final class Mod { + | public final long foo; + | + | public final Long bar; + | + | public final Object baz; + | + | public final @NonNull String qux; + | + | public final List<@NonNull String> foo2; + | + | public final @NonNull List bar2; + | + | public final @NonNull List<@NonNull String> baz2; + | + | public final @NonNull List<@NonNull Long> qux2; + """, + javaCode + ) + } + + @Test + fun `user defined type aliases`() { + val javaCode = + generateJavaCode( + """ + module mod + + typealias Simple = String + typealias Constrained = String(length >= 3) + typealias Parameterized = List + typealias Recursive1 = Parameterized(nonEmpty) + typealias Recursive2 = List + + simple: Simple + constrained: Constrained + parameterized: Parameterized + recursive1: Recursive1 + recursive2: Recursive2 + + class Foo { + simple: Simple + constrained: Constrained + parameterized: Parameterized + recursive1: Recursive1 + recursive2: Recursive2 + } + """ + ) + + assertContains( + """ + |public final class Mod { + | public final @NonNull String simple; + | + | public final @NonNull String constrained; + | + | public final @NonNull List<@NonNull Long> parameterized; + | + | public final @NonNull List<@NonNull Long> recursive1; + | + | public final @NonNull List<@NonNull String> recursive2; + """, + javaCode + ) + + assertContains( + """ + | public static final class Foo { + | public final @NonNull String simple; + | + | public final @NonNull String constrained; + | + | public final @NonNull List<@NonNull Long> parameterized; + | + | public final @NonNull List<@NonNull Long> recursive1; + | + | public final @NonNull List<@NonNull String> recursive2; + """, + javaCode + ) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `generic type aliases`() { + val javaCode = + generateJavaCode( + """ + module mod + + class Person { name: String } + + typealias List2 = List + typealias Map2 = Map + typealias StringMap = Map + typealias MMap = Map + + res1: List2 + res2: List2> + res3: Map2 + res4: StringMap + res5: MMap + + res6: List2 + res7: Map2 + res8: StringMap + res9: MMap + + class Foo { + res1: List2 + res2: List2> + res3: Map2 + res4: StringMap + res5: MMap + + res6: List2 + res7: Map2 + res8: StringMap + res9: MMap + } + """ + ) + + assertContains( + """ + |public final class Mod { + | public final @NonNull List<@NonNull Long> res1; + | + | public final @NonNull List<@NonNull List<@NonNull String>> res2; + | + | public final @NonNull Map<@NonNull Long, @NonNull String> res3; + | + | public final @NonNull Map<@NonNull String, @NonNull Duration> res4; + | + | public final @NonNull Map res5; + | + | public final @NonNull List<@NonNull Object> res6; + | + | public final @NonNull Map<@NonNull Object, @NonNull Object> res7; + | + | public final @NonNull Map<@NonNull String, @NonNull Object> res8; + | + | public final @NonNull Map<@NonNull Object, @NonNull Object> res9; + """, + javaCode + ) + + assertContains( + """ + | public static final class Foo { + | public final @NonNull List<@NonNull Long> res1; + | + | public final @NonNull List<@NonNull List<@NonNull String>> res2; + | + | public final @NonNull Map<@NonNull Long, @NonNull String> res3; + | + | public final @NonNull Map<@NonNull String, @NonNull Duration> res4; + | + | public final @NonNull Map res5; + | + | public final @NonNull List<@NonNull Object> res6; + | + | public final @NonNull Map<@NonNull Object, @NonNull Object> res7; + | + | public final @NonNull Map<@NonNull String, @NonNull Object> res8; + | + | public final @NonNull Map<@NonNull Object, @NonNull Object> res9; + """, + javaCode + ) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `union of string literals`() { + val javaCode = + generateJavaCode(""" + module mod + + x: "Pigeon"|"Barn Owl"|"Parrot" + """) + + assertContains("public final @NonNull String x;", javaCode) + + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `other union type`() { + val e = + assertThrows { + generateJavaCode(""" + module mod + + x: "Pigeon"|Int|"Parrot" + """) + } + assertThat(e).hasMessageContaining("Pkl union types are not supported") + } + + @Test + fun `stringy type`() { + val javaCode = + generateJavaCode( + """ + module mod + + v1: "RELEASE" + v2: "RELEASE"|String + v3: String|"RELEASE" + v4: "RELEASE"|String|"LATEST" + v5: Version|String|"LATEST" + v6: (Version|String)|("LATEST"|String) + + typealias Version = "RELEASE"|String|"LATEST" + """ + ) + + assertContains("public final @NonNull String v1;", javaCode) + assertContains("public final @NonNull String v2;", javaCode) + assertContains("public final @NonNull String v3;", javaCode) + assertContains("public final @NonNull String v4;", javaCode) + assertContains("public final @NonNull String v5;", javaCode) + assertContains("public final @NonNull String v6;", javaCode) + } + + @Test + fun `stringy type alias`() { + val javaCode = + generateJavaCode( + """ + module mod + + typealias Version1 = "RELEASE"|String + typealias Version2 = String|"RELEASE" + typealias Version3 = "RELEASE"|String|"LATEST" + typealias Version4 = Version3|String|"LATEST" + typealias Version5 = (Version4|String)|("LATEST"|String) + typealias Version6 = Version5 + + v1: Version1 + v2: Version2 + v3: Version3 + v4: Version4 + v5: Version5 + v6: Version6 + """ + ) + + assertContains("public final @NonNull String v1;", javaCode) + assertContains("public final @NonNull String v2;", javaCode) + assertContains("public final @NonNull String v3;", javaCode) + assertContains("public final @NonNull String v4;", javaCode) + assertContains("public final @NonNull String v5;", javaCode) + assertContains("public final @NonNull String v6;", javaCode) + } + + @Test + fun `spring boot config`() { + val javaCode = + generateJavaCode( + """ + module my.mod + + server: Server + + class Server { + port: Int + urls: Listing + } + """, + generateSpringBootConfig = true + ) + + // not worthwhile to add spring & spring boot dependency just so that this test can compile + // their annotations + val javaCodeWithoutSpringAnnotations = + javaCode + .lines() + .filterNot { it.contains("ConstructorBinding") || it.contains("ConfigurationProperties") } + .joinToString("\n") + + assertContains( + """ + |@ConstructorBinding + |@ConfigurationProperties + |public final class Mod { + """, + javaCode + ) + + assertContains(""" + | public final @NonNull Server server; + """, javaCode) + + assertContains( + """ + | @ConstructorBinding + | @ConfigurationProperties("server") + | public static final class Server { + """, + javaCode + ) + + assertContains( + """ + | public final long port; + | + | public final @NonNull List<@NonNull URI> urls; + """, + javaCode + ) + + assertCompilesSuccessfully(javaCodeWithoutSpringAnnotations) + } + + @Test + fun `import module`(@TempDir tempDir: Path) { + val library = + PklModule( + "library", + """ + module library + + class Person { name: String; age: Int } + + pigeon: Person + """ + .trimIndent() + ) + + val client = + PklModule( + "client", + """ + module client + + import "library.pkl" + + lib: library + + parrot: library.Person + """ + .trimIndent() + ) + + val javaSourceFiles = generateJavaFiles(tempDir, library, client) + val javaClientCode = + javaSourceFiles.entries.find { (fileName, _) -> fileName.endsWith("Client.java") }!!.value + + assertContains( + """ + |public final class Client { + | public final @NonNull Library lib; + | + | public final Library. @NonNull Person parrot; + """, + javaClientCode + ) + + assertDoesNotThrow { InMemoryJavaCompiler.compile(javaSourceFiles) } + } + + @Test + fun `extend module`(@TempDir tempDir: Path) { + val base = + PklModule( + "base", + """ + open module base + + open class Person { name: String } + + pigeon: Person + """ + .trimIndent() + ) + + val derived = + PklModule( + "derived", + """ + module derived + extends "base.pkl" + + class Person2 extends Person { age: Int } + + person1: Person + person2: Person2 + """ + .trimIndent() + ) + + val javaSourceFiles = generateJavaFiles(tempDir, base, derived) + val javaDerivedCode = + javaSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.java") }!!.value + + assertContains( + """ + |public final class Derived extends Base { + | public final Base. @NonNull Person person1; + | + | public final @NonNull Person2 person2; + """, + javaDerivedCode + ) + + assertDoesNotThrow { InMemoryJavaCompiler.compile(javaSourceFiles) } + } + + @Test + fun `empty module`() { + val javaCode = generateJavaCode("module mod") + assertContains("public final class Mod {", javaCode) + } + + @Test + fun `extend module that only contains type aliases`(@TempDir tempDir: Path) { + val base = + PklModule( + "base", + """ + abstract module base + + typealias Version = "LATEST"|String + """ + .trimIndent() + ) + + val derived = + PklModule( + "derived", + """ + module derived + + extends "base.pkl" + + v: Version = "1.2.3" + """ + .trimIndent() + ) + + val javaSourceFiles = generateJavaFiles(tempDir, base, derived) + val javaDerivedCode = + javaSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.java") }!!.value + + assertContains( + """ + |public final class Derived extends Base { + | public final @NonNull String v; + """, + javaDerivedCode + ) + + assertDoesNotThrow { InMemoryJavaCompiler.compile(javaSourceFiles) } + } + + @Test + fun `generated properties files`(@TempDir tempDir: Path) { + val pklModule = + PklModule( + "Mod.pkl", + """ + module org.pkl.Mod + + foo: Foo + + bar: Bar + + class Foo { + prop: String + } + + class Bar { + prop: Int + } + """ + .trimIndent() + ) + val generated = generateFiles(tempDir, pklModule) + val expectedPropertyFile = + "resources/META-INF/org/pkl/config/java/mapper/classes/org.pkl.Mod.properties" + assertThat(generated).containsKey(expectedPropertyFile) + val generatedFile = generated[expectedPropertyFile]!! + assertThat(generatedFile) + .contains("org.pkl.config.java.mapper.org.pkl.Mod\\#ModuleClass=org.pkl.Mod") + assertThat(generatedFile) + .contains("org.pkl.config.java.mapper.org.pkl.Mod\\#Foo=org.pkl.Mod\$Foo") + assertThat(generatedFile) + .contains("org.pkl.config.java.mapper.org.pkl.Mod\\#Bar=org.pkl.Mod\$Bar") + } + + @Test + fun `generated properties files with normalized java name`(@TempDir tempDir: Path) { + val pklModule = + PklModule( + "mod.pkl", + """ + module my.mod + + foo: Foo + + bar: Bar + + class Foo { + prop: String + } + + class Bar { + prop: Int + } + """ + .trimIndent() + ) + val generated = generateFiles(tempDir, pklModule) + val expectedPropertyFile = + "resources/META-INF/org/pkl/config/java/mapper/classes/my.mod.properties" + assertThat(generated).containsKey(expectedPropertyFile) + val generatedFile = generated[expectedPropertyFile]!! + assertThat(generatedFile).contains("org.pkl.config.java.mapper.my.mod\\#ModuleClass=my.Mod") + assertThat(generatedFile).contains("org.pkl.config.java.mapper.my.mod\\#Foo=my.Mod\$Foo") + assertThat(generatedFile).contains("org.pkl.config.java.mapper.my.mod\\#Bar=my.Mod\$Bar") + } + + @Test + fun `generates serializable classes`() { + val javaCode = + generateJavaCode( + """ + module mod + + class BigStruct { + boolean: Boolean + int: Int + float: Float + string: String + duration: Duration + dataSize: DataSize + pair: Pair + pair2: Pair + coll: Collection + coll2: Collection + list: List + list2: List + set: Set + set2: Set + map: Map + map2: Map + container: Mapping + container2: Mapping + other: SmallStruct + regex: Regex + nonNull: NonNull + enum: Direction + } + + class SmallStruct { + name: String + } + + typealias Direction = "north"|"east"|"south"|"west" + """, + implementSerializable = true + ) + + assertContains("implements Serializable", javaCode) + assertContains("private static final long serialVersionUID = 0L;", javaCode) + + val classes = compileJavaCode(javaCode) + + val smallStructCtor = classes.getValue("Mod\$SmallStruct").constructors.first() + val smallStruct = smallStructCtor.newInstance("pigeon") + + val enumClass = classes.getValue("Mod\$Direction") + val enumValue = enumClass.enumConstants.first() + + val bigStructCtor = classes.getValue("Mod\$BigStruct").constructors.first() + val bigStruct = + bigStructCtor.newInstance( + true, + 42L, + 42.3, + "string", + Duration(5.0, DurationUnit.MINUTES), + DataSize(3.0, DataSizeUnit.GIGABYTES), + Pair(1, 2), + Pair("pigeon", smallStruct), + listOf(1, 2, 3), + listOf(smallStruct, smallStruct), + listOf(1, 2, 3), + listOf(smallStruct, smallStruct), + setOf(1, 2, 3), + setOf(smallStruct, smallStruct), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to smallStruct, "two" to smallStruct), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to smallStruct, "two" to smallStruct), + smallStruct, + Pattern.compile("(i?)\\w*"), + smallStruct, + enumValue + ) + + fun confirmSerDe(instance: Any) { + var restoredInstance: Any? = null + + assertThatCode { + // serialize + val baos = ByteArrayOutputStream() + val oos = ObjectOutputStream(baos) + oos.writeObject(instance) + oos.flush() + + // deserialize + val bais = ByteArrayInputStream(baos.toByteArray()) + val ois = + object : ObjectInputStream(bais) { + override fun resolveClass(desc: ObjectStreamClass?): Class<*> { + return Class.forName(desc!!.name, false, instance.javaClass.classLoader) + } + } + restoredInstance = ois.readObject() + } + .doesNotThrowAnyException() + + assertThat(restoredInstance!!).isEqualTo(instance) + } + + confirmSerDe(enumValue) + confirmSerDe(smallStruct) + confirmSerDe(bigStruct) + } + + @Test + fun `override property type`() { + val javaCode = + generateJavaCode( + """ + module my.mod + + open class Foo + + class TheFoo extends Foo { + fooProp: String + } + + open class OpenClass { + prop: Foo + } + + class TheClass extends OpenClass { + prop: TheFoo + } + """ + .trimIndent() + ) + assertThat(javaCode) + .contains( + """ + | public static final class TheClass extends OpenClass { + | public final @NonNull TheFoo prop; + | + | public TheClass(@Named("prop") @NonNull TheFoo prop) { + | super(prop); + | this.prop = prop; + | } + | + | public TheClass withProp(@NonNull TheFoo prop) { + | return new TheClass(prop); + | } + """ + .trimMargin() + ) + assertCompilesSuccessfully(javaCode) + } + + @Test + fun `override property type, with getters`() { + val javaCode = + generateJavaCode( + """ + module my.mod + + open class Foo + + class TheFoo extends Foo { + fooProp: String + } + + open class OpenClass { + prop: Foo + } + + class TheClass extends OpenClass { + prop: TheFoo + } + """ + .trimIndent(), + generateGetters = true + ) + assertThat(javaCode) + .contains( + """ + | public static final class TheClass extends OpenClass { + | private final @NonNull TheFoo prop; + | + | public TheClass(@Named("prop") @NonNull TheFoo prop) { + | super(prop); + | this.prop = prop; + | } + | + | @Override + | public @NonNull TheFoo getProp() { + | return prop; + | } + """ + .trimMargin() + ) + assertCompilesSuccessfully(javaCode) + } + + private fun generateFiles(tempDir: Path, vararg pklModules: PklModule): Map { + val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) } + val evaluator = Evaluator.preconfigured() + return pklFiles.fold(mapOf()) { acc, pklFile -> + val pklSchema = evaluator.evaluateSchema(path(pklFile)) + acc + JavaCodeGenerator(pklSchema, JavaCodegenOptions()).output + } + } + + private fun generateJavaFiles(tempDir: Path, vararg pklModules: PklModule): Map { + val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) } + val evaluator = Evaluator.preconfigured() + return pklFiles.fold(mapOf()) { acc, pklFile -> + val pklSchema = evaluator.evaluateSchema(path(pklFile)) + val generator = JavaCodeGenerator(pklSchema, JavaCodegenOptions()) + acc + arrayOf(generator.javaFileName to generator.javaFile) + } + } + + private fun instantiateOtherAndPropertyTypes(): kotlin.Pair { + val otherCtor = propertyTypesClasses.getValue("my.Mod\$Other").constructors.first() + val other = otherCtor.newInstance("pigeon") + + val enumClass = propertyTypesClasses.getValue("my.Mod\$Direction") + val enumValue = enumClass.enumConstants.first() + + val propertyTypesCtor = + propertyTypesClasses.getValue("my.Mod\$PropertyTypes").constructors.first() + val propertyTypes = + propertyTypesCtor.newInstance( + true, + 42, + 42.3, + "string", + Duration(5.0, DurationUnit.MINUTES), + DurationUnit.MINUTES, + DataSize(3.0, DataSizeUnit.GIGABYTES), + DataSizeUnit.GIGABYTES, + "idea", + (null as String?), + Pair(1, 2), + Pair("pigeon", other), + listOf(1, 2, 3), + listOf(other, other), + listOf(1, 2, 3), + listOf(other, other), + setOf(1, 2, 3), + setOf(other, other), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to other, "two" to other), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to other, "two" to other), + other, + Pattern.compile("(i?)\\w*"), + other, + other, + enumValue + ) + + return other to propertyTypes + } + + private fun assertContains(part: String, code: String) { + val trimmedPart = part.trim().trimMargin() + if (!code.contains(trimmedPart)) { + // check for equality to get better error output (ide diff dialog) + assertThat(code).isEqualTo(trimmedPart) + } + } + + private fun assertEqualTo(expectedCode: String, actualCode: String) { + assertThat(actualCode.trim()).isEqualTo(expectedCode.trimIndent().trim()) + } +} diff --git a/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/PklModule.kt b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/PklModule.kt new file mode 100644 index 00000000..d19afe94 --- /dev/null +++ b/pkl-codegen-java/src/test/kotlin/org/pkl/codegen/java/PklModule.kt @@ -0,0 +1,26 @@ +/** + * 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.codegen.java + +import java.nio.file.Path +import org.pkl.commons.createParentDirectories +import org.pkl.commons.writeString + +data class PklModule(val name: String, val content: String) { + fun writeToDisk(path: Path): Path { + return path.createParentDirectories().writeString(content) + } +} diff --git a/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/GenerateGetters.jva b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/GenerateGetters.jva new file mode 100644 index 00000000..b1955249 --- /dev/null +++ b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/GenerateGetters.jva @@ -0,0 +1,125 @@ +package my; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.Objects; +import org.pkl.config.java.mapper.Named; +import org.pkl.config.java.mapper.NonNull; +import org.pkl.core.DataSize; +import org.pkl.core.Duration; + +public final class Mod { + private Mod() { + } + + private static void appendProperty(StringBuilder builder, String name, Object value) { + builder.append("\n ").append(name).append(" = "); + String[] lines = Objects.toString(value).split("\n"); + builder.append(lines[0]); + for (int i = 1; i < lines.length; i++) { + builder.append("\n ").append(lines[i]); + } + } + + public static final class GenerateGetters { + private final boolean urgent; + + private final @NonNull String url; + + private final @NonNull DataSize diskSize; + + private final @NonNull Duration ETA; + + private final @NonNull String _package; + + public GenerateGetters(@Named("urgent") boolean urgent, @Named("url") @NonNull String url, + @Named("diskSize") @NonNull DataSize diskSize, @Named("ETA") @NonNull Duration ETA, + @Named("package") @NonNull String _package) { + this.urgent = urgent; + this.url = url; + this.diskSize = diskSize; + this.ETA = ETA; + this._package = _package; + } + + public boolean isUrgent() { + return urgent; + } + + public GenerateGetters withUrgent(boolean urgent) { + return new GenerateGetters(urgent, url, diskSize, ETA, _package); + } + + public @NonNull String getUrl() { + return url; + } + + public GenerateGetters withUrl(@NonNull String url) { + return new GenerateGetters(urgent, url, diskSize, ETA, _package); + } + + public @NonNull DataSize getDiskSize() { + return diskSize; + } + + public GenerateGetters withDiskSize(@NonNull DataSize diskSize) { + return new GenerateGetters(urgent, url, diskSize, ETA, _package); + } + + public @NonNull Duration getETA() { + return ETA; + } + + public GenerateGetters withETA(@NonNull Duration ETA) { + return new GenerateGetters(urgent, url, diskSize, ETA, _package); + } + + public @NonNull String getPackage() { + return _package; + } + + public GenerateGetters withPackage(@NonNull String _package) { + return new GenerateGetters(urgent, url, diskSize, ETA, _package); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + GenerateGetters other = (GenerateGetters) obj; + if (!Objects.equals(this.urgent, other.urgent)) return false; + if (!Objects.equals(this.url, other.url)) return false; + if (!Objects.equals(this.diskSize, other.diskSize)) return false; + if (!Objects.equals(this.ETA, other.ETA)) return false; + if (!Objects.equals(this._package, other._package)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this.urgent); + result = 31 * result + Objects.hashCode(this.url); + result = 31 * result + Objects.hashCode(this.diskSize); + result = 31 * result + Objects.hashCode(this.ETA); + result = 31 * result + Objects.hashCode(this._package); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(300); + builder.append(GenerateGetters.class.getSimpleName()).append(" {"); + appendProperty(builder, "urgent", this.urgent); + appendProperty(builder, "url", this.url); + appendProperty(builder, "diskSize", this.diskSize); + appendProperty(builder, "ETA", this.ETA); + appendProperty(builder, "_package", this._package); + builder.append("\n}"); + return builder.toString(); + } + } +} diff --git a/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/Inheritance.jva b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/Inheritance.jva new file mode 100644 index 00000000..eec3a5d4 --- /dev/null +++ b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/Inheritance.jva @@ -0,0 +1,180 @@ +package my; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.Objects; +import org.pkl.config.java.mapper.Named; +import org.pkl.config.java.mapper.NonNull; +import org.pkl.core.Duration; + +public final class Mod { + private Mod() { + } + + private static void appendProperty(StringBuilder builder, String name, Object value) { + builder.append("\n ").append(name).append(" = "); + String[] lines = Objects.toString(value).split("\n"); + builder.append(lines[0]); + for (int i = 1; i < lines.length; i++) { + builder.append("\n ").append(lines[i]); + } + } + + public abstract static class Foo { + protected final long one; + + protected Foo(@Named("one") long one) { + this.one = one; + } + + public long getOne() { + return one; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + Foo other = (Foo) obj; + if (!Objects.equals(this.one, other.one)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this.one); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(100); + builder.append(Foo.class.getSimpleName()).append(" {"); + appendProperty(builder, "one", this.one); + builder.append("\n}"); + return builder.toString(); + } + } + + public static class None extends Foo { + public None(@Named("one") long one) { + super(one); + } + + public None withOne(long one) { + return new None(one); + } + } + + public static class Bar extends None { + protected final String two; + + public Bar(@Named("one") long one, @Named("two") String two) { + super(one); + this.two = two; + } + + public Bar withOne(long one) { + return new Bar(one, two); + } + + public String getTwo() { + return two; + } + + public Bar withTwo(String two) { + return new Bar(one, two); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + Bar other = (Bar) obj; + if (!Objects.equals(this.one, other.one)) return false; + if (!Objects.equals(this.two, other.two)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this.one); + result = 31 * result + Objects.hashCode(this.two); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(150); + builder.append(Bar.class.getSimpleName()).append(" {"); + appendProperty(builder, "one", this.one); + appendProperty(builder, "two", this.two); + builder.append("\n}"); + return builder.toString(); + } + } + + public static final class Baz extends Bar { + private final @NonNull Duration three; + + public Baz(@Named("one") long one, @Named("two") String two, + @Named("three") @NonNull Duration three) { + super(one, two); + this.three = three; + } + + public Baz withOne(long one) { + return new Baz(one, two, three); + } + + public Baz withTwo(String two) { + return new Baz(one, two, three); + } + + public @NonNull Duration getThree() { + return three; + } + + public Baz withThree(@NonNull Duration three) { + return new Baz(one, two, three); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + Baz other = (Baz) obj; + if (!Objects.equals(this.one, other.one)) return false; + if (!Objects.equals(this.two, other.two)) return false; + if (!Objects.equals(this.three, other.three)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this.one); + result = 31 * result + Objects.hashCode(this.two); + result = 31 * result + Objects.hashCode(this.three); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(200); + builder.append(Baz.class.getSimpleName()).append(" {"); + appendProperty(builder, "one", this.one); + appendProperty(builder, "two", this.two); + appendProperty(builder, "three", this.three); + builder.append("\n}"); + return builder.toString(); + } + } +} diff --git a/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/Javadoc.jva b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/Javadoc.jva new file mode 100644 index 00000000..5e4fc227 --- /dev/null +++ b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/Javadoc.jva @@ -0,0 +1,110 @@ +package my; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.Objects; +import org.pkl.config.java.mapper.Named; +import org.pkl.config.java.mapper.NonNull; + +/** + * module comment. + * *emphasized* `code`. + */ +public final class Mod { + /** + * module property comment. + * *emphasized* `code`. + */ + public final @NonNull Person pigeon; + + public Mod(@Named("pigeon") @NonNull Person pigeon) { + this.pigeon = pigeon; + } + + public Mod withPigeon(@NonNull Person pigeon) { + return new Mod(pigeon); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + Mod other = (Mod) obj; + if (!Objects.equals(this.pigeon, other.pigeon)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this.pigeon); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(100); + builder.append(Mod.class.getSimpleName()).append(" {"); + appendProperty(builder, "pigeon", this.pigeon); + builder.append("\n}"); + return builder.toString(); + } + + private static void appendProperty(StringBuilder builder, String name, Object value) { + builder.append("\n ").append(name).append(" = "); + String[] lines = Objects.toString(value).split("\n"); + builder.append(lines[0]); + for (int i = 1; i < lines.length; i++) { + builder.append("\n ").append(lines[i]); + } + } + + /** + * class comment. + * *emphasized* `code`. + */ + public static final class Person { + /** + * class property comment. + * *emphasized* `code`. + */ + public final @NonNull String name; + + public Person(@Named("name") @NonNull String name) { + this.name = name; + } + + public Person withName(@NonNull String name) { + return new Person(name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + Person other = (Person) obj; + if (!Objects.equals(this.name, other.name)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this.name); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(100); + builder.append(Person.class.getSimpleName()).append(" {"); + appendProperty(builder, "name", this.name); + builder.append("\n}"); + return builder.toString(); + } + } +} diff --git a/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/PropertyTypes.jva b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/PropertyTypes.jva new file mode 100644 index 00000000..d6aff187 --- /dev/null +++ b/pkl-codegen-java/src/test/resources/org/pkl/codegen/java/PropertyTypes.jva @@ -0,0 +1,410 @@ +package my; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; +import org.pkl.config.java.mapper.Named; +import org.pkl.config.java.mapper.NonNull; +import org.pkl.core.DataSize; +import org.pkl.core.DataSizeUnit; +import org.pkl.core.Duration; +import org.pkl.core.DurationUnit; +import org.pkl.core.Pair; + +public final class Mod { + private Mod() { + } + + private static void appendProperty(StringBuilder builder, String name, Object value) { + builder.append("\n ").append(name).append(" = "); + String[] lines = Objects.toString(value).split("\n"); + builder.append(lines[0]); + for (int i = 1; i < lines.length; i++) { + builder.append("\n ").append(lines[i]); + } + } + + public static final class PropertyTypes { + public final boolean _boolean; + + public final long _int; + + public final double _float; + + public final @NonNull String string; + + public final @NonNull Duration duration; + + public final @NonNull DurationUnit durationUnit; + + public final @NonNull DataSize dataSize; + + public final @NonNull DataSizeUnit dataSizeUnit; + + public final String nullable; + + public final String nullable2; + + public final @NonNull Pair pair; + + public final @NonNull Pair<@NonNull String, @NonNull Other> pair2; + + public final @NonNull Collection coll; + + public final @NonNull Collection<@NonNull Other> coll2; + + public final @NonNull List list; + + public final @NonNull List<@NonNull Other> list2; + + public final @NonNull Set set; + + public final @NonNull Set<@NonNull Other> set2; + + public final @NonNull Map map; + + public final @NonNull Map<@NonNull String, @NonNull Other> map2; + + public final @NonNull Map container; + + public final @NonNull Map<@NonNull String, @NonNull Other> container2; + + public final @NonNull Other other; + + public final @NonNull Pattern regex; + + public final Object any; + + public final @NonNull Object nonNull; + + public final @NonNull Direction _enum; + + public PropertyTypes(@Named("boolean") boolean _boolean, @Named("int") long _int, + @Named("float") double _float, @Named("string") @NonNull String string, + @Named("duration") @NonNull Duration duration, + @Named("durationUnit") @NonNull DurationUnit durationUnit, + @Named("dataSize") @NonNull DataSize dataSize, + @Named("dataSizeUnit") @NonNull DataSizeUnit dataSizeUnit, + @Named("nullable") String nullable, @Named("nullable2") String nullable2, + @Named("pair") @NonNull Pair pair, + @Named("pair2") @NonNull Pair<@NonNull String, @NonNull Other> pair2, + @Named("coll") @NonNull Collection coll, + @Named("coll2") @NonNull Collection<@NonNull Other> coll2, + @Named("list") @NonNull List list, + @Named("list2") @NonNull List<@NonNull Other> list2, @Named("set") @NonNull Set set, + @Named("set2") @NonNull Set<@NonNull Other> set2, + @Named("map") @NonNull Map map, + @Named("map2") @NonNull Map<@NonNull String, @NonNull Other> map2, + @Named("container") @NonNull Map container, + @Named("container2") @NonNull Map<@NonNull String, @NonNull Other> container2, + @Named("other") @NonNull Other other, @Named("regex") @NonNull Pattern regex, + @Named("any") Object any, @Named("nonNull") @NonNull Object nonNull, + @Named("enum") @NonNull Direction _enum) { + this._boolean = _boolean; + this._int = _int; + this._float = _float; + this.string = string; + this.duration = duration; + this.durationUnit = durationUnit; + this.dataSize = dataSize; + this.dataSizeUnit = dataSizeUnit; + this.nullable = nullable; + this.nullable2 = nullable2; + this.pair = pair; + this.pair2 = pair2; + this.coll = coll; + this.coll2 = coll2; + this.list = list; + this.list2 = list2; + this.set = set; + this.set2 = set2; + this.map = map; + this.map2 = map2; + this.container = container; + this.container2 = container2; + this.other = other; + this.regex = regex; + this.any = any; + this.nonNull = nonNull; + this._enum = _enum; + } + + public PropertyTypes withBoolean(boolean _boolean) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withInt(long _int) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withFloat(double _float) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withString(@NonNull String string) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withDuration(@NonNull Duration duration) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withDurationUnit(@NonNull DurationUnit durationUnit) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withDataSize(@NonNull DataSize dataSize) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withDataSizeUnit(@NonNull DataSizeUnit dataSizeUnit) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withNullable(String nullable) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withNullable2(String nullable2) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withPair(@NonNull Pair pair) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withPair2(@NonNull Pair<@NonNull String, @NonNull Other> pair2) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withColl(@NonNull Collection coll) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withColl2(@NonNull Collection<@NonNull Other> coll2) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withList(@NonNull List list) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withList2(@NonNull List<@NonNull Other> list2) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withSet(@NonNull Set set) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withSet2(@NonNull Set<@NonNull Other> set2) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withMap(@NonNull Map map) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withMap2(@NonNull Map<@NonNull String, @NonNull Other> map2) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withContainer(@NonNull Map container) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withContainer2(@NonNull Map<@NonNull String, @NonNull Other> container2) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withOther(@NonNull Other other) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withRegex(@NonNull Pattern regex) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withAny(Object any) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withNonNull(@NonNull Object nonNull) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + public PropertyTypes withEnum(@NonNull Direction _enum) { + return new PropertyTypes(_boolean, _int, _float, string, duration, durationUnit, dataSize, dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, map2, container, container2, other, regex, any, nonNull, _enum); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + PropertyTypes other = (PropertyTypes) obj; + if (!Objects.equals(this._boolean, other._boolean)) return false; + if (!Objects.equals(this._int, other._int)) return false; + if (!Objects.equals(this._float, other._float)) return false; + if (!Objects.equals(this.string, other.string)) return false; + if (!Objects.equals(this.duration, other.duration)) return false; + if (!Objects.equals(this.durationUnit, other.durationUnit)) return false; + if (!Objects.equals(this.dataSize, other.dataSize)) return false; + if (!Objects.equals(this.dataSizeUnit, other.dataSizeUnit)) return false; + if (!Objects.equals(this.nullable, other.nullable)) return false; + if (!Objects.equals(this.nullable2, other.nullable2)) return false; + if (!Objects.equals(this.pair, other.pair)) return false; + if (!Objects.equals(this.pair2, other.pair2)) return false; + if (!Objects.equals(this.coll, other.coll)) return false; + if (!Objects.equals(this.coll2, other.coll2)) return false; + if (!Objects.equals(this.list, other.list)) return false; + if (!Objects.equals(this.list2, other.list2)) return false; + if (!Objects.equals(this.set, other.set)) return false; + if (!Objects.equals(this.set2, other.set2)) return false; + if (!Objects.equals(this.map, other.map)) return false; + if (!Objects.equals(this.map2, other.map2)) return false; + if (!Objects.equals(this.container, other.container)) return false; + if (!Objects.equals(this.container2, other.container2)) return false; + if (!Objects.equals(this.other, other.other)) return false; + if (!Objects.equals(this.regex.pattern(), other.regex.pattern())) return false; + if (!Objects.equals(this.any, other.any)) return false; + if (!Objects.equals(this.nonNull, other.nonNull)) return false; + if (!Objects.equals(this._enum, other._enum)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this._boolean); + result = 31 * result + Objects.hashCode(this._int); + result = 31 * result + Objects.hashCode(this._float); + result = 31 * result + Objects.hashCode(this.string); + result = 31 * result + Objects.hashCode(this.duration); + result = 31 * result + Objects.hashCode(this.durationUnit); + result = 31 * result + Objects.hashCode(this.dataSize); + result = 31 * result + Objects.hashCode(this.dataSizeUnit); + result = 31 * result + Objects.hashCode(this.nullable); + result = 31 * result + Objects.hashCode(this.nullable2); + result = 31 * result + Objects.hashCode(this.pair); + result = 31 * result + Objects.hashCode(this.pair2); + result = 31 * result + Objects.hashCode(this.coll); + result = 31 * result + Objects.hashCode(this.coll2); + result = 31 * result + Objects.hashCode(this.list); + result = 31 * result + Objects.hashCode(this.list2); + result = 31 * result + Objects.hashCode(this.set); + result = 31 * result + Objects.hashCode(this.set2); + result = 31 * result + Objects.hashCode(this.map); + result = 31 * result + Objects.hashCode(this.map2); + result = 31 * result + Objects.hashCode(this.container); + result = 31 * result + Objects.hashCode(this.container2); + result = 31 * result + Objects.hashCode(this.other); + result = 31 * result + Objects.hashCode(this.regex); + result = 31 * result + Objects.hashCode(this.any); + result = 31 * result + Objects.hashCode(this.nonNull); + result = 31 * result + Objects.hashCode(this._enum); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(1400); + builder.append(PropertyTypes.class.getSimpleName()).append(" {"); + appendProperty(builder, "_boolean", this._boolean); + appendProperty(builder, "_int", this._int); + appendProperty(builder, "_float", this._float); + appendProperty(builder, "string", this.string); + appendProperty(builder, "duration", this.duration); + appendProperty(builder, "durationUnit", this.durationUnit); + appendProperty(builder, "dataSize", this.dataSize); + appendProperty(builder, "dataSizeUnit", this.dataSizeUnit); + appendProperty(builder, "nullable", this.nullable); + appendProperty(builder, "nullable2", this.nullable2); + appendProperty(builder, "pair", this.pair); + appendProperty(builder, "pair2", this.pair2); + appendProperty(builder, "coll", this.coll); + appendProperty(builder, "coll2", this.coll2); + appendProperty(builder, "list", this.list); + appendProperty(builder, "list2", this.list2); + appendProperty(builder, "set", this.set); + appendProperty(builder, "set2", this.set2); + appendProperty(builder, "map", this.map); + appendProperty(builder, "map2", this.map2); + appendProperty(builder, "container", this.container); + appendProperty(builder, "container2", this.container2); + appendProperty(builder, "other", this.other); + appendProperty(builder, "regex", this.regex); + appendProperty(builder, "any", this.any); + appendProperty(builder, "nonNull", this.nonNull); + appendProperty(builder, "_enum", this._enum); + builder.append("\n}"); + return builder.toString(); + } + } + + public static final class Other { + public final @NonNull String name; + + public Other(@Named("name") @NonNull String name) { + this.name = name; + } + + public Other withName(@NonNull String name) { + return new Other(name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (this.getClass() != obj.getClass()) return false; + Other other = (Other) obj; + if (!Objects.equals(this.name, other.name)) return false; + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Objects.hashCode(this.name); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(100); + builder.append(Other.class.getSimpleName()).append(" {"); + appendProperty(builder, "name", this.name); + builder.append("\n}"); + return builder.toString(); + } + } + + public enum Direction { + NORTH("north"), + + EAST("east"), + + SOUTH("south"), + + WEST("west"); + + private String value; + + private Direction(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + } +} diff --git a/pkl-codegen-kotlin/gradle.lockfile b/pkl-codegen-kotlin/gradle.lockfile new file mode 100644 index 00000000..42331c24 --- /dev/null +++ b/pkl-codegen-kotlin/gradle.lockfile @@ -0,0 +1,42 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.github.ajalt.clikt:clikt-jvm:3.5.1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt:3.5.1=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.squareup:kotlinpoet:1.6.0=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,runtimeClasspath,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.14=testRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-daemon-client:1.7.10=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-script-util:1.7.10=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-codegen-kotlin/pkl-codegen-kotlin.gradle.kts b/pkl-codegen-kotlin/pkl-codegen-kotlin.gradle.kts new file mode 100644 index 00000000..1bc2d9d8 --- /dev/null +++ b/pkl-codegen-kotlin/pkl-codegen-kotlin.gradle.kts @@ -0,0 +1,40 @@ +plugins { + pklAllProjects + pklKotlinLibrary + pklPublishLibrary +} + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-codegen-kotlin") + description.set(""" + Kotlin source code generator that generates corresponding Kotlin classes for Pkl classes, + simplifying consumption of Pkl configuration as statically typed Kotlin objects. + """.trimIndent()) + } + } + } +} + +tasks.jar { + manifest { + attributes += mapOf("Main-Class" to "org.pkl.codegen.kotlin.Main") + } +} + +dependencies { + implementation(project(":pkl-commons")) + api(project(":pkl-commons-cli")) + api(project(":pkl-core")) + + implementation(libs.kotlinPoet) + implementation(libs.kotlinReflect) + + testImplementation(project(":pkl-config-kotlin")) + testImplementation(project(":pkl-commons-test")) + testImplementation(libs.kotlinCompilerEmbeddable) + testRuntimeOnly(libs.kotlinScriptingCompilerEmbeddable) + testRuntimeOnly(libs.kotlinScriptUtil) +} diff --git a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGenerator.kt b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGenerator.kt new file mode 100644 index 00000000..57abbe20 --- /dev/null +++ b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGenerator.kt @@ -0,0 +1,56 @@ +/** + * 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.codegen.kotlin + +import java.io.IOException +import org.pkl.commons.cli.CliCommand +import org.pkl.commons.cli.CliException +import org.pkl.commons.createParentDirectories +import org.pkl.commons.writeString +import org.pkl.core.ModuleSource +import org.pkl.core.module.ModuleKeyFactories + +/** API for the Kotlin code generator CLI. */ +class CliKotlinCodeGenerator(private val options: CliKotlinCodeGeneratorOptions) : + CliCommand(options.base) { + + override fun doRun() { + val builder = evaluatorBuilder() + + try { + builder.build().use { evaluator -> + for (moduleUri in options.base.normalizedSourceModules) { + val schema = evaluator.evaluateSchema(ModuleSource.uri(moduleUri)) + val codeGenerator = KotlinCodeGenerator(schema, options.toKotlinCodegenOptions()) + try { + for ((fileName, fileContents) in codeGenerator.output) { + val outputFile = options.outputDir.resolve(fileName) + try { + outputFile.createParentDirectories().writeString(fileContents) + } catch (e: IOException) { + throw CliException("I/O error writing file `$outputFile`.\nCause: ${e.message}") + } + } + } catch (e: KotlinCodeGeneratorException) { + throw CliException(e.message!!) + } + } + } + } finally { + ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories) + } + } +} diff --git a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorOptions.kt b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorOptions.kt new file mode 100644 index 00000000..72e65eaa --- /dev/null +++ b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorOptions.kt @@ -0,0 +1,43 @@ +/** + * 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.codegen.kotlin + +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions + +/** Configuration options for [CliKotlinCodeGenerator]. */ +data class CliKotlinCodeGeneratorOptions( + /** Base options shared between CLI commands. */ + val base: CliBaseOptions, + + /** The directory where generated source code is placed. */ + val outputDir: Path, + + /** The characters to use for indenting generated source code. */ + val indent: String = " ", + + /** Whether to generate Kdoc based on doc comments for Pkl modules, classes, and properties. */ + val generateKdoc: Boolean = false, + + /** Whether to generate config classes for use with Spring Boot. */ + val generateSpringBootConfig: Boolean = false, + + /** Whether to make generated classes implement [java.io.Serializable] */ + val implementSerializable: Boolean = false +) { + fun toKotlinCodegenOptions(): KotlinCodegenOptions = + KotlinCodegenOptions(indent, generateKdoc, generateSpringBootConfig) +} diff --git a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/KotlinCodeGenerator.kt b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/KotlinCodeGenerator.kt new file mode 100644 index 00000000..f2699e7e --- /dev/null +++ b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/KotlinCodeGenerator.kt @@ -0,0 +1,788 @@ +/** + * 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.codegen.kotlin + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.io.StringWriter +import java.net.URI +import java.util.* +import org.pkl.core.* +import org.pkl.core.util.CodeGeneratorUtils + +data class KotlinCodegenOptions( + /** The characters to use for indenting generated Kotlin code. */ + val indent: String = " ", + + /** Whether to generate KDoc based on doc comments for Pkl modules, classes, and properties. */ + val generateKdoc: Boolean = false, + + /** Whether to generate config classes for use with Spring Boot. */ + val generateSpringBootConfig: Boolean = false, + + /** Whether to make generated classes implement [java.io.Serializable] */ + val implementSerializable: Boolean = false +) + +class KotlinCodeGeneratorException(message: String) : RuntimeException(message) + +/** Entrypoint for the Kotlin code generator API. */ +class KotlinCodeGenerator( + /** The schema for the module to generate */ + private val moduleSchema: ModuleSchema, + + /** The options to use for the code generator */ + private val options: KotlinCodegenOptions, +) { + companion object { + // Prevent class name from being replaced with shaded name + // when pkl-codegen-kotlin is shaded and embedded in pkl-tools + // (requires circumventing kotlinc constant folding). + private val KOTLIN_TEXT_PACKAGE_NAME = buildString { + append("kot") + append("lin.") + append("text") + } + + // `StringBuilder::class.asClassName()` generates "java.lang.StringBuilder", + // apparently because `StringBuilder` is an `expect class`. + private val STRING_BUILDER = ClassName(KOTLIN_TEXT_PACKAGE_NAME, "StringBuilder") + + private val STRING = String::class.asClassName() + private val ANY_NULL = ANY.copy(nullable = true) + private val NOTHING = Nothing::class.asClassName() + private val KOTLIN_PAIR = kotlin.Pair::class.asClassName() + private val COLLECTION = Collection::class.asClassName() + private val LIST = List::class.asClassName() + private val SET = Set::class.asClassName() + private val MAP = Map::class.asClassName() + private val DURATION = Duration::class.asClassName() + private val DURATION_UNIT = DurationUnit::class.asClassName() + private val DATA_SIZE = DataSize::class.asClassName() + private val DATA_SIZE_UNIT = DataSizeUnit::class.asClassName() + private val PMODULE = PModule::class.asClassName() + private val PCLASS = PClass::class.asClassName() + private val REGEX = Regex::class.asClassName() + private val URI = URI::class.asClassName() + private val VERSION = Version::class.asClassName() + + private const val PROPERTY_PREFIX: String = "org.pkl.config.java.mapper." + } + + val output: Map + get() { + return mapOf(kotlinFileName to kotlinFile, propertyFileName to propertiesFile) + } + + private val propertyFileName: String + get() = + "resources/META-INF/org/pkl/config/java/mapper/classes/${moduleSchema.moduleName}.properties" + + private val propertiesFile: String + get() { + val props = Properties() + props["$PROPERTY_PREFIX${moduleSchema.moduleClass.qualifiedName}"] = + moduleSchema.moduleClass.toKotlinPoetName().reflectionName() + for (pClass in moduleSchema.classes.values) { + props["$PROPERTY_PREFIX${pClass.qualifiedName}"] = + pClass.toKotlinPoetName().reflectionName() + } + return StringWriter() + .apply { props.store(this, "Kotlin mappings for Pkl module `${moduleSchema.moduleName}`") } + .toString() + } + + val kotlinFileName: String + get() = buildString { + append("kot") + append("lin/${relativeOutputPathFor(moduleSchema.moduleName)}") + } + + val kotlinFile: String + get() { + if (moduleSchema.moduleUri.scheme == "pkl") { + throw KotlinCodeGeneratorException( + "Cannot generate Kotlin code for a Pkl standard library module (`${moduleSchema.moduleUri}`)." + ) + } + + val pModuleClass = moduleSchema.moduleClass + + val hasProperties = pModuleClass.properties.any { !it.value.isHidden } + val isGenerateClass = hasProperties || pModuleClass.isOpen || pModuleClass.isAbstract + val moduleType = + if (isGenerateClass) { + generateTypeSpec(pModuleClass, moduleSchema) + } else { + generateObjectSpec(pModuleClass) + } + + for (pClass in moduleSchema.classes.values) { + moduleType.addType(generateTypeSpec(pClass, moduleSchema).ensureSerializable().build()) + } + + // generate append method for module classes w/o parent class; reuse in subclasses and nested + // classes + val isGenerateAppendPropertyMethod = + // check if we can inherit someone else's append method + pModuleClass.superclass!!.info == PClassInfo.Module && + // check if anyone is (potentially) going to use our append method + (pModuleClass.isOpen || + pModuleClass.isAbstract || + (hasProperties && !moduleType.modifiers.contains(KModifier.DATA)) || + moduleType.typeSpecs.any { !it.modifiers.contains(KModifier.DATA) }) + + if (isGenerateAppendPropertyMethod) { + val appendPropertyMethodModifier = + if (pModuleClass.isOpen || pModuleClass.isAbstract) { + // alternative is `@JvmStatic protected` + // (`protected` alone isn't sufficient as of Kotlin 1.6) + KModifier.PUBLIC + } else KModifier.PRIVATE + if (isGenerateClass) { + moduleType.addType( + TypeSpec.companionObjectBuilder() + .addFunction( + appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build() + ) + .build() + ) + } else { // kotlin object + moduleType.addFunction( + appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build() + ) + } + } + + val moduleName = moduleSchema.moduleName + val index = moduleName.lastIndexOf(".") + val packageName = if (index == -1) "" else moduleName.substring(0, index) + val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() } + + val fileSpec = FileSpec.builder(packageName, moduleTypeName).indent(options.indent) + + for (typeAlias in moduleSchema.typeAliases.values) { + if (typeAlias.aliasedType is PType.Alias) { + // generate top-level type alias (Kotlin doesn't support nested type aliases) + fileSpec.addTypeAlias(generateTypeAliasSpec(typeAlias).build()) + } else { + val stringLiterals = mutableSetOf() + if (CodeGeneratorUtils.isRepresentableAsEnum(typeAlias.aliasedType, stringLiterals)) { + // generate nested enum class + moduleType.addType(generateEnumTypeSpec(typeAlias, stringLiterals).build()) + } else { + // generate top-level type alias (Kotlin doesn't support nested type aliases) + fileSpec.addTypeAlias(generateTypeAliasSpec(typeAlias).build()) + } + } + } + + fileSpec.addType(moduleType.build()) + return fileSpec.build().toString() + } + + private fun relativeOutputPathFor(moduleName: String): String { + val nameParts = moduleName.split(".") + val dirPath = nameParts.dropLast(1).joinToString("/") + val fileName = nameParts.last().replaceFirstChar { it.titlecaseChar() } + return if (dirPath.isEmpty()) { + "$fileName.kt" + } else { + "$dirPath/$fileName.kt" + } + } + + private fun generateObjectSpec(pClass: PClass): TypeSpec.Builder { + val builder = TypeSpec.objectBuilder(pClass.toKotlinPoetName()) + val docComment = pClass.docComment + if (docComment != null && options.generateKdoc) { + builder.addKdoc(renderAsKdoc(docComment)) + } + return builder + } + + private fun generateTypeSpec(pClass: PClass, schema: ModuleSchema): TypeSpec.Builder { + val isModuleClass = pClass == schema.moduleClass + val kotlinPoetClassName = pClass.toKotlinPoetName() + val superclass = + pClass.superclass?.takeIf { it.info != PClassInfo.Typed && it.info != PClassInfo.Module } + val superProperties = superclass?.allProperties?.filterValues { !it.isHidden } ?: mapOf() + val properties = pClass.properties.filterValues { !it.isHidden } + val allProperties = superProperties + properties + + fun PClass.Property.isRegex(): Boolean = + (this.type as? PType.Class)?.pClass?.info == PClassInfo.Regex + + val containRegexProperty = properties.values.any { it.isRegex() } + + fun generateConstructor(): FunSpec { + val builder = FunSpec.constructorBuilder() + for ((name, property) in allProperties) { + builder.addParameter(name, property.type.toKotlinPoetName()) + } + return builder.build() + } + + fun generateCopyMethod(parameters: Map, isOverride: Boolean): FunSpec { + val methodBuilder = FunSpec.builder("copy").returns(kotlinPoetClassName) + + if (isOverride) { + methodBuilder.addModifiers(KModifier.OVERRIDE) + } + if (pClass.isOpen || pClass.isAbstract) { + methodBuilder.addModifiers(KModifier.OPEN) + } + + for ((name, property) in parameters) { + val paramBuilder = ParameterSpec.builder(name, property.type.toKotlinPoetName()) + if (!isOverride) { + paramBuilder.defaultValue("this.%N", name) + } + methodBuilder.addParameter(paramBuilder.build()) + } + + val codeBuilder = CodeBlock.builder().add("return %T(", kotlinPoetClassName) + var firstProperty = true + for (name in allProperties.keys) { + if (firstProperty) { + codeBuilder.add("%N", name) + firstProperty = false + } else { + codeBuilder.add(", %N", name) + } + } + codeBuilder.add(")\n") + + return methodBuilder.addCode(codeBuilder.build()).build() + } + + // besides generating copy method for current class, + // override copy methods inherited from parent classes + fun generateCopyMethods(typeBuilder: TypeSpec.Builder) { + var prevParameterCount = Int.MAX_VALUE + for (currClass in generateSequence(pClass) { it.superclass }) { + if (currClass.isAbstract) continue + + val currParameters = currClass.allProperties.filter { !it.value.isHidden } + + // avoid generating multiple methods with same no. of parameters + if (currParameters.size < prevParameterCount) { + val isOverride = currClass !== pClass || superclass != null && properties.isEmpty() + typeBuilder.addFunction(generateCopyMethod(currParameters, isOverride)) + prevParameterCount = currParameters.size + } + } + } + + fun generateEqualsMethod(): FunSpec { + val builder = + FunSpec.builder("equals") + .addModifiers(KModifier.OVERRIDE) + .addParameter("other", ANY_NULL) + .returns(BOOLEAN) + .addStatement("if (this === other) return true") + // generating this.javaClass instead of class literal avoids a SpotBugs warning + .addStatement("if (this.javaClass != other?.javaClass) return false") + .addStatement("other as %T", kotlinPoetClassName) + + for ((propertyName, property) in allProperties) { + val accessor = if (property.isRegex()) "%N.pattern" else "%N" + builder.addStatement( + "if (this.$accessor != other.$accessor) return false", + propertyName, + propertyName + ) + } + + builder.addStatement("return true") + return builder.build() + } + + fun generateHashCodeMethod(): FunSpec { + val builder = + FunSpec.builder("hashCode") + .addModifiers(KModifier.OVERRIDE) + .returns(INT) + .addStatement("var result = 1") + + for (propertyName in allProperties.keys) { + // use Objects.hashCode() because Kotlin's Any?.hashCode() + // doesn't work for platform types (will get NPE if null) + builder.addStatement( + "result = 31 * result + %T.hashCode(this.%N)", + Objects::class, + propertyName + ) + } + + builder.addStatement("return result") + return builder.build() + } + + fun generateToStringMethod(): FunSpec { + val builder = FunSpec.builder("toString").addModifiers(KModifier.OVERRIDE).returns(STRING) + + var builderSize = 50 + val appendBuilder = CodeBlock.builder() + for (propertyName in allProperties.keys) { + builderSize += 50 + appendBuilder.addStatement( + "appendProperty(builder, %S, this.%N)", + propertyName, + propertyName + ) + } + + builder + .addStatement("val builder = %T(%L)", STRING_BUILDER, builderSize) + .addStatement( + // generate `::class.java.simpleName` instead of `::class.simpleName` + // to avoid making user code depend on kotlin-reflect + "builder.append(%T::class.java.simpleName).append(\" {\")", + kotlinPoetClassName + ) + .addCode(appendBuilder.build()) + // not using %S here because it generates `"\n" + "{"` + // with a line break in the generated code after `+` + .addStatement("builder.append(\"\\n}\")") + .addStatement("return builder.toString()") + + return builder.build() + } + + fun generateDeprecation( + annotations: Collection, + addAnnotation: (AnnotationSpec) -> Unit + ) { + annotations + .firstOrNull { it.classInfo == PClassInfo.Deprecated } + ?.let { deprecation -> + val builder = AnnotationSpec.builder(Deprecated::class) + (deprecation["message"] as String?)?.let { builder.addMember("message = %S", it) } + addAnnotation(builder.build()) + } + } + + fun generateProperty(propertyName: String, property: PClass.Property): PropertySpec { + val typeName = property.type.toKotlinPoetName() + val builder = PropertySpec.builder(propertyName, typeName).initializer("%L", propertyName) + + generateDeprecation(property.annotations) { builder.addAnnotation(it) } + + val docComment = property.docComment + if (docComment != null && options.generateKdoc) { + builder.addKdoc(renderAsKdoc(docComment)) + } + if (propertyName in superProperties) { + builder.addModifiers(KModifier.OVERRIDE) + } + if (pClass.isOpen || pClass.isAbstract) { + builder.addModifiers(KModifier.OPEN) + } + + return builder.build() + } + + fun generateSpringBootAnnotations(builder: TypeSpec.Builder) { + builder.addAnnotation( + ClassName("org.springframework.boot.context.properties", "ConstructorBinding") + ) + + if (isModuleClass) { + builder.addAnnotation( + ClassName("org.springframework.boot.context.properties", "ConfigurationProperties") + ) + } else { + // not very efficient to repeat computing module property base types for every class + val modulePropertiesWithMatchingType = + schema.moduleClass.allProperties.values.filter { property -> + var propertyType = property.type + while (propertyType is PType.Constrained || propertyType is PType.Nullable) { + if (propertyType is PType.Constrained) { + propertyType = propertyType.baseType + } else if (propertyType is PType.Nullable) { + propertyType = propertyType.baseType + } + } + propertyType is PType.Class && propertyType.pClass == pClass + } + if (modulePropertiesWithMatchingType.size == 1) { + // exactly one module property has this type -> make it available for direct injection + // (potential improvement: make type available for direct injection if it occurs exactly + // once in property tree) + builder.addAnnotation( + AnnotationSpec.builder( + ClassName("org.springframework.boot.context.properties", "ConfigurationProperties") + ) + // use "value" instead of "prefix" to entice JavaPoet to generate a single-line + // annotation + // that can easily be filtered out by JavaCodeGeneratorTest.`spring boot config` + .addMember("%S", modulePropertiesWithMatchingType.first().simpleName) + .build() + ) + } + } + } + + fun generateRegularClass(): TypeSpec.Builder { + val builder = TypeSpec.classBuilder(kotlinPoetClassName) + + if (options.generateSpringBootConfig) { + generateSpringBootAnnotations(builder) + } + + builder.primaryConstructor(generateConstructor()) + + val docComment = pClass.docComment + if (docComment != null && options.generateKdoc) { + builder.addKdoc(renderAsKdoc(docComment)) + } + + if (pClass.isAbstract) { + builder.addModifiers(KModifier.ABSTRACT) + } else if (pClass.isOpen) { + builder.addModifiers(KModifier.OPEN) + } + + superclass?.let { superclass -> + val superclassName = superclass.toKotlinPoetName() + builder.superclass(superclassName) + for (propertyName in superProperties.keys) { + builder.addSuperclassConstructorParameter(propertyName) + } + } + + for ((name, property) in properties) { + builder.addProperty(generateProperty(name, property)) + } + + generateCopyMethods(builder) + + builder + .addFunction(generateEqualsMethod()) + .addFunction(generateHashCodeMethod()) + .addFunction(generateToStringMethod()) + + return builder + } + + fun generateDataClass(): TypeSpec.Builder { + val builder = TypeSpec.classBuilder(kotlinPoetClassName).addModifiers(KModifier.DATA) + + if (options.generateSpringBootConfig) { + generateSpringBootAnnotations(builder) + } + + builder.primaryConstructor(generateConstructor()) + + generateDeprecation(pClass.annotations) { builder.addAnnotation(it) } + + val docComment = pClass.docComment + if (docComment != null && options.generateKdoc) { + builder.addKdoc(renderAsKdoc(docComment)) + } + + for ((name, property) in properties) { + builder.addProperty(generateProperty(name, property)) + } + + // Regex requires special approach when compared to another Regex + // So we need to override `.equals` method even for kotlin's `data class`es if + // any of the properties is of Regex type + if (containRegexProperty) { + builder.addFunction(generateEqualsMethod()) + } + + return builder + } + + return if (superclass == null && !pClass.isAbstract && !pClass.isOpen) generateDataClass() + else generateRegularClass() + } + + private fun TypeSpec.Builder.ensureSerializable(): TypeSpec.Builder { + if (!options.implementSerializable) { + return this + } + + if (!this.superinterfaces.containsKey(java.io.Serializable::class.java.asTypeName())) { + this.addSuperinterface(java.io.Serializable::class.java) + } + + var useExistingCompanionBuilder = false + val companionBuilder = + this.typeSpecs + .find { it.isCompanion } + ?.let { + useExistingCompanionBuilder = true + it.toBuilder(TypeSpec.Kind.OBJECT) + } + ?: TypeSpec.companionObjectBuilder() + + if (!companionBuilder.propertySpecs.any { it.name == "serialVersionUID" }) + companionBuilder.addProperty( + PropertySpec.builder( + "serialVersionUID", + Long::class.java, + KModifier.PRIVATE, + KModifier.CONST + ) + .initializer("0L") + .build() + ) + + if (!useExistingCompanionBuilder) { + this.addType(companionBuilder.build()) + } + + return this + } + + private fun generateEnumTypeSpec( + typeAlias: TypeAlias, + stringLiterals: Set + ): TypeSpec.Builder { + val enumConstantToPklNames = + stringLiterals + .groupingBy { literal -> + CodeGeneratorUtils.toEnumConstantName(literal) + ?: throw KotlinCodeGeneratorException( + "Cannot generate Kotlin enum class for Pkl type alias `${typeAlias.displayName}` " + + "because string literal type \"$literal\" cannot be converted to a valid enum constant name." + ) + } + .reduce { enumConstantName, firstLiteral, secondLiteral -> + throw KotlinCodeGeneratorException( + "Cannot generate Kotlin enum class for Pkl type alias `${typeAlias.displayName}` " + + "because string literal types \"$firstLiteral\" and \"$secondLiteral\" " + + "would both be converted to enum constant name `$enumConstantName`." + ) + } + + val builder = + TypeSpec.enumBuilder(typeAlias.simpleName) + .primaryConstructor( + FunSpec.constructorBuilder().addParameter("value", String::class).build() + ) + .addProperty(PropertySpec.builder("value", String::class).initializer("value").build()) + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.OVERRIDE) + .addStatement("return value") + .build() + ) + for ((enumConstantName, pklName) in enumConstantToPklNames) { + builder.addEnumConstant( + enumConstantName, + TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", pklName).build() + ) + } + + return builder + } + + private fun generateTypeAliasSpec(typeAlias: TypeAlias): TypeAliasSpec.Builder { + val builder = + TypeAliasSpec.builder(typeAlias.simpleName, typeAlias.aliasedType.toKotlinPoetName()) + for (typeParameter in typeAlias.typeParameters) { + builder.addTypeVariable( + TypeVariableName(typeParameter.name, typeParameter.variance.toKotlinPoet()) + ) + } + + val docComment = typeAlias.docComment + if (docComment != null && options.generateKdoc) { + builder.addKdoc(renderAsKdoc(docComment)) + } + + return builder + } + + private fun TypeParameter.Variance.toKotlinPoet(): KModifier? = + when (this) { + TypeParameter.Variance.COVARIANT -> KModifier.OUT + TypeParameter.Variance.CONTRAVARIANT -> KModifier.IN + else -> null + } + + // do the minimum work necessary to avoid kotlin compile errors + // generating idiomatic KDoc would require parsing doc comments, converting member links, etc. + private fun renderAsKdoc(docComment: String): String = docComment + + private fun appendPropertyMethod() = + FunSpec.builder("appendProperty") + .addParameter("builder", STRING_BUILDER) + .addParameter("name", STRING) + .addParameter("value", ANY_NULL) + .addStatement("builder.append(\"\\n \").append(name).append(\" = \")") + .addStatement("val lines = value.toString().split(\"\\n\")") + .addStatement("builder.append(lines[0])") + .beginControlFlow("for (i in 1..lines.lastIndex)") + .addStatement("builder.append(\"\\n \").append(lines[i])") + .endControlFlow() + + private fun PClass.toKotlinPoetName(): ClassName { + val index = moduleName.lastIndexOf(".") + val packageName = if (index == -1) "" else moduleName.substring(0, index) + val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() } + return if (isModuleClass) { + ClassName(packageName, moduleTypeName) + } else { + ClassName(packageName, moduleTypeName, simpleName) + } + } + + private fun TypeAlias.toKotlinPoetName(): ClassName { + val index = moduleName.lastIndexOf(".") + val packageName = if (index == -1) "" else moduleName.substring(0, index) + + return when { + aliasedType is PType.Alias -> { + // Kotlin type generated for [this] is a top-level type alias + ClassName(packageName, simpleName) + } + CodeGeneratorUtils.isRepresentableAsEnum(aliasedType, null) -> { + if (isStandardLibraryMember) { + throw KotlinCodeGeneratorException( + "Standard library typealias `${qualifiedName}` is not supported by Kotlin code generator." + + " If you think this is an omission, please let us know." + ) + } + // Kotlin type generated for [this] is a nested enum class + val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() } + ClassName(packageName, moduleTypeName, simpleName) + } + else -> { + // Kotlin type generated for [this] is a top-level type alias + ClassName(packageName, simpleName) + } + } + } + + private fun PType.toKotlinPoetName(): TypeName = + when (this) { + PType.UNKNOWN -> ANY_NULL + PType.NOTHING -> NOTHING + is PType.StringLiteral -> STRING + is PType.Class -> { + // if in doubt, spell it out + when (val classInfo = pClass.info) { + PClassInfo.Any -> ANY_NULL + PClassInfo.Typed, + PClassInfo.Dynamic -> ANY + PClassInfo.Boolean -> BOOLEAN + PClassInfo.String -> STRING + // seems more useful to generate `Double` than `kotlin.Number` + PClassInfo.Number -> DOUBLE + PClassInfo.Int -> LONG + PClassInfo.Float -> DOUBLE + PClassInfo.Duration -> DURATION + PClassInfo.DataSize -> DATA_SIZE + PClassInfo.Pair -> + KOTLIN_PAIR.parameterizedBy( + if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName(), + if (typeArguments.isEmpty()) ANY_NULL else typeArguments[1].toKotlinPoetName() + ) + PClassInfo.Collection -> + COLLECTION.parameterizedBy( + if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName() + ) + PClassInfo.List, + PClassInfo.Listing -> + LIST.parameterizedBy( + if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName() + ) + PClassInfo.Set -> + SET.parameterizedBy( + if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName() + ) + PClassInfo.Map, + PClassInfo.Mapping -> + MAP.parameterizedBy( + if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName(), + if (typeArguments.isEmpty()) ANY_NULL else typeArguments[1].toKotlinPoetName() + ) + PClassInfo.Module -> PMODULE + PClassInfo.Class -> PCLASS + PClassInfo.Regex -> REGEX + PClassInfo.Version -> VERSION + else -> + when { + !classInfo.isStandardLibraryClass -> pClass.toKotlinPoetName() + else -> + throw KotlinCodeGeneratorException( + "Standard library class `${pClass.qualifiedName}` is not supported by Kotlin code generator. " + + "If you think this is an omission, please let us know." + ) + } + } + } + is PType.Nullable -> baseType.toKotlinPoetName().copy(nullable = true) + is PType.Constrained -> baseType.toKotlinPoetName() + is PType.Alias -> + when (typeAlias.qualifiedName) { + "pkl.base#NonNull" -> ANY + // Not currently generating Kotlin unsigned types + // because it's not clear if the benefits outweigh the drawbacks: + // - breaking change + // - Kotlin unsigned types aren't intended for domain modeling + // - diverts from Java code generator + // - doesn't increase safety + // - range already checked on Pkl side + // - conversion to signed type doesn't perform range check + "pkl.base#Int8" -> BYTE + "pkl.base#Int16", + "pkl.base#UInt8" -> SHORT + "pkl.base#Int32", + "pkl.base#UInt16" -> INT + "pkl.base#UInt", + "pkl.base#UInt32" -> LONG + "pkl.base#DurationUnit" -> DURATION_UNIT + "pkl.base#DataSizeUnit" -> DATA_SIZE_UNIT + "pkl.base#Uri" -> URI + else -> { + val className = typeAlias.toKotlinPoetName() + when { + typeAlias.typeParameters.isEmpty() -> className + typeArguments.isEmpty() -> { + // no type arguments provided for a type alias with type parameters -> fill in + // `Any?` (equivalent of `unknown`) + val typeArgs = Array(typeAlias.typeParameters.size) { ANY_NULL } + className.parameterizedBy(*typeArgs) + } + else -> className.parameterizedBy(*typeArguments.toKotlinPoet()) + } + } + } + is PType.Function -> + throw KotlinCodeGeneratorException( + "Pkl function types are not supported by the Kotlin code generator." + ) + is PType.Union -> + if (CodeGeneratorUtils.isRepresentableAsString(this)) STRING + else + throw KotlinCodeGeneratorException( + "Pkl union types are not supported by the Kotlin code generator." + ) + + // occurs on RHS of generic type aliases + is PType.TypeVariable -> TypeVariableName(typeParameter.name) + else -> throw AssertionError("Encountered unexpected PType subclass: $this") + } + + private fun List.toKotlinPoet(): Array = + map { it.toKotlinPoetName() }.toTypedArray() +} diff --git a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/Main.kt b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/Main.kt new file mode 100644 index 00000000..efac3676 --- /dev/null +++ b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/Main.kt @@ -0,0 +1,96 @@ +/** + * 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. + */ +@file:JvmName("Main") + +package org.pkl.codegen.kotlin + +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.path +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.cli.cliMain +import org.pkl.commons.cli.commands.ModulesCommand +import org.pkl.commons.toPath +import org.pkl.core.Release + +/** Main method for the Kotlin code generator CLI. */ +internal fun main(args: Array) { + cliMain { PklKotlinCodegenCommand().main(args) } +} + +class PklKotlinCodegenCommand : + ModulesCommand( + name = "pkl-codegen-kotlin", + helpLink = Release.current().documentation().homepage(), + ) { + + private val defaults = CliKotlinCodeGeneratorOptions(CliBaseOptions(), "".toPath()) + + private val outputDir: Path by + option( + names = arrayOf("-o", "--output-dir"), + metavar = "", + help = "The directory where generated source code is placed." + ) + .path() + .default(defaults.outputDir) + + private val indent: String by + option( + names = arrayOf("--indent"), + metavar = "", + help = "The characters to use for indenting generated source code." + ) + .default(defaults.indent) + + private val generateKdoc: Boolean by + option( + names = arrayOf("--generate-kdoc"), + help = + "Whether to generate Kdoc based on doc comments " + + "for Pkl modules, classes, and properties." + ) + .flag() + + private val generateSpringboot: Boolean by + option( + names = arrayOf("--generate-spring-boot"), + help = "Whether to generate config classes for use with Spring boot." + ) + .flag() + + private val implementSerializable: Boolean by + option( + names = arrayOf("--implement-serializable"), + help = "Whether to make generated classes implement java.io.Serializable" + ) + .flag() + + override fun run() { + val options = + CliKotlinCodeGeneratorOptions( + base = baseOptions.baseOptions(modules, projectOptions), + outputDir = outputDir, + indent = indent, + generateKdoc = generateKdoc, + generateSpringBootConfig = generateSpringboot, + implementSerializable = implementSerializable + ) + CliKotlinCodeGenerator(options).run() + } +} diff --git a/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorTest.kt b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorTest.kt new file mode 100644 index 00000000..be56f0ae --- /dev/null +++ b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorTest.kt @@ -0,0 +1,162 @@ +/** + * 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.codegen.kotlin + +import java.nio.file.Path +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.readString + +class CliKotlinCodeGeneratorTest { + @Test + fun `module inheritance`(@TempDir tempDir: Path) { + val module1 = + PklModule( + "org.mod1", + """ + open module org.mod1 + + pigeon: Person + + class Person { + name: String + age: Int + } + """ + ) + + val module2 = + PklModule( + "org.mod2", + """ + module org.mod2 + + extends "mod1.pkl" + + parrot: Person + """ + ) + + val module1File = module1.writeToDisk(tempDir.resolve("org/mod1.pkl")) + val module2File = module2.writeToDisk(tempDir.resolve("org/mod2.pkl")) + val outputDir = tempDir.resolve("output") + + val generator = + CliKotlinCodeGenerator( + CliKotlinCodeGeneratorOptions( + CliBaseOptions(listOf(module1File.toUri(), module2File.toUri())), + outputDir + ) + ) + + generator.run() + + val module1KotlinFile = outputDir.resolve("kotlin/org/Mod1.kt") + assertThat(module1KotlinFile).exists() + + val module2KotlinFile = outputDir.resolve("kotlin/org/Mod2.kt") + assertThat(module2KotlinFile).exists() + + assertContains( + """ + open class Mod1( + open val pigeon: Person + ) { + """ + .trimIndent(), + module1KotlinFile.readString() + ) + + assertContains( + """ + class Mod2( + pigeon: Mod1.Person, + val parrot: Mod1.Person + ) : Mod1(pigeon) { + """ + .trimIndent(), + module2KotlinFile.readString() + ) + } + + @Test + fun `class name clashes`(@TempDir tempDir: Path) { + val module1 = + PklModule( + "org.mod1", + """ + module org.mod1 + + class Person { + name: String + } + """ + ) + + val module2 = + PklModule( + "org.mod2", + """ + module org.mod2 + + import "mod1.pkl" + + person1: mod1.Person + person2: Person + + class Person { + age: Int + } + """ + ) + + val module1PklFile = module1.writeToDisk(tempDir.resolve("org/mod1.pkl")) + val module2PklFile = module2.writeToDisk(tempDir.resolve("org/mod2.pkl")) + val outputDir = tempDir.resolve("output") + + val generator = + CliKotlinCodeGenerator( + CliKotlinCodeGeneratorOptions( + CliBaseOptions(listOf(module1PklFile.toUri(), module2PklFile.toUri())), + outputDir + ) + ) + + generator.run() + + val module2KotlinFile = outputDir.resolve("kotlin/org/Mod2.kt") + assertContains( + """ + data class Mod2( + val person1: Mod1.Person, + val person2: Person + ) + """ + .trimIndent(), + module2KotlinFile.readString() + ) + } + + private fun assertContains(part: String, code: String) { + val trimmedPart = part.trim().trimMargin() + if (!code.contains(trimmedPart)) { + // check for equality to get better error output (ide diff dialog) + assertThat(code).isEqualTo(trimmedPart) + } + } +} diff --git a/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/InMemoryKotlinCompiler.kt b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/InMemoryKotlinCompiler.kt new file mode 100644 index 00000000..1673441d --- /dev/null +++ b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/InMemoryKotlinCompiler.kt @@ -0,0 +1,87 @@ +/** + * 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.codegen.kotlin + +import javax.script.ScriptEngineManager +import javax.script.ScriptException +import kotlin.reflect.KClass +import kotlin.text.RegexOption.MULTILINE +import kotlin.text.RegexOption.UNIX_LINES +import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback + +class CompilationFailedException(msg: String?, cause: Throwable? = null) : + RuntimeException(msg, cause) + +object InMemoryKotlinCompiler { + init { + // prevent "Unable to load JNA library" warning + setIdeaIoUseFallback() + } + + // Implementation notes: + // * all [sourceFiles] are currently combined into a single file + // * implementation makes assumptions about structure of generated source files + fun compile(sourceFiles: Map): Map> { + fun String.findClasses( + prefix: String = "", + nameGroup: Int = 2, + bodyGroup: Int = 4, + regex: String = + "^(data |open |enum )?class\\s+(\\w+) *(\\([^)]*\\))?.*$\\n((^ .*\\n|^$\\n)*)", + transform: (String, String) -> Sequence> = { name, body -> + sequenceOf(Pair(name, prefix + name)) + body.findClasses("$prefix$name.") + } + ): Sequence> = // (simpleName1, qualifiedName1), ... + Regex(regex, setOf(MULTILINE, UNIX_LINES)).findAll(this).flatMap { + transform(it.groupValues[nameGroup], it.groupValues[bodyGroup].trimIndent()) + } + + fun String.findOuterObjects(): Sequence> = // (simpleName, qualifiedName) + findClasses("", 1, 2, "^object\\s+(\\w+).*$\n((^ .*$\n|^$\n)*)") { name, body -> + body.findClasses("$name.") + } + + val (importLines, remainder) = + sourceFiles.entries + .flatMap { (_, text) -> text.lines() } + .partition { it.startsWith("import") } + val importBlock = importLines.sorted().distinct() + val (packageLines, code) = remainder.partition { it.startsWith("package") } + val packageBlock = packageLines.distinct() + assert( + packageBlock.size <= 1 + ) // everything is in the same package and/or there is no package line + val sourceText = listOf(packageBlock, importBlock, code).flatten().joinToString("\n") + + val (simpleNames, qualifiedNames) = + sourceText.findClasses().plus(sourceText.findOuterObjects()).unzip() + val instrumentation = + "listOf>(${qualifiedNames.joinToString(",") { "$it::class" }})" + + // create new engine for each compilation + // (otherwise we sometimes get kotlin compiler exceptions) + val engine = ScriptEngineManager().getEngineByExtension("kts")!! + val classes = + try { + @Suppress("UNCHECKED_CAST") + engine.eval("$sourceText\n\n$instrumentation") as List> + } catch (e: ScriptException) { + throw CompilationFailedException(e.message, e) + } + + return simpleNames.zip(classes).toMap() + } +} diff --git a/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/KotlinCodeGeneratorTest.kt b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/KotlinCodeGeneratorTest.kt new file mode 100644 index 00000000..e8fd0319 --- /dev/null +++ b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/KotlinCodeGeneratorTest.kt @@ -0,0 +1,1579 @@ +/** + * 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.codegen.kotlin + +import java.io.* +import java.nio.file.Path +import kotlin.reflect.KClass +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.memberProperties +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.pkl.core.* +import org.pkl.core.util.IoUtils + +class KotlinCodeGeneratorTest { + companion object { + // according to: + // https://github.com/JetBrains/kotlin/blob/master/core/descriptors/ + // src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java + internal val kotlinKeywords = + setOf( + "package", + "as", + "typealias", + "class", + "this", + "super", + "val", + "var", + "fun", + "for", + "null", + "true", + "false", + "is", + "in", + "throw", + "return", + "break", + "continue", + "object", + "if", + "try", + "else", + "while", + "do", + "when", + "interface", + "typeof" + ) + + private val simpleClass by lazy { + compileKotlinCode( + generateKotlinCode( + """ + module my.mod + + open class Simple { + str: String + list: List + } + """ + ) + ) + .getValue("Simple") + } + + private val propertyTypesKotlinCode by lazy { + generateKotlinCode( + """ + module my.mod + + open class PropertyTypes { + boolean: Boolean + int: Int + float: Float + string: String + duration: Duration + durationUnit: DurationUnit + dataSize: DataSize + dataSizeUnit: DataSizeUnit + nullable: String? + nullable2: String? + pair: Pair + pair2: Pair + coll: Collection + coll2: Collection + list: List + list2: List + set: Set + set2: Set + map: Map + map2: Map + container: Mapping + container2: Mapping + other: Other + regex: Regex + any: Any + nonNull: NonNull + enum: Direction + } + + open class Other { + name: String + } + + typealias Direction = "north"|"east"|"south"|"west" + """ + ) + } + + private val propertyTypesClasses by lazy { compileKotlinCode(propertyTypesKotlinCode) } + + private fun generateKotlinCode( + pklCode: String, + generateKdoc: Boolean = false, + generateSpringBootConfig: Boolean = false, + implementSerializable: Boolean = false + ): String { + + val module = Evaluator.preconfigured().evaluateSchema(ModuleSource.text(pklCode)) + + val generator = + KotlinCodeGenerator( + module, + KotlinCodegenOptions( + generateKdoc = generateKdoc, + generateSpringBootConfig = generateSpringBootConfig, + implementSerializable = implementSerializable + ) + ) + return generator.kotlinFile + } + + private fun compileKotlinCode(kotlinCode: String): Map> = + InMemoryKotlinCompiler.compile(mapOf("my/Mod.kt" to kotlinCode)) + + private fun assertCompilesSuccessfully(sourceText: String) = compileKotlinCode(sourceText) + } + + @Test + fun testEquals() { + val ctor = simpleClass.constructors.first() + val instance1 = ctor.call("foo", listOf(1, 2, 3)) + val instance2 = ctor.call("foo", listOf(1, 2, 3)) + val instance3 = ctor.call("foo", listOf(1, 3, 2)) + val instance4 = ctor.call("bar", listOf(1, 2, 3)) + + assertThat(instance1).isEqualTo(instance1) + assertThat(instance1).isEqualTo(instance2) + assertThat(instance2).isEqualTo(instance1) + + assertThat(instance3).isNotEqualTo(instance1) + assertThat(instance4).isNotEqualTo(instance1) + } + + @Test + fun testHashCode() { + val ctor = simpleClass.constructors.first() + val instance1 = ctor.call("foo", listOf(1, 2, 3)) + val instance2 = ctor.call("foo", listOf(1, 2, 3)) + val instance3 = ctor.call("foo", listOf(1, 3, 2)) + val instance4 = ctor.call("bar", listOf(1, 2, 3)) + + assertThat(instance1.hashCode()).isEqualTo(instance1.hashCode()) + assertThat(instance1.hashCode()).isEqualTo(instance2.hashCode()) + assertThat(instance3.hashCode()).isNotEqualTo(instance1.hashCode()) + assertThat(instance4.hashCode()).isNotEqualTo(instance1.hashCode()) + } + + @Test + fun testToString() { + val (_, propertyTypes) = instantiateOtherAndPropertyTypes() + + assertEqualTo( + """ + PropertyTypes { + boolean = true + int = 42 + float = 42.3 + string = string + duration = 5.min + durationUnit = min + dataSize = 3.gb + dataSizeUnit = gb + nullable = idea + nullable2 = null + pair = (1, 2) + pair2 = (pigeon, Other { + name = pigeon + }) + coll = [1, 2] + coll2 = [Other { + name = pigeon + }, Other { + name = pigeon + }] + list = [1, 2] + list2 = [Other { + name = pigeon + }, Other { + name = pigeon + }] + set = [1, 2] + set2 = [Other { + name = pigeon + }] + map = {1=one, 2=two} + map2 = {one=Other { + name = pigeon + }, two=Other { + name = pigeon + }} + container = {1=one, 2=two} + container2 = {one=Other { + name = pigeon + }, two=Other { + name = pigeon + }} + other = Other { + name = pigeon + } + regex = (i?)\w* + any = Other { + name = pigeon + } + nonNull = Other { + name = pigeon + } + enum = north + } + """, + propertyTypes.toString() + ) + } + + @Test + fun `deprecated property with message`() { + val javaCode = + generateKotlinCode( + """ + class ClassWithDeprecatedProperty { + @Deprecated { message = "property deprecation message" } + deprecatedProperty: Int = 1337 + } + """ + .trimIndent() + ) + val expectedPropertyDef = + """ + | data class ClassWithDeprecatedProperty( + | @Deprecated(message = "property deprecation message") + | val deprecatedProperty: Long + """ + .trimMargin() + assertThat(javaCode).contains(expectedPropertyDef) + } + + @Test + fun `deprecated class with message`() { + val javaCode = + generateKotlinCode( + """ + @Deprecated { message = "class deprecation message" } + class DeprecatedClass { + propertyOfDeprecatedClass: Int = 42 + } + """ + .trimIndent() + ) + val expected = + """ + | @Deprecated(message = "class deprecation message") + | data class DeprecatedClass( + """ + .trimMargin() + assertThat(javaCode).contains(expected) + } + + @Test + fun `deprecated module class with message`() { + val javaCode = + generateKotlinCode( + """ + @Deprecated{ message = "module class deprecation message" } + module DeprecatedModule + + propertyInDeprecatedModuleClass : Int = 42 + """ + .trimIndent() + ) + val expected = + """ + |@Deprecated(message = "module class deprecation message") + |data class DeprecatedModule( + """ + .trimMargin() + assertThat(javaCode).contains(expected) + } + + @Test + fun `deprecated property`() { + val javaCode = + generateKotlinCode( + """ + class ClassWithDeprecatedProperty { + @Deprecated + deprecatedProperty: Int = 1337 + } + """ + .trimIndent() + ) + val expectedPropertyDef = + """ + | data class ClassWithDeprecatedProperty( + | @Deprecated + | val deprecatedProperty: Long + """ + .trimMargin() + assertThat(javaCode).contains(expectedPropertyDef) + } + + @Test + fun `deprecated class`() { + val javaCode = + generateKotlinCode( + """ + @Deprecated + class DeprecatedClass { + propertyOfDeprecatedClass: Int = 42 + } + """ + .trimIndent() + ) + val expected = + """ + | @Deprecated + | data class DeprecatedClass( + """ + .trimMargin() + assertThat(javaCode).contains(expected) + } + + @Test + fun `deprecated module class`() { + val javaCode = + generateKotlinCode( + """ + @Deprecated + module DeprecatedModule + + propertyInDeprecatedModuleClass : Int = 42 + """ + .trimIndent() + ) + val expected = + """ + |@Deprecated + |data class DeprecatedModule( + """ + .trimMargin() + assertThat(javaCode).contains(expected) + } + + @Test + fun properties() { + val (other, propertyTypes) = instantiateOtherAndPropertyTypes() + + assertThat(readProperty(other, "name")).isEqualTo("pigeon") + assertThat(readProperty(propertyTypes, "boolean")).isEqualTo(true) + assertThat(readProperty(propertyTypes, "int")).isEqualTo(42L) + assertThat(readProperty(propertyTypes, "float")).isEqualTo(42.3) + assertThat(readProperty(propertyTypes, "string")).isEqualTo("string") + assertThat(readProperty(propertyTypes, "duration")) + .isEqualTo(Duration(5.0, DurationUnit.MINUTES)) + assertThat(readProperty(propertyTypes, "dataSize")) + .isEqualTo(DataSize(3.0, DataSizeUnit.GIGABYTES)) + assertThat(readProperty(propertyTypes, "nullable")).isEqualTo("idea") + assertThat(readProperty(propertyTypes, "nullable2")).isEqualTo(null) + assertThat(readProperty(propertyTypes, "list")).isEqualTo(listOf(1, 2)) + assertThat(readProperty(propertyTypes, "list2")).isEqualTo(listOf(other, other)) + assertThat(readProperty(propertyTypes, "set")).isEqualTo(setOf(1, 2)) + assertThat(readProperty(propertyTypes, "set2")).isEqualTo(setOf(other)) + assertThat(readProperty(propertyTypes, "map")).isEqualTo(mapOf(1 to "one", 2 to "two")) + assertThat(readProperty(propertyTypes, "map2")).isEqualTo(mapOf("one" to other, "two" to other)) + assertThat(readProperty(propertyTypes, "container")).isEqualTo(mapOf(1 to "one", 2 to "two")) + assertThat(readProperty(propertyTypes, "container2")) + .isEqualTo(mapOf("one" to other, "two" to other)) + assertThat(readProperty(propertyTypes, "other")).isEqualTo(other) + assertThat(readProperty(propertyTypes, "regex")).isInstanceOf(Regex::class.java) + assertThat(readProperty(propertyTypes, "any")).isEqualTo(other) + assertThat(readProperty(propertyTypes, "nonNull")).isEqualTo(other) + } + + private fun readProperty(receiver: Any, name: String): Any? { + val property = receiver.javaClass.kotlin.memberProperties.find { it.name == name }!! + return property.invoke(receiver) + } + + @Test + fun `properties 2`() { + assertEqualTo( + IoUtils.readClassPathResourceAsString(javaClass, "PropertyTypes.kotlin"), + propertyTypesKotlinCode + ) + } + + @Test + fun `enum constant names`() { + val cases = + listOf( + "camelCasedName" to "CAMEL_CASED_NAME", + "hyphenated-name" to "HYPHENATED_NAME", + "EnQuad\u2000EmSpace\u2003IdeographicSpace\u3000" to "EN_QUAD_EM_SPACE_IDEOGRAPHIC_SPACE_", + "ᾊᾨ" to "ᾊᾨ", + "0-digit" to "_0_DIGIT", + "digit-1" to "DIGIT_1", + "42" to "_42", + "àœü" to "ÀŒÜ", + "日本-つくば" to "日本_つくば" + ) + val kotlinCode = + generateKotlinCode( + """ + module my.mod + typealias MyTypeAlias = ${cases.joinToString(" | ") { "\"${it.first}\"" }} + """ + .trimIndent() + ) + val kotlinClass = compileKotlinCode(kotlinCode).getValue("MyTypeAlias").java + + assertThat(kotlinClass.enumConstants.size) + .isEqualTo(cases.size) // make sure zip doesn't drop cases + + assertAll( + "generated enum constants have correct names", + kotlinClass.declaredFields.zip(cases) { field, (_, kotlinName) -> + { + assertThat(field.name).isEqualTo(kotlinName) + Unit + } + } + ) + + assertAll( + "toString() returns Pkl name", + kotlinClass.enumConstants.zip(cases) { enumConstant, (pklName, _) -> + { + assertThat(enumConstant.toString()).isEqualTo(pklName) + Unit + } + } + ) + } + + @Test + fun `conflicting enum constant names`() { + val pklCode = + """ + module my.mod + typealias MyTypeAlias = "foo-bar" | "foo bar" + """ + .trimIndent() + + val exception = assertThrows { generateKotlinCode(pklCode) } + assertThat(exception) + .hasMessageContainingAll("both be converted to enum constant name", "FOO_BAR") + } + + @Test + fun `empty enum constant name`() { + val pklCode = + """ + module my.mod + typealias MyTypeAlias = "foo" | "" | "bar" + """ + .trimIndent() + + val exception = assertThrows { generateKotlinCode(pklCode) } + assertThat(exception).hasMessageContaining("cannot be converted") + } + + @Test + fun `inconvertible enum constant name`() { + val pklCode = + """ + module my.mod + typealias MyTypeAlias = "foo" | "✅" | "bar" + """ + .trimIndent() + + val exception = assertThrows { generateKotlinCode(pklCode) } + assertThat(exception).hasMessageContainingAll("✅", "cannot be converted") + } + + @Test + fun `data class`() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + class Person { + name: String + age: Int + hobbies: List + friends: Map + sibling: Person? + } + """ + ) + + assertEqualTo( + """ + package my + + import kotlin.Long + import kotlin.String + import kotlin.collections.List + import kotlin.collections.Map + + object Mod { + data class Person( + val name: String, + val age: Long, + val hobbies: List, + val friends: Map, + val sibling: Person? + ) + } + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `recursive types`() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + open class Foo { + other: Int + bar: Bar + } + open class Bar { + foo: Foo + other: String + } + """ + ) + + assertContains( + """ + | open class Foo( + | open val other: Long, + | open val bar: Bar + | ) + """ + .trimMargin(), + kotlinCode + ) + + assertContains( + """ + | open class Bar( + | open val foo: Foo, + | open val other: String + | ) + """ + .trimMargin(), + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun inheritance() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + open class Foo { + one: Int + } + open class None extends Foo {} + open class Bar extends None { + two: String + } + class Baz extends Bar { + three: Duration + } + """ + ) + + assertContains( + """ + | open class Foo( + | open val one: Long + | ) + """ + .trimMargin(), + kotlinCode + ) + + assertContains( + """ + | open class None( + | one: Long + | ) : Foo(one) + """ + .trimMargin(), + kotlinCode + ) + + assertContains( + """ + | open class Bar( + | one: Long, + | open val two: String + | ) : None(one) + """ + .trimMargin(), + kotlinCode + ) + + assertEqualTo( + IoUtils.readClassPathResourceAsString(javaClass, "Inheritance.kotlin"), + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun keywords() { + val props = kotlinKeywords.joinToString("\n") { "`$it`: Int" } + + val fooClass = + compileKotlinCode( + generateKotlinCode( + """ + module my.mod + + class Foo { + $props + } + """ + ) + ) + .getValue("Foo") + + assertThat(fooClass.declaredMemberProperties.map { it.name }).hasSameElementsAs(kotlinKeywords) + } + + @Test + fun `module properties`() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + pigeon: Person + parrot: Person + + class Person { + name: String + } + """ + ) + + assertEqualTo( + """ + package my + + import kotlin.String + + data class Mod( + val pigeon: Person, + val parrot: Person + ) { + data class Person( + val name: String + ) + } + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `simple module name`() { + val kotlinCode = + generateKotlinCode( + """ + module mod + + pigeon: Person + parrot: Person + + class Person { + name: String + } + """ + ) + + assertEqualTo( + """ + import kotlin.String + + data class Mod( + val pigeon: Person, + val parrot: Person + ) { + data class Person( + val name: String + ) + } + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `hidden properties`() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + hidden pigeon1: String + parrot1: String + + class Persons { + hidden pigeon2: String + parrot2: String + } + """ + ) + + assertThat(kotlinCode) + .doesNotContain("pigeon1: String") + .contains("parrot1: String") + .doesNotContain("pigeon2: String") + .contains("parrot2: String") + } + + @Test + fun kdoc() { + val kotlinCode = + generateKotlinCode( + """ + /// module comment. + /// *emphasized* `code`. + module my.mod + + /// module property comment. + /// *emphasized* `code`. + pigeon: Person + + /// class comment. + /// *emphasized* `code`. + open class Product { + /// class property comment. + /// *emphasized* `code`. + price: String + } + + /// class comment. + /// *emphasized* `code`. + class Person { + /// class property comment. + /// *emphasized* `code`. + name: String + } + + /// type alias comment. + /// *emphasized* `code`. + typealias Email = String(contains("@")) + """, + generateKdoc = true + ) + + assertEqualTo(IoUtils.readClassPathResourceAsString(javaClass, "Kdoc.kotlin"), kotlinCode) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `kdoc 2`() { + val kotlinCode = + generateKotlinCode( + """ + /// module comment. + /// *emphasized* `code`. + module my.mod + + class Product + """, + generateKdoc = true + ) + + assertEqualTo( + """ + package my + + /** + * module comment. + * *emphasized* `code`. + */ + object Mod { + data class Product + } + """, + kotlinCode + ) + } + + @Test + fun `pkl_base type aliases`() { + val kotlinCode = + generateKotlinCode( + """ + module mod + + uint8: UInt8 + uint16: UInt16 + uint32: UInt32 + uint: UInt + int8: Int8 + int16: Int16 + int32: Int32 + uri: Uri + + pair: Pair + list: List + set: Set + map: Map + listing: Listing + mapping: Mapping + nullable: UInt16? + + class Foo { + uint8: UInt8 + uint16: UInt16 + uint32: UInt32 + uint: UInt + int8: Int8 + int16: Int16 + int32: Int32 + uri: Uri + list: List + } + """ + ) + + assertEqualTo( + """ + import java.net.URI + import kotlin.Byte + import kotlin.Int + import kotlin.Long + import kotlin.Pair + import kotlin.Short + import kotlin.collections.List + import kotlin.collections.Map + import kotlin.collections.Set + + data class Mod( + val uint8: Short, + val uint16: Int, + val uint32: Long, + val uint: Long, + val int8: Byte, + val int16: Short, + val int32: Int, + val uri: URI, + val pair: Pair, + val list: List, + val set: Set, + val map: Map, + val listing: List, + val mapping: Map, + val nullable: Int? + ) { + data class Foo( + val uint8: Short, + val uint16: Int, + val uint32: Long, + val uint: Long, + val int8: Byte, + val int16: Short, + val int32: Int, + val uri: URI, + val list: List + ) + } + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `user defined type aliases`() { + val kotlinCode = + generateKotlinCode( + """ + module mod + + typealias Simple = String + typealias Constrained = String(length >= 3) + typealias Parameterized = List + typealias Recursive1 = Parameterized(nonEmpty) + typealias Recursive2 = List + + simple: Simple + constrained: Constrained + parameterized: Parameterized + recursive1: Recursive1 + recursive2: Recursive2 + + class Foo { + simple: Simple + constrained: Constrained + parameterized: Parameterized + recursive1: Recursive1 + recursive2: Recursive2 + } + """ + ) + + assertEqualTo( + """ + import kotlin.Long + import kotlin.String + import kotlin.collections.List + + typealias Simple = String + + typealias Constrained = String + + typealias Parameterized = List + + typealias Recursive1 = Parameterized + + typealias Recursive2 = List + + data class Mod( + val simple: Simple, + val constrained: Constrained, + val parameterized: Parameterized, + val recursive1: Recursive1, + val recursive2: Recursive2 + ) { + data class Foo( + val simple: Simple, + val constrained: Constrained, + val parameterized: Parameterized, + val recursive1: Recursive1, + val recursive2: Recursive2 + ) + } + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun genericTypeAliases() { + val kotlinCode = + generateKotlinCode( + """ + module mod + + class Person { name: String } + + typealias List2 = List + typealias Map2 = Map + typealias StringMap = Map + typealias MMap = Map + + res1: List2 + res2: List2> + res3: Map2 + res4: StringMap + res5: MMap + + res6: List2 + res7: Map2 + res8: StringMap + res9: MMap + + class Foo { + res1: List2 + res2: List2> + res3: Map2 + res4: StringMap + res5: MMap + + res6: List2 + res7: Map2 + res8: StringMap + res9: MMap + } + """ + ) + + assertContains( + """ + |data class Mod( + | val res1: List2, + | val res2: List2>, + | val res3: Map2, + | val res4: StringMap, + | val res5: MMap, + | val res6: List2, + | val res7: Map2, + | val res8: StringMap, + | val res9: MMap + """, + kotlinCode + ) + + assertContains( + """ + | data class Foo( + | val res1: List2, + | val res2: List2>, + | val res3: Map2, + | val res4: StringMap, + | val res5: MMap, + | val res6: List2, + | val res7: Map2, + | val res8: StringMap, + | val res9: MMap + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `union of string literals`() { + val kotlinCode = + generateKotlinCode(""" + module mod + + x: "Pigeon"|"Barn Owl"|"Parrot" + """) + + assertContains( + """ + data class Mod( + val x: String + ) + """ + .trimIndent(), + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `other union type`() { + val e = + assertThrows { + generateKotlinCode(""" + module mod + + x: "Pigeon"|Int|"Parrot" + """) + } + assertThat(e).hasMessageContaining("Pkl union types are not supported") + } + + @Test + fun `stringy type`() { + val kotlinCode = + generateKotlinCode( + """ + module mod + + v1: "RELEASE" + v2: "RELEASE"|String + v3: String|"RELEASE" + v4: "RELEASE"|String|"LATEST" + v5: Version|String|"LATEST" + v6: (Version|String)|("LATEST"|String) + + typealias Version = "RELEASE"|String|"LATEST" + """ + ) + + assertContains("v1: String", kotlinCode) + assertContains("v2: String", kotlinCode) + assertContains("v3: String", kotlinCode) + assertContains("v4: String", kotlinCode) + assertContains("v5: String", kotlinCode) + assertContains("v6: String", kotlinCode) + } + + @Test + fun `stringy type alias`() { + val kotlinCode = + generateKotlinCode( + """ + module mod + + typealias Version1 = "RELEASE"|String + typealias Version2 = String|"RELEASE" + typealias Version3 = "RELEASE"|String|"LATEST" + typealias Version4 = Version3|String|"LATEST" // ideally wouldn't be inlined + typealias Version5 = (Version4|String)|("LATEST"|String) + typealias Version6 = Version5 // not inlined + """ + ) + + assertContains("typealias Version1 = String", kotlinCode) + assertContains("typealias Version2 = String", kotlinCode) + assertContains("typealias Version3 = String", kotlinCode) + assertContains("typealias Version4 = String", kotlinCode) + assertContains("typealias Version5 = String", kotlinCode) + assertContains("typealias Version6 = Version5", kotlinCode) + } + + @Test + fun `spring boot config`() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + server: Server + + class Server { + port: Int + urls: Listing + } + """, + generateSpringBootConfig = true + ) + + // not worthwhile to add spring & spring boot dependency just so that this test can compile + // their annotations + val kotlinCodeWithoutSpringAnnotations = + kotlinCode + .lines() + .filterNot { it.contains("ConstructorBinding") || it.contains("ConfigurationProperties") } + .joinToString("\n") + + assertContains( + """ + |@ConstructorBinding + |@ConfigurationProperties + |data class Mod( + | val server: Server + """, + kotlinCode + ) + + assertContains( + """ + | @ConstructorBinding + | @ConfigurationProperties("server") + | data class Server( + | val port: Long, + | val urls: List + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCodeWithoutSpringAnnotations) + } + + @Test + fun `import module`(@TempDir tempDir: Path) { + val library = + PklModule( + "library", + """ + module library + + class Person { name: String; age: Int } + + pigeon: Person + """ + .trimIndent() + ) + + val client = + PklModule( + "client", + """ + module client + + import "library.pkl" + + lib: library + + parrot: library.Person + """ + .trimIndent() + ) + + val kotlinSourceFiles = generateKotlinFiles(tempDir, library, client) + val kotlinClientCode = + kotlinSourceFiles.entries.find { (fileName, _) -> fileName.endsWith("Client.kt") }!!.value + + assertContains( + """ + |data class Client( + | val lib: Library, + | val parrot: Library.Person + |) + """, + kotlinClientCode + ) + + assertDoesNotThrow { InMemoryKotlinCompiler.compile(kotlinSourceFiles) } + } + + @Test + fun `extend module`(@TempDir tempDir: Path) { + val base = + PklModule( + "base", + """ + open module base + + open class Person { name: String } + + pigeon: Person + """ + .trimIndent() + ) + + val derived = + PklModule( + "derived", + """ + module derived + extends "base.pkl" + + class Person2 extends Person { age: Int } + + person1: Person + person2: Person2 + """ + .trimIndent() + ) + + val kotlinSourceFiles = generateKotlinFiles(tempDir, base, derived) + val kotlinDerivedCode = + kotlinSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.kt") }!!.value + + assertContains( + """ + |class Derived( + | pigeon: Base.Person, + | val person1: Base.Person, + | val person2: Person2 + |) : Base(pigeon) + """, + kotlinDerivedCode + ) + + assertContains( + """ + | class Person2( + | name: String, + | val age: Long + | ) : Base.Person(name) + """, + kotlinDerivedCode + ) + + assertDoesNotThrow { InMemoryKotlinCompiler.compile(kotlinSourceFiles) } + } + + @Test + fun `empty module`() { + val kotlinCode = generateKotlinCode("module mod") + assertEqualTo("object Mod", kotlinCode) + } + + @Test + fun `extend module that only contains type aliases`(@TempDir tempDir: Path) { + val moduleOne = + PklModule( + "base", + """ + abstract module base + + typealias Version = "LATEST"|String + """ + .trimIndent() + ) + + val moduleTwo = + PklModule( + "derived", + """ + module derived + + extends "base.pkl" + + v: Version = "1.2.3" + """ + .trimIndent() + ) + + val kotlinSourceFiles = generateKotlinFiles(tempDir, moduleOne, moduleTwo) + val kotlinDerivedCode = + kotlinSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.kt") }!!.value + + assertContains( + """ + |class Derived( + | val v: Version + |) : Base() + """, + kotlinDerivedCode + ) + + assertDoesNotThrow { InMemoryKotlinCompiler.compile(kotlinSourceFiles) } + } + + @Test + fun `generated properties files`(@TempDir tempDir: Path) { + val pklModule = + PklModule( + "Mod.pkl", + """ + module org.pkl.Mod + + foo: Foo + + bar: Bar + + class Foo { + prop: String + } + + class Bar { + prop: Int + } + """ + .trimIndent() + ) + val generated = generateFiles(tempDir, pklModule) + val expectedPropertyFile = + "resources/META-INF/org/pkl/config/java/mapper/classes/org.pkl.Mod.properties" + assertThat(generated).containsKey(expectedPropertyFile) + val propertyFileContents = generated[expectedPropertyFile]!! + assertThat(propertyFileContents) + .contains("org.pkl.config.java.mapper.org.pkl.Mod\\#ModuleClass=org.pkl.Mod") + assertThat(propertyFileContents) + .contains("org.pkl.config.java.mapper.org.pkl.Mod\\#Foo=org.pkl.Mod\$Foo") + assertThat(propertyFileContents) + .contains("org.pkl.config.java.mapper.org.pkl.Mod\\#Bar=org.pkl.Mod\$Bar") + } + + @Test + fun `generates serializable classes`() { + val kotlinCode = + generateKotlinCode( + """ + module mod + + class BigStruct { + boolean: Boolean + int: Int + float: Float + string: String + duration: Duration + dataSize: DataSize + pair: Pair + pair2: Pair + coll: Collection + coll2: Collection + list: List + list2: List + set: Set + set2: Set + map: Map + map2: Map + container: Mapping + container2: Mapping + other: SmallStruct + regex: Regex + nonNull: NonNull + enum: Direction + } + + class SmallStruct { + name: String + } + + typealias Direction = "north"|"east"|"south"|"west" + """, + implementSerializable = true + ) + + assertContains(": Serializable", kotlinCode) + assertContains("private const val serialVersionUID: Long = 0L", kotlinCode) + + val classes = compileKotlinCode(kotlinCode) + val enumClass = classes.getValue("Direction") + val enumValue = enumClass.java.enumConstants.first() + + val smallStructCtor = classes.getValue("SmallStruct").constructors.first() + val smallStruct = smallStructCtor.call("pigeon") + + val bigStructCtor = classes.getValue("BigStruct").constructors.first() + val bigStruct = + bigStructCtor.call( + true, + 42L, + 42.3, + "string", + Duration(5.0, DurationUnit.MINUTES), + DataSize(3.0, DataSizeUnit.GIGABYTES), + kotlin.Pair(1, 2), + kotlin.Pair("pigeon", smallStruct), + listOf(1, 2, 3), + listOf(smallStruct, smallStruct), + listOf(1, 2, 3), + listOf(smallStruct, smallStruct), + setOf(1, 2, 3), + setOf(smallStruct, smallStruct), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to smallStruct, "two" to smallStruct), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to smallStruct, "two" to smallStruct), + smallStruct, + Regex("(i?)\\w*"), + smallStruct, + enumValue + ) + + fun confirmSerDe(instance: Any) { + var restoredInstance: Any? = null + + assertThatCode { + // serialize + val baos = ByteArrayOutputStream() + val oos = ObjectOutputStream(baos) + oos.writeObject(instance) + oos.flush() + + // deserialize + val bais = ByteArrayInputStream(baos.toByteArray()) + val ois = + object : ObjectInputStream(bais) { + override fun resolveClass(desc: ObjectStreamClass?): Class<*> { + return Class.forName(desc!!.name, false, instance.javaClass.classLoader) + } + } + restoredInstance = ois.readObject() + } + .doesNotThrowAnyException() + + assertThat(restoredInstance!!).isEqualTo(instance) + } + + confirmSerDe(enumValue) + confirmSerDe(smallStruct) + confirmSerDe(bigStruct) + } + + private fun generateFiles(tempDir: Path, vararg pklModules: PklModule): Map { + val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) } + val evaluator = Evaluator.preconfigured() + return pklFiles.fold(mapOf()) { acc, pklFile -> + val pklSchema = evaluator.evaluateSchema(ModuleSource.path(pklFile)) + acc + KotlinCodeGenerator(pklSchema, KotlinCodegenOptions()).output + } + } + + private fun generateKotlinFiles( + tempDir: Path, + vararg pklModules: PklModule + ): Map { + val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) } + val evaluator = Evaluator.preconfigured() + return pklFiles.fold(mapOf()) { acc, pklFile -> + val pklSchema = evaluator.evaluateSchema(ModuleSource.path(pklFile)) + val generator = KotlinCodeGenerator(pklSchema, KotlinCodegenOptions()) + acc + arrayOf(generator.kotlinFileName to generator.kotlinFile) + } + } + + private fun instantiateOtherAndPropertyTypes(): kotlin.Pair { + val otherCtor = propertyTypesClasses.getValue("Other").constructors.first() + val other = otherCtor.call("pigeon") + + val enumClass = propertyTypesClasses.getValue("Direction").java + val enumValue = enumClass.enumConstants.first() + + val propertyTypesCtor = propertyTypesClasses.getValue("PropertyTypes").constructors.first() + val propertyTypes = + propertyTypesCtor.call( + true, + 42, + 42.3, + "string", + Duration(5.0, DurationUnit.MINUTES), + DurationUnit.MINUTES, + DataSize(3.0, DataSizeUnit.GIGABYTES), + DataSizeUnit.GIGABYTES, + "idea", + null, + kotlin.Pair(1, 2), + kotlin.Pair("pigeon", other), + listOf(1, 2), + listOf(other, other), + listOf(1, 2), + listOf(other, other), + setOf(1, 2), + setOf(other), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to other, "two" to other), + mapOf(1 to "one", 2 to "two"), + mapOf("one" to other, "two" to other), + other, + Regex("(i?)\\w*"), + other, + other, + enumValue + ) + + return other to propertyTypes + } + + private fun assertContains(part: String, code: String) { + val trimmedPart = part.trim().trimMargin() + if (!code.contains(trimmedPart)) { + // check for equality to get better error output (ide diff dialog) + assertThat(code).isEqualTo(trimmedPart) + } + } + + private fun assertEqualTo(expectedCode: String, actualCode: String) { + assertThat(actualCode.trim()).isEqualTo(expectedCode.trimIndent().trim()) + } +} diff --git a/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/PklModule.kt b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/PklModule.kt new file mode 100644 index 00000000..1d3409ec --- /dev/null +++ b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/PklModule.kt @@ -0,0 +1,26 @@ +/** + * 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.codegen.kotlin + +import java.nio.file.Path +import org.pkl.commons.createParentDirectories +import org.pkl.commons.writeString + +data class PklModule(val name: String, val content: String) { + fun writeToDisk(path: Path): Path { + return path.createParentDirectories().writeString(content) + } +} diff --git a/pkl-codegen-kotlin/src/test/resources/META-INF/services/javax.script.ScriptEngineFactory b/pkl-codegen-kotlin/src/test/resources/META-INF/services/javax.script.ScriptEngineFactory new file mode 100644 index 00000000..f8f59003 --- /dev/null +++ b/pkl-codegen-kotlin/src/test/resources/META-INF/services/javax.script.ScriptEngineFactory @@ -0,0 +1 @@ +org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory \ No newline at end of file diff --git a/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/Inheritance.kotlin b/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/Inheritance.kotlin new file mode 100644 index 00000000..d560090a --- /dev/null +++ b/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/Inheritance.kotlin @@ -0,0 +1,159 @@ +package my + +import java.util.Objects +import kotlin.Any +import kotlin.Boolean +import kotlin.Int +import kotlin.Long +import kotlin.String +import kotlin.text.StringBuilder +import org.pkl.core.Duration + +object Mod { + private fun appendProperty( + builder: StringBuilder, + name: String, + value: Any? + ) { + builder.append("\n ").append(name).append(" = ") + val lines = value.toString().split("\n") + builder.append(lines[0]) + for (i in 1..lines.lastIndex) { + builder.append("\n ").append(lines[i]) + } + } + + open class Foo( + open val one: Long + ) { + open fun copy(one: Long = this.one): Foo = Foo(one) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (this.javaClass != other?.javaClass) return false + other as Foo + if (this.one != other.one) return false + return true + } + + override fun hashCode(): Int { + var result = 1 + result = 31 * result + Objects.hashCode(this.one) + return result + } + + override fun toString(): String { + val builder = StringBuilder(100) + builder.append(Foo::class.java.simpleName).append(" {") + appendProperty(builder, "one", this.one) + builder.append("\n}") + return builder.toString() + } + } + + open class None( + one: Long + ) : Foo(one) { + open override fun copy(one: Long): None = None(one) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (this.javaClass != other?.javaClass) return false + other as None + if (this.one != other.one) return false + return true + } + + override fun hashCode(): Int { + var result = 1 + result = 31 * result + Objects.hashCode(this.one) + return result + } + + override fun toString(): String { + val builder = StringBuilder(100) + builder.append(None::class.java.simpleName).append(" {") + appendProperty(builder, "one", this.one) + builder.append("\n}") + return builder.toString() + } + } + + open class Bar( + one: Long, + open val two: String + ) : None(one) { + open fun copy(one: Long = this.one, two: String = this.two): Bar = Bar(one, two) + + open override fun copy(one: Long): Bar = Bar(one, two) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (this.javaClass != other?.javaClass) return false + other as Bar + if (this.one != other.one) return false + if (this.two != other.two) return false + return true + } + + override fun hashCode(): Int { + var result = 1 + result = 31 * result + Objects.hashCode(this.one) + result = 31 * result + Objects.hashCode(this.two) + return result + } + + override fun toString(): String { + val builder = StringBuilder(150) + builder.append(Bar::class.java.simpleName).append(" {") + appendProperty(builder, "one", this.one) + appendProperty(builder, "two", this.two) + builder.append("\n}") + return builder.toString() + } + } + + class Baz( + one: Long, + two: String, + val three: Duration + ) : Bar(one, two) { + fun copy( + one: Long = this.one, + two: String = this.two, + three: Duration = this.three + ): Baz = Baz(one, two, three) + + override fun copy(one: Long, two: String): Baz = Baz(one, two, three) + + override fun copy(one: Long): Baz = Baz(one, two, three) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (this.javaClass != other?.javaClass) return false + other as Baz + if (this.one != other.one) return false + if (this.two != other.two) return false + if (this.three != other.three) return false + return true + } + + override fun hashCode(): Int { + var result = 1 + result = 31 * result + Objects.hashCode(this.one) + result = 31 * result + Objects.hashCode(this.two) + result = 31 * result + Objects.hashCode(this.three) + return result + } + + override fun toString(): String { + val builder = StringBuilder(200) + builder.append(Baz::class.java.simpleName).append(" {") + appendProperty(builder, "one", this.one) + appendProperty(builder, "two", this.two) + appendProperty(builder, "three", this.three) + builder.append("\n}") + return builder.toString() + } + } +} diff --git a/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/Kdoc.kotlin b/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/Kdoc.kotlin new file mode 100644 index 00000000..f1284de6 --- /dev/null +++ b/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/Kdoc.kotlin @@ -0,0 +1,89 @@ +package my + +import java.util.Objects +import kotlin.Any +import kotlin.Boolean +import kotlin.Int +import kotlin.String +import kotlin.text.StringBuilder + +/** + * type alias comment. + * *emphasized* `code`. + */ +typealias Email = String + +/** + * module comment. + * *emphasized* `code`. + */ +data class Mod( + /** + * module property comment. + * *emphasized* `code`. + */ + val pigeon: Person +) { + /** + * class comment. + * *emphasized* `code`. + */ + open class Product( + /** + * class property comment. + * *emphasized* `code`. + */ + open val price: String + ) { + open fun copy(price: String = this.price): Product = Product(price) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (this.javaClass != other?.javaClass) return false + other as Product + if (this.price != other.price) return false + return true + } + + override fun hashCode(): Int { + var result = 1 + result = 31 * result + Objects.hashCode(this.price) + return result + } + + override fun toString(): String { + val builder = StringBuilder(100) + builder.append(Product::class.java.simpleName).append(" {") + appendProperty(builder, "price", this.price) + builder.append("\n}") + return builder.toString() + } + } + + /** + * class comment. + * *emphasized* `code`. + */ + data class Person( + /** + * class property comment. + * *emphasized* `code`. + */ + val name: String + ) + + companion object { + private fun appendProperty( + builder: StringBuilder, + name: String, + value: Any? + ) { + builder.append("\n ").append(name).append(" = ") + val lines = value.toString().split("\n") + builder.append(lines[0]) + for (i in 1..lines.lastIndex) { + builder.append("\n ").append(lines[i]) + } + } + } +} diff --git a/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/PropertyTypes.kotlin b/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/PropertyTypes.kotlin new file mode 100644 index 00000000..a3bca435 --- /dev/null +++ b/pkl-codegen-kotlin/src/test/resources/org/pkl/codegen/kotlin/PropertyTypes.kotlin @@ -0,0 +1,239 @@ +package my + +import java.util.Objects +import kotlin.Any +import kotlin.Boolean +import kotlin.Double +import kotlin.Int +import kotlin.Long +import kotlin.Pair +import kotlin.String +import kotlin.collections.Collection +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.Set +import kotlin.text.Regex +import kotlin.text.StringBuilder +import org.pkl.core.DataSize +import org.pkl.core.DataSizeUnit +import org.pkl.core.Duration +import org.pkl.core.DurationUnit + +object Mod { + private fun appendProperty( + builder: StringBuilder, + name: String, + value: Any? + ) { + builder.append("\n ").append(name).append(" = ") + val lines = value.toString().split("\n") + builder.append(lines[0]) + for (i in 1..lines.lastIndex) { + builder.append("\n ").append(lines[i]) + } + } + + open class PropertyTypes( + open val boolean: Boolean, + open val int: Long, + open val float: Double, + open val string: String, + open val duration: Duration, + open val durationUnit: DurationUnit, + open val dataSize: DataSize, + open val dataSizeUnit: DataSizeUnit, + open val nullable: String?, + open val nullable2: String?, + open val pair: Pair, + open val pair2: Pair, + open val coll: Collection, + open val coll2: Collection, + open val list: List, + open val list2: List, + open val set: Set, + open val set2: Set, + open val map: Map, + open val map2: Map, + open val container: Map, + open val container2: Map, + open val other: Other, + open val regex: Regex, + open val any: Any?, + open val nonNull: Any, + open val enum: Direction + ) { + open fun copy( + boolean: Boolean = this.boolean, + int: Long = this.int, + float: Double = this.float, + string: String = this.string, + duration: Duration = this.duration, + durationUnit: DurationUnit = this.durationUnit, + dataSize: DataSize = this.dataSize, + dataSizeUnit: DataSizeUnit = this.dataSizeUnit, + nullable: String? = this.nullable, + nullable2: String? = this.nullable2, + pair: Pair = this.pair, + pair2: Pair = this.pair2, + coll: Collection = this.coll, + coll2: Collection = this.coll2, + list: List = this.list, + list2: List = this.list2, + set: Set = this.set, + set2: Set = this.set2, + map: Map = this.map, + map2: Map = this.map2, + container: Map = this.container, + container2: Map = this.container2, + other: Other = this.other, + regex: Regex = this.regex, + any: Any? = this.any, + nonNull: Any = this.nonNull, + enum: Direction = this.enum + ): PropertyTypes = PropertyTypes(boolean, int, float, string, duration, durationUnit, dataSize, + dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map, + map2, container, container2, other, regex, any, nonNull, enum) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (this.javaClass != other?.javaClass) return false + other as PropertyTypes + if (this.boolean != other.boolean) return false + if (this.int != other.int) return false + if (this.float != other.float) return false + if (this.string != other.string) return false + if (this.duration != other.duration) return false + if (this.durationUnit != other.durationUnit) return false + if (this.dataSize != other.dataSize) return false + if (this.dataSizeUnit != other.dataSizeUnit) return false + if (this.nullable != other.nullable) return false + if (this.nullable2 != other.nullable2) return false + if (this.pair != other.pair) return false + if (this.pair2 != other.pair2) return false + if (this.coll != other.coll) return false + if (this.coll2 != other.coll2) return false + if (this.list != other.list) return false + if (this.list2 != other.list2) return false + if (this.set != other.set) return false + if (this.set2 != other.set2) return false + if (this.map != other.map) return false + if (this.map2 != other.map2) return false + if (this.container != other.container) return false + if (this.container2 != other.container2) return false + if (this.other != other.other) return false + if (this.regex.pattern != other.regex.pattern) return false + if (this.any != other.any) return false + if (this.nonNull != other.nonNull) return false + if (this.enum != other.enum) return false + return true + } + + override fun hashCode(): Int { + var result = 1 + result = 31 * result + Objects.hashCode(this.boolean) + result = 31 * result + Objects.hashCode(this.int) + result = 31 * result + Objects.hashCode(this.float) + result = 31 * result + Objects.hashCode(this.string) + result = 31 * result + Objects.hashCode(this.duration) + result = 31 * result + Objects.hashCode(this.durationUnit) + result = 31 * result + Objects.hashCode(this.dataSize) + result = 31 * result + Objects.hashCode(this.dataSizeUnit) + result = 31 * result + Objects.hashCode(this.nullable) + result = 31 * result + Objects.hashCode(this.nullable2) + result = 31 * result + Objects.hashCode(this.pair) + result = 31 * result + Objects.hashCode(this.pair2) + result = 31 * result + Objects.hashCode(this.coll) + result = 31 * result + Objects.hashCode(this.coll2) + result = 31 * result + Objects.hashCode(this.list) + result = 31 * result + Objects.hashCode(this.list2) + result = 31 * result + Objects.hashCode(this.set) + result = 31 * result + Objects.hashCode(this.set2) + result = 31 * result + Objects.hashCode(this.map) + result = 31 * result + Objects.hashCode(this.map2) + result = 31 * result + Objects.hashCode(this.container) + result = 31 * result + Objects.hashCode(this.container2) + result = 31 * result + Objects.hashCode(this.other) + result = 31 * result + Objects.hashCode(this.regex) + result = 31 * result + Objects.hashCode(this.any) + result = 31 * result + Objects.hashCode(this.nonNull) + result = 31 * result + Objects.hashCode(this.enum) + return result + } + + override fun toString(): String { + val builder = StringBuilder(1400) + builder.append(PropertyTypes::class.java.simpleName).append(" {") + appendProperty(builder, "boolean", this.boolean) + appendProperty(builder, "int", this.int) + appendProperty(builder, "float", this.float) + appendProperty(builder, "string", this.string) + appendProperty(builder, "duration", this.duration) + appendProperty(builder, "durationUnit", this.durationUnit) + appendProperty(builder, "dataSize", this.dataSize) + appendProperty(builder, "dataSizeUnit", this.dataSizeUnit) + appendProperty(builder, "nullable", this.nullable) + appendProperty(builder, "nullable2", this.nullable2) + appendProperty(builder, "pair", this.pair) + appendProperty(builder, "pair2", this.pair2) + appendProperty(builder, "coll", this.coll) + appendProperty(builder, "coll2", this.coll2) + appendProperty(builder, "list", this.list) + appendProperty(builder, "list2", this.list2) + appendProperty(builder, "set", this.set) + appendProperty(builder, "set2", this.set2) + appendProperty(builder, "map", this.map) + appendProperty(builder, "map2", this.map2) + appendProperty(builder, "container", this.container) + appendProperty(builder, "container2", this.container2) + appendProperty(builder, "other", this.other) + appendProperty(builder, "regex", this.regex) + appendProperty(builder, "any", this.any) + appendProperty(builder, "nonNull", this.nonNull) + appendProperty(builder, "enum", this.enum) + builder.append("\n}") + return builder.toString() + } + } + + open class Other( + open val name: String + ) { + open fun copy(name: String = this.name): Other = Other(name) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (this.javaClass != other?.javaClass) return false + other as Other + if (this.name != other.name) return false + return true + } + + override fun hashCode(): Int { + var result = 1 + result = 31 * result + Objects.hashCode(this.name) + return result + } + + override fun toString(): String { + val builder = StringBuilder(100) + builder.append(Other::class.java.simpleName).append(" {") + appendProperty(builder, "name", this.name) + builder.append("\n}") + return builder.toString() + } + } + + enum class Direction( + val value: String + ) { + NORTH("north"), + + EAST("east"), + + SOUTH("south"), + + WEST("west"); + + override fun toString() = value + } +} diff --git a/pkl-commons-cli/gradle.lockfile b/pkl-commons-cli/gradle.lockfile new file mode 100644 index 00000000..6cac9f47 --- /dev/null +++ b/pkl-commons-cli/gradle.lockfile @@ -0,0 +1,37 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.github.ajalt.clikt:clikt-jvm:3.5.1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt:3.5.1=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,runtimeClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:22.3.1=default,runtimeClasspath,testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=default,runtimeClasspath,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=default,runtimeClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=default,runtimeClasspath,testRuntimeClasspath +empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-commons-cli/pkl-commons-cli.gradle.kts b/pkl-commons-cli/pkl-commons-cli.gradle.kts new file mode 100644 index 00000000..e24e1fef --- /dev/null +++ b/pkl-commons-cli/pkl-commons-cli.gradle.kts @@ -0,0 +1,28 @@ +plugins { + pklAllProjects + pklKotlinLibrary + pklPublishLibrary +} + +dependencies { + api(project(":pkl-core")) + api(libs.clikt) { + // force clikt to use our version of the kotlin stdlib + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-common") + } + + implementation(project(":pkl-commons")) + testImplementation(project(":pkl-commons-test")) +} + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-commons-cli") + description.set("Internal CLI utilities. NOT A PUBLIC API.") + } + } + } +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt new file mode 100644 index 00000000..d33f12e5 --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt @@ -0,0 +1,170 @@ +/** + * 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.commons.cli + +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.util.regex.Pattern +import org.pkl.core.module.ProjectDependenciesManager +import org.pkl.core.util.IoUtils + +/** Base options shared between CLI commands. */ +data class CliBaseOptions( + /** The source modules to evaluate. Relative URIs are resolved against [workingDir]. */ + private val sourceModules: List = listOf(), + + /** + * The URI patterns that determine which modules can be loaded and evaluated. Patterns are matched + * against the beginning of module URIs. At least one pattern needs to match for a module to be + * loadable. Both [sourceModules] and module imports are subject to this check. + */ + val allowedModules: List? = null, + + /** + * The URI patterns that determine which external resources can be read. Patterns are matched + * against the beginning of resource URIs. At least one pattern needs to match for a resource to + * be readable. + */ + val allowedResources: List? = null, + + /** + * The environment variables to set. Pkl code can read environment variables with + * `read("env:")`. + */ + val environmentVariables: Map? = null, + + /** + * The external properties to set. Pkl code can read external properties with + * `read("prop:")`. + */ + val externalProperties: Map? = null, + + /** + * The directories, ZIP archives, or JAR archives to search when resolving `modulepath:` URIs. + * Relative paths are resolved against [workingDir]. + */ + private val modulePath: List? = null, + + /** + * The base path that relative module paths passed as command-line arguments are resolved against. + */ + private val workingDir: Path = IoUtils.getCurrentWorkingDir(), + + /** + * The root directory for `file:` modules and resources. If non-null, access to file-based modules + * and resources is restricted to those located under [rootDir]. Any symlinks are resolved before + * this check is performed. + */ + private val rootDir: Path? = null, + + /** + * The Pkl settings file to use. A settings file is a Pkl module amending the `pkl.settings` + * standard library module. If `null`, `~/.pkl/settings.pkl` (if present) or the defaults + * specified in the `pkl:settings` standard library module are used. + */ + private val settings: URI? = null, + + /** + * The root directory of the project. The directory must contain a `PklProject` that amends the + * `pkl.Project` standard library module. + * + * If `null`, looks for a `PklProject` file in [workingDir], traversing up to [rootDir], or `/` if + * [rootDir] is `null`. + * + * This can be disabled with [noProject]. + */ + private val projectDir: Path? = null, + + /** + * The duration after which evaluation of a source module will be timed out. Note that a timeout + * is treated the same as a program error in that any subsequent source modules will not be + * evaluated. + */ + val timeout: Duration? = null, + + /** The cache directory for storing packages. */ + private val moduleCacheDir: Path? = null, + + /** Whether to disable the module cache. */ + val noCache: Boolean = false, + + /** Ignores any evaluator settings set in the PklProject file. */ + val omitProjectSettings: Boolean = false, + + /** Disables all behavior related to projects. */ + val noProject: Boolean = false, + + /** Tells whether to run the CLI in test mode. This is an internal option. */ + val testMode: Boolean = false, + + /** + * [X.509 certificates](https://en.wikipedia.org/wiki/X.509) in PEM format. + * + * Elements can either be a [Path] or a [java.io.InputStream]. Input streams are closed when + * [CliCommand] is initialized. + * + * If not empty, this determines the CA root certs used for all HTTPS requests. Warning: this + * affects the whole Java runtime, not just the Pkl API! + */ + val caCertificates: List = emptyList(), +) { + + companion object { + tailrec fun Path.getProjectFile(rootDir: Path?): Path? { + val candidate = resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME) + return when { + Files.exists(candidate) -> candidate + parent == null -> null + rootDir != null && !parent.startsWith(rootDir) -> null + else -> parent.getProjectFile(rootDir) + } + } + } + + /** [workingDir] after normalization. */ + val normalizedWorkingDir: Path = IoUtils.getCurrentWorkingDir().resolve(workingDir) + + /** [rootDir] after normalization. */ + val normalizedRootDir: Path? = rootDir?.let(normalizedWorkingDir::resolve) + + /** [sourceModules] after normalization. */ + val normalizedSourceModules: List = + sourceModules + .map { uri -> + if (uri.isAbsolute) uri else IoUtils.resolve(normalizedWorkingDir.toUri(), uri) + } + // sort modules to make cli output independent of source module order + .sorted() + + val normalizedSettingsModule: URI? = + settings?.let { uri -> + if (uri.isAbsolute) uri else IoUtils.resolve(normalizedWorkingDir.toUri(), uri) + } + + /** [modulePath] after normalization. */ + val normalizedModulePath: List? = modulePath?.map(normalizedWorkingDir::resolve) + + /** [moduleCacheDir] after normalization. */ + val normalizedModuleCacheDir: Path? = moduleCacheDir?.let(normalizedWorkingDir::resolve) + + /** The effective project directory, if exists. */ + val normalizedProjectFile: Path? by lazy { + projectDir?.resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME) + ?: normalizedWorkingDir.getProjectFile(rootDir) + } +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt new file mode 100644 index 00000000..df07952f --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt @@ -0,0 +1,208 @@ +/** + * 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.commons.cli + +import java.nio.file.Path +import java.util.regex.Pattern +import org.pkl.core.* +import org.pkl.core.module.ModuleKeyFactories +import org.pkl.core.module.ModuleKeyFactory +import org.pkl.core.module.ModulePathResolver +import org.pkl.core.project.Project +import org.pkl.core.resource.ResourceReader +import org.pkl.core.resource.ResourceReaders +import org.pkl.core.runtime.CertificateUtils +import org.pkl.core.settings.PklSettings +import org.pkl.core.util.IoUtils + +/** Building block for CLI commands. Configured programmatically to allow for embedding. */ +abstract class CliCommand(protected val cliOptions: CliBaseOptions) { + init { + if (cliOptions.caCertificates.isNotEmpty()) { + CertificateUtils.setupAllX509CertificatesGlobally(cliOptions.caCertificates) + } + } + + /** Runs this command. */ + fun run() { + if (cliOptions.testMode) { + IoUtils.setTestMode() + } + try { + doRun() + } catch (e: PklException) { + throw CliException(e.message!!) + } catch (e: CliException) { + throw e + } catch (e: Exception) { + throw CliBugException(e) + } + } + + /** + * Implements this command. May throw [PklException] or [CliException]. Any other thrown exception + * is treated as a bug. + */ + protected abstract fun doRun() + + /** The Pkl settings used by this command. */ + @Suppress("MemberVisibilityCanBePrivate") + protected val settings: PklSettings by lazy { + try { + if (cliOptions.normalizedSettingsModule != null) { + PklSettings.load(ModuleSource.uri(cliOptions.normalizedSettingsModule)) + } else { + PklSettings.loadFromPklHomeDir() + } + } catch (e: PklException) { + // do not use `errorRenderer` because it depends on `settings` + throw CliException(e.toString()) + } + } + + /** The Project used by this command. */ + protected val project: Project? by lazy { + if (cliOptions.noProject) { + return@lazy null + } + cliOptions.normalizedProjectFile?.let { loadProject(it) } + } + + protected fun loadProject(projectFile: Path): Project { + val securityManager = + SecurityManagers.standard( + cliOptions.allowedModules ?: SecurityManagers.defaultAllowedModules, + cliOptions.allowedResources ?: SecurityManagers.defaultAllowedResources, + SecurityManagers.defaultTrustLevels, + cliOptions.normalizedRootDir + ) + val envVars = cliOptions.environmentVariables ?: System.getenv() + val stackFrameTransformer = + if (IoUtils.isTestMode()) StackFrameTransformers.empty + else StackFrameTransformers.defaultTransformer + return Project.loadFromPath( + projectFile, + securityManager, + cliOptions.timeout, + stackFrameTransformer, + envVars + ) + } + + private val projectSettings: Project.EvaluatorSettings? by lazy { + if (cliOptions.omitProjectSettings) { + return@lazy null + } + project?.settings + } + + protected val allowedModules: List by lazy { + cliOptions.allowedModules + ?: projectSettings?.allowedModules ?: SecurityManagers.defaultAllowedModules + } + + protected val allowedResources: List by lazy { + cliOptions.allowedResources + ?: projectSettings?.allowedResources ?: SecurityManagers.defaultAllowedResources + } + + protected val rootDir: Path? by lazy { cliOptions.normalizedRootDir ?: projectSettings?.rootDir } + + protected val environmentVariables: Map by lazy { + cliOptions.environmentVariables ?: projectSettings?.env ?: System.getenv() + } + + protected val externalProperties: Map by lazy { + cliOptions.externalProperties ?: projectSettings?.externalProperties ?: emptyMap() + } + + protected val moduleCacheDir: Path? by lazy { + if (cliOptions.noCache) null + else + cliOptions.normalizedModuleCacheDir + ?: projectSettings?.let { settings -> + if (settings.isNoCache == true) null else settings.moduleCacheDir + } + ?: IoUtils.getDefaultModuleCacheDir() + } + + protected val modulePath: List by lazy { + cliOptions.normalizedModulePath ?: projectSettings?.modulePath ?: emptyList() + } + + protected val stackFrameTransformer: StackFrameTransformer by lazy { + if (cliOptions.testMode) { + StackFrameTransformers.empty + } else { + StackFrameTransformers.createDefault(settings) + } + } + + protected val securityManager: SecurityManager by lazy { + SecurityManagers.standard( + allowedModules, + allowedResources, + SecurityManagers.defaultTrustLevels, + rootDir + ) + } + + protected fun moduleKeyFactories(modulePathResolver: ModulePathResolver): List { + return buildList { + add(ModuleKeyFactories.standardLibrary) + add(ModuleKeyFactories.modulePath(modulePathResolver)) + add(ModuleKeyFactories.pkg) + add(ModuleKeyFactories.projectpackage) + addAll(ModuleKeyFactories.fromServiceProviders()) + add(ModuleKeyFactories.file) + add(ModuleKeyFactories.genericUrl) + } + } + + private fun resourceReaders(modulePathResolver: ModulePathResolver): List { + return buildList { + add(ResourceReaders.environmentVariable()) + add(ResourceReaders.externalProperty()) + add(ResourceReaders.modulePath(modulePathResolver)) + add(ResourceReaders.pkg()) + add(ResourceReaders.projectpackage()) + add(ResourceReaders.file()) + add(ResourceReaders.http()) + add(ResourceReaders.https()) + } + } + + /** + * Creates an [EvaluatorBuilder] preconfigured according to [cliOptions]. To avoid resource leaks, + * `ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)` must be called once the returned + * builder and evaluators built by it are no longer in use. + */ + protected fun evaluatorBuilder(): EvaluatorBuilder { + // indirectly closed by `ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)` + val modulePathResolver = ModulePathResolver(modulePath) + return EvaluatorBuilder.unconfigured() + .setStackFrameTransformer(stackFrameTransformer) + .apply { project?.let { setProjectDependencies(it.dependencies) } } + .setSecurityManager(securityManager) + .setExternalProperties(externalProperties) + .setEnvironmentVariables(environmentVariables) + .addModuleKeyFactories(moduleKeyFactories(modulePathResolver)) + .addResourceReaders(resourceReaders(modulePathResolver)) + .setLogger(Loggers.stdErr()) + .setTimeout(cliOptions.timeout) + .setModuleCacheDir(moduleCacheDir) + } +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliException.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliException.kt new file mode 100644 index 00000000..59c65f39 --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliException.kt @@ -0,0 +1,47 @@ +/** + * 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.commons.cli + +import org.pkl.commons.printStackTraceToString + +/** A CLI error to report back to users. */ +open class CliException( + /** + * The error message to report back to CLI users. The message is expected to be displayed as-is + * without any further enrichment. As such the message should be comprehensive and designed with + * the CLI user in mind. + */ + message: String, + + /** The process exit code to use. */ + val exitCode: Int = 1 +) : RuntimeException(message) { + + override fun toString(): String = message!! +} + +/** An unexpected CLI error classified as bug. */ +class CliBugException( + /** The cause for the bug. */ + private val theCause: Exception, + + /** The process exit code to use. */ + exitCode: Int = 1 +) : + CliException("An unexpected error has occurred. Would you mind filing a bug report?", exitCode) { + + override fun toString(): String = "$message\n\n${theCause.printStackTraceToString()}" +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliMain.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliMain.kt new file mode 100644 index 00000000..c177bd16 --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliMain.kt @@ -0,0 +1,46 @@ +/** + * 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.commons.cli + +import java.io.PrintStream +import kotlin.system.exitProcess + +/** Building block for CLIs. Intended to be called from a `main` method. */ +fun cliMain(block: () -> Unit) { + fun printError(error: Throwable, stream: PrintStream) { + val message = error.toString() + stream.print(message) + // ensure CLI output always ends with newline + if (!message.endsWith('\n')) stream.println() + } + + try { + block() + } catch (e: CliTestException) { + // no need to print the error, the test results will already do it + exitProcess(e.exitCode) + } catch (e: CliException) { + printError(e, if (e.exitCode == 0) System.out else System.err) + exitProcess(e.exitCode) + } catch (e: Exception) { + printError(CliBugException(e), System.err) + exitProcess(1) + } +} + +object CliMain { + val compat: String? = System.getProperty("org.pkl.compat") +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliTestException.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliTestException.kt new file mode 100644 index 00000000..fa81df3b --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliTestException.kt @@ -0,0 +1,18 @@ +/** + * 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.commons.cli + +class CliTestException(msg: String) : CliException(msg, 1) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliTestOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliTestOptions.kt new file mode 100644 index 00000000..ddcb714e --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliTestOptions.kt @@ -0,0 +1,20 @@ +/** + * 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.commons.cli + +import java.nio.file.Path + +class CliTestOptions(val junitDir: Path? = null, val overwrite: Boolean = false) diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseCommand.kt new file mode 100644 index 00000000..f5ae90b3 --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseCommand.kt @@ -0,0 +1,59 @@ +/** + * 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.commons.cli.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import java.net.URI +import java.net.URISyntaxException +import org.pkl.commons.cli.CliException +import org.pkl.core.runtime.VmUtils +import org.pkl.core.util.IoUtils + +abstract class BaseCommand(name: String, helpLink: String, help: String = "") : + CliktCommand( + name = name, + help = help, + epilog = "For more information, visit $helpLink", + ) { + val baseOptions by BaseOptions() + + /** + * Parses [moduleName] into a URI. If scheme is not present, we expect that this is a file path + * and encode any possibly invalid characters. If a scheme is present, we expect that this is a + * valid URI. + */ + protected fun parseModuleName(moduleName: String): URI = + when (moduleName) { + "-" -> VmUtils.REPL_TEXT_URI + else -> + try { + IoUtils.toUri(moduleName) + } catch (e: URISyntaxException) { + val message = buildString { + append("Module URI `$moduleName` has invalid syntax (${e.reason}).") + if (e.index > -1) { + append("\n\n") + append(moduleName) + append("\n") + append(" ".repeat(e.index)) + append("^") + } + } + throw CliException(message) + } + } +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt new file mode 100644 index 00000000..3c03eff4 --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt @@ -0,0 +1,192 @@ +/** + * 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.commons.cli.commands + +import com.github.ajalt.clikt.parameters.groups.OptionGroup +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.long +import com.github.ajalt.clikt.parameters.types.path +import java.io.File +import java.io.InputStream +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.util.regex.Pattern +import java.util.stream.Collectors +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.core.util.IoUtils + +@Suppress("MemberVisibilityCanBePrivate") +class BaseOptions : OptionGroup() { + companion object { + fun includedCARootCerts(): InputStream { + return BaseOptions::class.java.getResourceAsStream("IncludedCARoots.pem")!! + } + } + + private val defaults = CliBaseOptions() + + private val output = + arrayOf("json", "jsonnet", "pcf", "properties", "plist", "textproto", "xml", "yaml") + + val allowedModules: List by + option( + names = arrayOf("--allowed-modules"), + help = "URI patterns that determine which modules can be loaded and evaluated." + ) + .convert("") { Pattern.compile(it) } + .splitAll(",") + + val allowedResources: List by + option( + names = arrayOf("--allowed-resources"), + help = "URI patterns that determine which external resources can be read." + ) + .convert("") { Pattern.compile(it) } + .splitAll(",") + + val rootDir: Path? by + option( + names = arrayOf("--root-dir"), + help = + "Restricts access to file-based modules and resources to those located under the root directory." + ) + .single() + .path() + + val cacheDir: Path? by + option(names = arrayOf("--cache-dir"), help = "The cache directory for storing packages.") + .single() + .path() + + val workingDir: Path by + option( + names = arrayOf("-w", "--working-dir"), + help = "Base path that relative module paths are resolved against." + ) + .single() + .path() + .default(defaults.normalizedWorkingDir) + + val properties: Map by + option( + names = arrayOf("-p", "--property"), + metavar = "", + help = "External property to set (repeatable)." + ) + .associate() + + val noCache: Boolean by + option(names = arrayOf("--no-cache"), help = "Disable cacheing of packages") + .single() + .flag(default = false) + + val format: String? by + option( + names = arrayOf("-f", "--format"), + help = "Output format to generate. <${output.joinToString()}>" + ) + .single() + + val envVars: Map by + option( + names = arrayOf("-e", "--env-var"), + metavar = "", + help = "Environment variable to set (repeatable)." + ) + .associate() + + val modulePath: List by + option( + names = arrayOf("--module-path"), + metavar = "", + help = + "Directories, ZIP archives, or JAR archives to search when resolving `modulepath:` URIs." + ) + .path() + .splitAll(File.pathSeparator) + + val settings: URI? by + option(names = arrayOf("--settings"), help = "Pkl settings module to use.").single().convert { + IoUtils.toUri(it) + } + + val timeout: Duration? by + option( + names = arrayOf("-t", "--timeout"), + metavar = "", + help = "Duration, in seconds, after which evaluation of a source module will be timed out." + ) + .single() + .long() + .convert { Duration.ofSeconds(it) } + + val caCertificates: List by + option( + names = arrayOf("--ca-certificates"), + metavar = "", + help = "Replaces the built-in CA certificates with the provided certificate file." + ) + .path() + .multiple() + + /** + * 1. If `--ca-certificates` option is not empty, use that. + * 2. If directory `~/.pkl/cacerts` is not empty, use that. + * 3. Use the bundled CA certificates. + */ + private fun getEffectiveCaCertificates(): List { + return caCertificates + .ifEmpty { + val home = System.getProperty("user.home") + val cacerts = Path.of(home, ".pkl", "cacerts") + if (cacerts.exists() && cacerts.isDirectory()) + Files.list(cacerts).filter(Path::isRegularFile).collect(Collectors.toList()) + else emptyList() + } + .ifEmpty { listOf(includedCARootCerts()) } + } + + fun baseOptions( + modules: List, + projectOptions: ProjectOptions? = null, + testMode: Boolean = false + ): CliBaseOptions { + return CliBaseOptions( + sourceModules = modules, + allowedModules = allowedModules.ifEmpty { null }, + allowedResources = allowedResources.ifEmpty { null }, + environmentVariables = envVars.ifEmpty { null }, + externalProperties = properties.mapValues { it.value.ifBlank { "true" } }.ifEmpty { null }, + modulePath = modulePath.ifEmpty { null }, + workingDir = workingDir, + settings = settings, + rootDir = rootDir, + projectDir = projectOptions?.projectDir, + timeout = timeout, + moduleCacheDir = cacheDir ?: defaults.normalizedModuleCacheDir, + noCache = noCache, + testMode = testMode, + omitProjectSettings = projectOptions?.omitProjectSettings ?: false, + noProject = projectOptions?.noProject ?: false, + caCertificates = getEffectiveCaCertificates() + ) + } +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ModulesCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ModulesCommand.kt new file mode 100644 index 00000000..c19fe58c --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ModulesCommand.kt @@ -0,0 +1,36 @@ +/** + * 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.commons.cli.commands + +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.convert +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import java.net.URI + +abstract class ModulesCommand(name: String, helpLink: String, help: String = "") : + BaseCommand( + name = name, + help = help, + helpLink = helpLink, + ) { + open val modules: List by + argument(name = "", help = "Module paths or URIs to evaluate.") + .convert { parseModuleName(it) } + .multiple(required = true) + + protected val projectOptions by ProjectOptions() +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/OptionExtensions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/OptionExtensions.kt new file mode 100644 index 00000000..781488fd --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/OptionExtensions.kt @@ -0,0 +1,49 @@ +/** + * 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.commons.cli.commands + +import com.github.ajalt.clikt.parameters.options.NullableOption +import com.github.ajalt.clikt.parameters.options.OptionWithValues +import com.github.ajalt.clikt.parameters.options.transformAll + +/** Forbid this option from being repeated. */ +fun NullableOption.single(): NullableOption { + return transformAll { + if (it.size > 1) { + fail("Option cannot be repeated") + } + it.lastOrNull() + } +} + +/** + * Allow this option to be repeated and to receive multiple values separated by [separator]. This is + * a mix of [split][com.github.ajalt.clikt.parameters.options.split] and + * [multiple][com.github.ajalt.clikt.parameters.options.multiple] joined together. + */ +fun NullableOption.splitAll( + separator: String = ",", + default: List = emptyList() +): OptionWithValues, List, ValueT> { + return copy( + transformValue = transformValue, + transformEach = { it }, + transformAll = { it.flatten().ifEmpty { default } }, + validator = {}, + nvalues = 1, + valueSplit = Regex.fromLiteral(separator) + ) +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ProjectOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ProjectOptions.kt new file mode 100644 index 00000000..fd76af84 --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ProjectOptions.kt @@ -0,0 +1,54 @@ +/** + * 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.commons.cli.commands + +import com.github.ajalt.clikt.parameters.groups.OptionGroup +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.path +import java.nio.file.Path + +/** + * Options related to projects for CLI commands that are related to normal evaluation (`pkl eval`, + * `pkl test`). + */ +class ProjectOptions : OptionGroup() { + val projectDir: Path? by + option( + names = arrayOf("--project-dir"), + metavar = "", + help = + "The project directory to use for this command. By default, searches up from the working directory for a PklProject file." + ) + .single() + .path() + + val omitProjectSettings: Boolean by + option( + names = arrayOf("--omit-project-settings"), + help = "Ignores evaluator settings set in the PklProject file." + ) + .single() + .flag(default = false) + + val noProject: Boolean by + option( + names = arrayOf("--no-project"), + help = "Disables loading settings and dependencies from the PklProject file." + ) + .single() + .flag(default = false) +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/TestOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/TestOptions.kt new file mode 100644 index 00000000..a438151b --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/TestOptions.kt @@ -0,0 +1,38 @@ +/** + * 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.commons.cli.commands + +import com.github.ajalt.clikt.parameters.groups.OptionGroup +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.path +import java.nio.file.Path +import org.pkl.commons.cli.CliTestOptions + +class TestOptions : OptionGroup() { + private val junitReportDir: Path? by + option( + names = arrayOf("--junit-reports"), + metavar = "", + help = "Directory where to store JUnit reports." + ) + .path() + + private val overwrite: Boolean by + option(names = arrayOf("--overwrite"), help = "Force generation of expected examples.").flag() + + val cliTestOptions: CliTestOptions by lazy { CliTestOptions(junitReportDir, overwrite) } +} diff --git a/pkl-commons-cli/src/main/resources/org/pkl/commons/cli/commands/IncludedCARoots.pem b/pkl-commons-cli/src/main/resources/org/pkl/commons/cli/commands/IncludedCARoots.pem new file mode 100644 index 00000000..1390856c --- /dev/null +++ b/pkl-commons-cli/src/main/resources/org/pkl/commons/cli/commands/IncludedCARoots.pem @@ -0,0 +1,3593 @@ +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw +gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw +BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0 +MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1 +c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ +bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ +2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E +T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j +5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM +C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T +DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX +wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A +2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm +nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl +N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj +c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS +5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS +Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr +hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/ +B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI +AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw +H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+ +b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk +2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol +IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk +5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY +n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG +A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw +FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx +MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u +aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b +RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z +YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 +QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw +yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ +BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ +SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH +r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 +4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me +dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw +q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 +nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu +H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC +XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd +6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf ++I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi +kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 +wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB +TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C +MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn +4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I +aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy +qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu +IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow +RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY +U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv +Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br +YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF +nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH +6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt +eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ +c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ +MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH +HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf +jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 +5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB +rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c +wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB +AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp +WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 +xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ +2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ +IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 +aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X +em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR +dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ +OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ +hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy +tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y +IEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig +RUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb +3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA +BoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5 +3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou +owReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/ +wZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF +ZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf +BgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv +civUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2 +AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50 +soIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI +WJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi +tJ/X5g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig +Um9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk +MQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg +Q2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD +VQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy +dXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+ +QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq +1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp +2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK +DOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape +az6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF +3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88 +oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM +g9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3 +mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd +BgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U +nrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX +dKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+ +MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL +/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX +CI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa +ZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW +2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7 +N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3 +Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB +As8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp +5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu +1uwJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y +IFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB +pDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h +IENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG +A1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU +cnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid +RtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V +seq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme +9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV +EY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW +hnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/ +DeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I +/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ +yonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts +L1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN +zl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIJAOF8N0D9G/5nMA0GCSqGSIb3DQEBDAUAMF0xCzAJBgNV +BAYTAkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScw +JQYDVQQDEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTMwHhcNMTYwNjE2 +MDYxNzE2WhcNMzgwMTE4MDYxNzE2WjBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UEAxMeU2VjdXJpdHkg +Q29tbXVuaWNhdGlvbiBSb290Q0EzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA48lySfcw3gl8qUCBWNO0Ot26YQ+TUG5pPDXC7ltzkBtnTCHsXzW7OT4r +CmDvu20rhvtxosis5FaU+cmvsXLUIKx00rgVrVH+hXShuRD+BYD5UpOzQD11EKzA +lrenfna84xtSGc4RHwsENPXY9Wk8d/Nk9A2qhd7gCVAEF5aEt8iKvE1y/By7z/MG +TfmfZPd+pmaGNXHIEYBMwXFAWB6+oHP2/D5Q4eAvJj1+XCO1eXDe+uDRpdYMQXF7 +9+qMHIjH7Iv10S9VlkZ8WjtYO/u62C21Jdp6Ts9EriGmnpjKIG58u4iFW/vAEGK7 +8vknR+/RiTlDxN/e4UG/VHMgly1s2vPUB6PmudhvrvyMGS7TZ2crldtYXLVqAvO4 +g160a75BflcJdURQVc1aEWEhCmHCqYj9E7wtiS/NYeCVvsq1e+F7NGcLH7YMx3we +GVPKp7FKFSBWFHA9K4IsD50VHUeAR/94mQ4xr28+j+2GaR57GIgUssL8gjMunEst ++3A7caoreyYn8xrC3PsXuKHqy6C0rtOUfnrQq8PsOC0RLoi/1D+tEjtCrI8Cbn3M +0V9hvqG8OmpI6iZVIhZdXw3/JzOfGAN0iltSIEdrRU0id4xVJ/CvHozJgyJUt5rQ +T9nO/NkuHJYosQLTA70lUhw0Zk8jq/R3gpYd0VcwCBEF/VfR2ccCAwEAAaNCMEAw +HQYDVR0OBBYEFGQUfPxYchamCik0FW8qy7z8r6irMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQDcAiMI4u8hOscNtybS +YpOnpSNyByCCYN8Y11StaSWSntkUz5m5UoHPrmyKO1o5yGwBQ8IibQLwYs1OY0PA +FNr0Y/Dq9HHuTofjcan0yVflLl8cebsjqodEV+m9NU1Bu0soo5iyG9kLFwfl9+qd +9XbXv8S2gVj/yP9kaWJ5rW4OH3/uHWnlt3Jxs/6lATWUVCvAUm2PVcTJ0rjLyjQI +UYWg9by0F1jqClx6vWPGOi//lkkZhOpn2ASxYfQAW0q3nHE3GYV5v4GwxxMOdnE+ +OoAGrgYWp421wsTL/0ClXI2lyTrtcoHKXJg80jQDdwj98ClZXSEIx2C/pHF7uNke +gr4Jr2VvKKu/S7XuPghHJ6APbw+LP6yVGPO5DtxnVW5inkYO0QR4ynKudtml+LLf +iAlhi+8kTtFZP1rUPcmTPCtk9YENFpb3ksP+MW/oKjJ0DvRMmEoYDjBU1cXrvMUV +nuiZIesnKwkK2/HmcBhWuwzkvvnoEKQTkrgc4NtnHVMDpCKn3F2SEDzq//wbEBrD +2NCcnWXL0CsnMQMeNuE9dnUM/0Umud1RvCPHX9jYhxBAEg09ODfnRDwYwFMJZI// +1ZqmfHAuc1Uh6N//g7kdPjIe1qZ9LPFm6Vwdp6POXiUyK+OVrCoHzrQoeIY8Laad +TdJ0MN1kURXbg4NR16/9M51NZg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- diff --git a/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/BaseCommandTest.kt b/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/BaseCommandTest.kt new file mode 100644 index 00000000..e9e1b42c --- /dev/null +++ b/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/BaseCommandTest.kt @@ -0,0 +1,74 @@ +/** + * 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.commons.cli + +import com.github.ajalt.clikt.core.BadParameterValue +import com.github.ajalt.clikt.core.PrintHelpMessage +import java.io.File +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.commons.cli.commands.BaseCommand + +class BaseCommandTest { + + private val cmd = + object : BaseCommand("test", "") { + override fun run() = Unit + } + + @Test + fun `invalid timeout`() { + val e = assertThrows { cmd.parse(arrayOf("--timeout", "abc")) } + assertThat(e).hasMessageContaining("timeout") + } + + @Test + fun `help queries do not present as errors`() { + assertThrows { cmd.parse(arrayOf("--help")) } + } + + @Test + fun `external properties without value default to 'true'`() { + cmd.parse(arrayOf("-p", "flag1", "-p", "flag2", "-p", "FOO=bar")) + val props = cmd.baseOptions.baseOptions(emptyList()).externalProperties + + assertThat(props).isEqualTo(mapOf("flag1" to "true", "flag2" to "true", "FOO" to "bar")) + } + + @Test + fun `--allowed-modules, --allowed-resources and --module-path can be repeated`() { + cmd.parse(arrayOf("--allowed-modules", "m1,m2,m3", "--allowed-modules", "m4")) + + assertThat(cmd.baseOptions.allowedModules.map(Pattern::toString)) + .isEqualTo(listOf("m1", "m2", "m3", "m4")) + + cmd.parse(arrayOf("--allowed-resources", "r1,r2,r3", "--allowed-resources", "r4")) + assertThat(cmd.baseOptions.allowedResources.map(Pattern::toString)) + .isEqualTo(listOf("r1", "r2", "r3", "r4")) + + val sep = File.pathSeparator + cmd.parse(arrayOf("--module-path", "p1${sep}p2${sep}p3", "--module-path", "p4")) + assertThat(cmd.baseOptions.modulePath).isEqualTo(listOf("p1", "p2", "p3", "p4").map(Path::of)) + + cmd.parse(arrayOf()) + assertThat(cmd.baseOptions.allowedModules).isEmpty() + + assertThat(cmd.baseOptions.allowedResources).isEmpty() + } +} diff --git a/pkl-commons-test/gradle.lockfile b/pkl-commons-test/gradle.lockfile new file mode 100644 index 00000000..f61ce1b4 --- /dev/null +++ b/pkl-commons-test/gradle.lockfile @@ -0,0 +1,30 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +net.bytebuddy:byte-buddy:1.12.21=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-commons-test/pkl-commons-test.gradle.kts b/pkl-commons-test/pkl-commons-test.gradle.kts new file mode 100644 index 00000000..9a22956b --- /dev/null +++ b/pkl-commons-test/pkl-commons-test.gradle.kts @@ -0,0 +1,130 @@ +import java.security.MessageDigest + +plugins { + pklAllProjects + pklKotlinLibrary +} + +// note: no need to publish this library + +dependencies { + api(libs.junitApi) + api(libs.junitEngine) + api(libs.junitParams) + api(project(":pkl-commons")) // for convenience + implementation(libs.assertj) +} + + +/** + * Creates test packages from the `src/test/files/packages` directory. + * + * These packages are used by PackageServer to serve assets when running + * LanguageSnippetTests and PackageResolversTest. + */ +val createTestPackages = tasks.create("createTestPackages") + +fun toHex(hash: ByteArray): String { + val hexDigitTable = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') + val builder = StringBuilder(hash.size * 2) + for (b in hash) { + builder.append(hexDigitTable[b.toInt() shr 4 and 0xF]) + builder.append(hexDigitTable[b.toInt() and 0xF]) + } + return builder.toString() +} + +fun File.computeChecksum(): String { + val md = MessageDigest.getInstance("SHA-256") + val hash = md.digest(readBytes()) + return toHex(hash) +} + +tasks.processResources { + dependsOn(createTestPackages) + dependsOn(generateCerts) +} + +val mainSourceSet by sourceSets.named("main") { + resources { + srcDir(buildDir.resolve("test-packages/")) + srcDir(buildDir.resolve("keystore/")) + } +} + +val sourcesJar = tasks.named("sourcesJar").get() + +for (packageDir in file("src/main/files/packages").listFiles()!!) { + if (!packageDir.isDirectory) continue + val destinationDir = buildDir.resolve("test-packages/org/pkl/commons/test/packages/${packageDir.name}") + val metadataJson = packageDir.resolve("${packageDir.name}.json") + val packageContents = packageDir.resolve("package") + val zipFileName = "${packageDir.name}.zip" + val archiveFile = destinationDir.resolve(zipFileName) + + tasks.create("zip-${packageDir.name}", Zip::class) { + archiveFileName.set(zipFileName) + from(packageContents) + destinationDirectory.set(destinationDir) + // required so that checksums are reproducible + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + } + + val copyTask = tasks.create("copy-${packageDir.name}", Copy::class) { + dependsOn("zip-${packageDir.name}") + from(metadataJson) + into(destinationDir) + val shasumFile = file("$destinationDir/${packageDir.name}.json.sha256") + outputs.file(shasumFile) + doFirst { + expand(mapOf("computedChecksum" to archiveFile.computeChecksum())) + } + doLast { + val outputFile = file("$destinationDir").resolve("${packageDir.name}.json") + shasumFile.writeText(outputFile.computeChecksum()) + } + createTestPackages.dependsOn(this) + } + + sourcesJar.dependsOn.add(copyTask) +} + +val generateKeys by tasks.registering(JavaExec::class) { + val outputFile = file("$buildDir/keystore/localhost.p12") + outputs.file(outputFile) + mainClass.set("sun.security.tools.keytool.Main") + args = listOf( + "-genkeypair", + "-keyalg", "RSA", + "-alias", "integ_tests", + "-keystore", "localhost.p12", + "-storepass", "password", + "-dname", "CN=localhost" + ) + workingDir = file("$buildDir/keystore/") + onlyIf { !outputFile.exists() } + doFirst { + workingDir.mkdirs() + } +} + +val generateCerts by tasks.registering(JavaExec::class) { + dependsOn("generateKeys") + val outputFile = file("$buildDir/keystore/localhost.pem") + outputs.file(outputFile) + mainClass.set("sun.security.tools.keytool.Main") + args = listOf( + "-exportcert", + "-alias", "integ_tests", + "-storepass", "password", + "-keystore", "localhost.p12", + "-rfc", + "-file", "localhost.pem" + ) + workingDir = file("$buildDir/keystore/") + onlyIf { !outputFile.exists() } + doFirst { + workingDir.mkdirs() + } +} diff --git a/pkl-commons-test/src/main/files/packages/badChecksum@1.0.0/badChecksum@1.0.0.json b/pkl-commons-test/src/main/files/packages/badChecksum@1.0.0/badChecksum@1.0.0.json new file mode 100644 index 00000000..a6da5330 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badChecksum@1.0.0/badChecksum@1.0.0.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:12110/badChecksum@1.0.0", + "name": "bugs", + "packageZipUrl": "https://localhost:12110/badChecksum@1.0.0/badChecksum@1.0.0.zip", + "dependencies": {}, + "version": "1.0.0", + "packageZipChecksums": { + "sha256": "intentionally bogus checksum" + } +} diff --git a/pkl-commons-test/src/main/files/packages/badChecksum@1.0.0/package/Bug.pkl b/pkl-commons-test/src/main/files/packages/badChecksum@1.0.0/package/Bug.pkl new file mode 100644 index 00000000..c5830830 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badChecksum@1.0.0/package/Bug.pkl @@ -0,0 +1 @@ +module bugs.Bug diff --git a/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/badImportsWithinPackage@1.0.0.json b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/badImportsWithinPackage@1.0.0.json new file mode 100644 index 00000000..ad1f393e --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/badImportsWithinPackage@1.0.0.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:12110/badImportsWithinPackage@1.1.0", + "name": "badImportsWithinPackage", + "packageZipUrl": "https://localhost:12110/badImportsWithinPackage@1.0.0/badImportsWithinPackage@1.0.0.zip", + "dependencies": {}, + "version": "1.0.0", + "packageZipChecksums": { + "sha256": "$computedChecksum" + } +} diff --git a/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/invalidPath.pkl b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/invalidPath.pkl new file mode 100644 index 00000000..29bd3417 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/invalidPath.pkl @@ -0,0 +1 @@ +res = import("not/a/valid/path.pkl") diff --git a/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/invalidPathRead.pkl b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/invalidPathRead.pkl new file mode 100644 index 00000000..330a85a4 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/invalidPathRead.pkl @@ -0,0 +1 @@ +res = read("not/a/valid/path.txt") diff --git a/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/unknownDependency.pkl b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/unknownDependency.pkl new file mode 100644 index 00000000..d46dc47b --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/unknownDependency.pkl @@ -0,0 +1 @@ +res = import("@fruits/Foo.pkl") diff --git a/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/unknownDependencyRead.pkl b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/unknownDependencyRead.pkl new file mode 100644 index 00000000..d69a886b --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badImportsWithinPackage@1.0.0/package/unknownDependencyRead.pkl @@ -0,0 +1 @@ +res = read("@notapackage/Foo.txt") diff --git a/pkl-commons-test/src/main/files/packages/badMetadataJson@1.0.0/badMetadataJson@1.0.0.json b/pkl-commons-test/src/main/files/packages/badMetadataJson@1.0.0/badMetadataJson@1.0.0.json new file mode 100644 index 00000000..ede72e79 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badMetadataJson@1.0.0/badMetadataJson@1.0.0.json @@ -0,0 +1 @@ +this is not json diff --git a/pkl-commons-test/src/main/files/packages/badMetadataJson@1.0.0/package/Bug.pkl b/pkl-commons-test/src/main/files/packages/badMetadataJson@1.0.0/package/Bug.pkl new file mode 100644 index 00000000..c5830830 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badMetadataJson@1.0.0/package/Bug.pkl @@ -0,0 +1 @@ +module bugs.Bug diff --git a/pkl-commons-test/src/main/files/packages/badPackageZipUrl@1.0.0/badPackageZipUrl@1.0.0.json b/pkl-commons-test/src/main/files/packages/badPackageZipUrl@1.0.0/badPackageZipUrl@1.0.0.json new file mode 100644 index 00000000..a481cd15 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badPackageZipUrl@1.0.0/badPackageZipUrl@1.0.0.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:12110/badPackagezipUrl@1.1.0", + "name": "bugs", + "packageZipUrl": "ftp://wait/a/minute", + "dependencies": {}, + "version": "1.0.0", + "packageZipChecksums": { + "sha256": "$computedChecksum" + } +} diff --git a/pkl-commons-test/src/main/files/packages/badPackageZipUrl@1.0.0/package/Bug.pkl b/pkl-commons-test/src/main/files/packages/badPackageZipUrl@1.0.0/package/Bug.pkl new file mode 100644 index 00000000..c5830830 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/badPackageZipUrl@1.0.0/package/Bug.pkl @@ -0,0 +1 @@ +module bugs.Bug diff --git a/pkl-commons-test/src/main/files/packages/birds@0.5.0/birds@0.5.0.json b/pkl-commons-test/src/main/files/packages/birds@0.5.0/birds@0.5.0.json new file mode 100644 index 00000000..8d734c84 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/birds@0.5.0/birds@0.5.0.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "name": "birds", + "packageUri": "package://localhost:12110/birds@0.5.0", + "packageZipUrl": "https://localhost:12110/birds@0.5.0/birds@0.5.0.zip", + "dependencies": { + "fruities": { + "uri": "package://localhost:12110/fruit@1.0.5", + "checksums": { + "sha256": "b4ea243de781feeab7921227591e6584db5d0673340f30fab2ffe8ad5c9f75f5" + } + } + }, + "version": "0.5.0", + "packageZipChecksums": { + "sha256": "$computedChecksum" + }, + "sourceCodeUrlScheme": "https://example.com/birds/v0.5.0/blob%{path}#L%{line}-L%{endLine}", + "sourceCode": "https://example.com/birds", + "documentation": "https://example.com/bird-docs", + "license": "UNLICENSED", + "authors": [ + "petey-bird@example.com", + "polly-bird@example.com" + ], + "issueTracker": "https://example.com/birds/issues" +} diff --git a/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/Bird.pkl b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/Bird.pkl new file mode 100644 index 00000000..841f1e0a --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/Bird.pkl @@ -0,0 +1,7 @@ +open module birds.Bird + +import "@fruities/Fruit.pkl" + +name: String + +favoriteFruit: Fruit diff --git a/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/allFruit.pkl b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/allFruit.pkl new file mode 100644 index 00000000..021aa906 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/allFruit.pkl @@ -0,0 +1,4 @@ +module birds.allFruit + +fruit = import*("@fruities/catalog/*.pkl") +fruitFiles = read*("@fruities/catalog/*.pkl") diff --git a/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog.pkl b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog.pkl new file mode 100644 index 00000000..8a6d7c1d --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog.pkl @@ -0,0 +1,4 @@ +module birds.catalog + +catalog = import*("catalog/*.pkl") +catalogFiles = read*("catalog/*.pkl") diff --git a/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog/Ostritch.pkl b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog/Ostritch.pkl new file mode 100644 index 00000000..89d69f26 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog/Ostritch.pkl @@ -0,0 +1,7 @@ +amends "../Bird.pkl" + +name = "Ostritch" + +favoriteFruit { + name = "Orange" +} diff --git a/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog/Swallow.pkl b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog/Swallow.pkl new file mode 100644 index 00000000..604e6b3b --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/catalog/Swallow.pkl @@ -0,0 +1,7 @@ +amends "../Bird.pkl" + +import "@fruities/catalog/apple.pkl" + +name = "Swallow" + +favoriteFruit = apple diff --git a/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/some/dir/Bird.pkl b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/some/dir/Bird.pkl new file mode 100644 index 00000000..09d39b4b --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/birds@0.5.0/package/some/dir/Bird.pkl @@ -0,0 +1,7 @@ +amends "..." + +name = "Bird" + +favoriteFruit { + name = "Fruit" +} diff --git a/pkl-commons-test/src/main/files/packages/fruit@1.0.5/fruit@1.0.5.json b/pkl-commons-test/src/main/files/packages/fruit@1.0.5/fruit@1.0.5.json new file mode 100644 index 00000000..48925abc --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/fruit@1.0.5/fruit@1.0.5.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:12110/fruit@1.0.5", + "name": "fruit", + "version": "1.0.5", + "packageZipUrl": "https://localhost:12110/fruit@1.0.5/fruit@1.0.5.zip", + "dependencies": {}, + "packageZipChecksums": { + "sha256": "$computedChecksum" + }, + "sourceCode": "https://example.com/fruit", + "documentation": "https://example.com/fruit-docs", + "license": "UNLICENSED", + "authors": [ + "apple-1@example.com", + "banana-2@example.com" + ], + "issueTracker": "https://example.com/fruit/issues" +} diff --git a/pkl-commons-test/src/main/files/packages/fruit@1.0.5/package/Fruit.pkl b/pkl-commons-test/src/main/files/packages/fruit@1.0.5/package/Fruit.pkl new file mode 100644 index 00000000..3dbfa98b --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/fruit@1.0.5/package/Fruit.pkl @@ -0,0 +1,3 @@ +module fruit.Fruit + +name: String diff --git a/pkl-commons-test/src/main/files/packages/fruit@1.0.5/package/catalog/apple.pkl b/pkl-commons-test/src/main/files/packages/fruit@1.0.5/package/catalog/apple.pkl new file mode 100644 index 00000000..6acd6d81 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/fruit@1.0.5/package/catalog/apple.pkl @@ -0,0 +1,3 @@ +amends "../Fruit.pkl" + +name = "Apple" diff --git a/pkl-commons-test/src/main/files/packages/fruit@1.1.0/fruit@1.1.0.json b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/fruit@1.1.0.json new file mode 100644 index 00000000..65e2e008 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/fruit@1.1.0.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:12110/fruit@1.1.0", + "name": "fruit", + "version": "1.1.0", + "packageZipUrl": "https://localhost:12110/fruit@1.1.0/fruit@1.1.0.zip", + "dependencies": {}, + "packageZipChecksums": { + "sha256": "$computedChecksum" + }, + "sourceCode": "https://example.com/fruit", + "documentation": "https://example.com/fruit-docs", + "license": "UNLICENSED", + "authors": [ + "apple-1@example.com", + "banana-2@example.com" + ], + "issueTracker": "https://example.com/fruit/issues" +} diff --git a/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/Fruit.pkl b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/Fruit.pkl new file mode 100644 index 00000000..3dbfa98b --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/Fruit.pkl @@ -0,0 +1,3 @@ +module fruit.Fruit + +name: String diff --git a/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/catalog/apple.pkl b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/catalog/apple.pkl new file mode 100644 index 00000000..b6d2f16e --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/catalog/apple.pkl @@ -0,0 +1,3 @@ +amends "../Fruit.pkl" + +name = "Apple 🍎" diff --git a/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/catalog/pineapple.pkl b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/catalog/pineapple.pkl new file mode 100644 index 00000000..e1a02b70 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/fruit@1.1.0/package/catalog/pineapple.pkl @@ -0,0 +1,3 @@ +amends "../Fruit.pkl" + +name = "Pineapple 🍍" diff --git a/pkl-commons-test/src/main/files/packages/packageContainingWildcardCharacters@1.0.0/package/name with [wildcard]! characters~~.pkl b/pkl-commons-test/src/main/files/packages/packageContainingWildcardCharacters@1.0.0/package/name with [wildcard]! characters~~.pkl new file mode 100644 index 00000000..e69de29b diff --git a/pkl-commons-test/src/main/files/packages/packageContainingWildcardCharacters@1.0.0/packageContainingWildcardCharacters@1.0.0.json b/pkl-commons-test/src/main/files/packages/packageContainingWildcardCharacters@1.0.0/packageContainingWildcardCharacters@1.0.0.json new file mode 100644 index 00000000..de72ecba --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/packageContainingWildcardCharacters@1.0.0/packageContainingWildcardCharacters@1.0.0.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:12110/packageContainingWildcardCharacters@1.0.0", + "name": "packageContainingWildcardCharacters", + "version": "1.0.0", + "packageZipUrl": "https://localhost:12110/packageContainingWildcardCharacters@1.0.0/packageContainingWildcardCharacters@1.0.0.zip", + "dependencies": {}, + "packageZipChecksums": { + "sha256": "$computedChecksum" + } +} diff --git a/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/FileTestUtils.kt b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/FileTestUtils.kt new file mode 100644 index 00000000..ae6a3283 --- /dev/null +++ b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/FileTestUtils.kt @@ -0,0 +1,106 @@ +/** + * 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.commons.test + +import java.nio.file.Path +import kotlin.io.path.* +import kotlin.streams.toList +import org.assertj.core.api.Assertions.fail +import org.pkl.commons.* + +object FileTestUtils { + val rootProjectDir: Path by lazy { + val workingDir = currentWorkingDir + workingDir.takeIf { it.resolve("settings.gradle.kts").exists() } + ?: workingDir.parent.takeIf { it.resolve("settings.gradle.kts").exists() } + ?: workingDir.parent.parent.takeIf { it.resolve("settings.gradle.kts").exists() } + ?: throw AssertionError("Failed to locate root project directory.") + } + val selfSignedCertificate: Path by lazy { + rootProjectDir.resolve("pkl-commons-test/build/keystore/localhost.pem") + } +} + +fun Path.listFilesRecursively(): List = + walk(99).use { paths -> paths.filter { it.isRegularFile() || it.isSymbolicLink() }.toList() } + +data class SnippetOutcome(val expectedOutFile: Path, val actual: String, val success: Boolean) { + private val expectedErrFile = + expectedOutFile.resolveSibling(expectedOutFile.toString().replaceAfterLast('.', "err")) + + private val expectedOutExists = expectedOutFile.exists() + private val expectedErrExists = expectedErrFile.exists() + private val overwrite + get() = System.getenv().containsKey("OVERWRITE_SNIPPETS") + + private val expected by lazy { + when { + expectedOutExists && expectedErrExists -> + fail("Test has both expected out and .err files: $displayName") + expectedOutExists -> expectedOutFile.readString() + expectedErrExists -> expectedErrFile.readString() + else -> "" + } + } + + private val displayName by lazy { + val path = expectedOutFile.toString() + val baseDir = "src/test/files" + val index = path.indexOf(baseDir) + val endIndex = path.lastIndexOf('.') + if (index == -1 || endIndex == -1) path else path.substring(index + baseDir.length, endIndex) + } + + fun check() { + when { + success && !expectedOutExists && !expectedErrExists && actual.isBlank() -> return + !success && expectedOutExists && !overwrite -> + failWithDiff("Test was expected to succeed, but failed: $displayName") + !success && expectedOutExists -> { + expectedOutFile.deleteExisting() + expectedErrFile.writeString(actual) + fail("Wrote file $expectedErrFile for $displayName and deleted $expectedOutFile") + } + success && expectedErrExists && !overwrite -> + failWithDiff("Test was expected to fail, but succeeded: $displayName") + success && expectedErrExists -> { + expectedErrFile.deleteExisting() + expectedOutFile.writeString(actual) + fail("Wrote file $expectedOutFile for $displayName and deleted $expectedErrFile") + } + !expectedOutExists && !expectedErrExists && actual.isNotBlank() -> { + val file = if (success) expectedOutFile else expectedErrFile + file.createParentDirectories().writeString(actual) + failWithDiff("Created missing file $file for $displayName") + } + else -> { + assert(success && expectedOutExists || !success && expectedErrExists) + if (actual != expected) { + if (overwrite) { + val file = if (success) expectedOutFile else expectedErrFile + file.writeString(actual) + fail("Overwrote file $file for $displayName") + } else { + failWithDiff("Output was different from expected: $displayName") + } + } + } + } + } + + private fun failWithDiff(message: String): Nothing = + throw PklAssertionFailedError(message, expected, actual) +} diff --git a/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/InputOutputTestEngine.kt b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/InputOutputTestEngine.kt new file mode 100644 index 00000000..debcf07b --- /dev/null +++ b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/InputOutputTestEngine.kt @@ -0,0 +1,155 @@ +/** + * 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.commons.test + +import java.nio.file.Path +import java.util.Locale +import kotlin.io.path.isRegularFile +import kotlin.io.path.useDirectoryEntries +import kotlin.reflect.KClass +import org.junit.platform.engine.* +import org.junit.platform.engine.TestDescriptor.Type +import org.junit.platform.engine.discovery.ClassSelector +import org.junit.platform.engine.discovery.MethodSelector +import org.junit.platform.engine.discovery.PackageSelector +import org.junit.platform.engine.discovery.UniqueIdSelector +import org.junit.platform.engine.support.descriptor.* +import org.junit.platform.engine.support.hierarchical.EngineExecutionContext +import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine +import org.junit.platform.engine.support.hierarchical.Node +import org.junit.platform.engine.support.hierarchical.Node.DynamicTestExecutor + +abstract class InputOutputTestEngine : + HierarchicalTestEngine() { + protected val rootProjectDir = FileTestUtils.rootProjectDir + + protected abstract val testClass: KClass<*> + + protected open val includedTests: List = listOf(Regex(".*")) + + @Suppress("RegExpUnexpectedAnchor") + protected open val excludedTests: List = listOf(Regex("$^")) + + protected abstract val inputDir: Path + + protected abstract val isInputFile: (Path) -> Boolean + + protected abstract fun expectedOutputFileFor(inputFile: Path): Path + + protected abstract fun generateOutputFor(inputFile: Path): Pair + + class ExecutionContext : EngineExecutionContext + + override fun getId(): String = this::class.java.simpleName + + init { + // Enforce consistent locale for tests to avoid inconsistent formatting. + Locale.setDefault(Locale.ROOT) + } + + override fun discover( + discoveryRequest: EngineDiscoveryRequest, + uniqueId: UniqueId + ): TestDescriptor { + val packageSelectors = discoveryRequest.getSelectorsByType(PackageSelector::class.java) + val classSelectors = discoveryRequest.getSelectorsByType(ClassSelector::class.java) + val methodSelectors = discoveryRequest.getSelectorsByType(MethodSelector::class.java) + val uniqueIdSelectors = discoveryRequest.getSelectorsByType(UniqueIdSelector::class.java) + + val packageName = testClass.java.`package`.name + val className = testClass.java.name + + if ( + methodSelectors.isEmpty() && + (packageSelectors.isEmpty() || packageSelectors.any { it.packageName == packageName }) && + (classSelectors.isEmpty() || classSelectors.any { it.className == className }) + ) { + + val rootNode = InputDirNode(uniqueId, inputDir, ClassSource.from(testClass.java)) + return doDiscover(rootNode, uniqueIdSelectors) + } + + // return empty descriptor w/o children + return EngineDescriptor(uniqueId, javaClass.simpleName) + } + + private fun doDiscover( + dirNode: InputDirNode, + uniqueIdSelectors: List + ): TestDescriptor { + dirNode.inputDir.useDirectoryEntries { children -> + for (child in children) { + val testPath = child.toString() + val testName = child.fileName.toString() + if (child.isRegularFile()) { + if ( + isInputFile(child) && + includedTests.any { it.matches(testPath) } && + !excludedTests.any { it.matches(testPath) } + ) { + val childId = dirNode.uniqueId.append("inputFileNode", testName) + if ( + uniqueIdSelectors.isEmpty() || + uniqueIdSelectors.any { childId.hasPrefix(it.uniqueId) } + ) { + dirNode.addChild(InputFileNode(childId, child)) + } // else skip + } + } else { + val childId = dirNode.uniqueId.append("inputDirNode", testName) + dirNode.addChild( + doDiscover( + InputDirNode(childId, child, DirectorySource.from(child.toFile())), + uniqueIdSelectors + ) + ) + } + } + } + return dirNode + } + + override fun createExecutionContext(request: ExecutionRequest) = ExecutionContext() + + private inner class InputDirNode(uniqueId: UniqueId, val inputDir: Path, source: TestSource) : + AbstractTestDescriptor(uniqueId, inputDir.fileName.toString(), source), Node { + override fun getType() = Type.CONTAINER + } + + private inner class InputFileNode(uniqueId: UniqueId, private val inputFile: Path) : + AbstractTestDescriptor( + uniqueId, + inputFile.fileName.toString(), + FileSource.from(inputFile.toFile()) + ), + Node { + + override fun getType() = Type.TEST + + override fun execute( + context: ExecutionContext, + dynamicTestExecutor: DynamicTestExecutor + ): ExecutionContext { + + val (success, actualOutput) = generateOutputFor(inputFile) + val expectedOutputFile = expectedOutputFileFor(inputFile) + + SnippetOutcome(expectedOutputFile, actualOutput, success).check() + + return context + } + } +} diff --git a/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/PackageServer.kt b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/PackageServer.kt new file mode 100644 index 00000000..230f19b1 --- /dev/null +++ b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/PackageServer.kt @@ -0,0 +1,145 @@ +/** + * 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.commons.test + +import com.sun.net.httpserver.HttpHandler +import com.sun.net.httpserver.HttpsConfigurator +import com.sun.net.httpserver.HttpsParameters +import com.sun.net.httpserver.HttpsServer +import java.net.BindException +import java.net.InetSocketAddress +import java.nio.file.* +import java.security.KeyStore +import java.util.concurrent.Executors +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import kotlin.io.path.isRegularFile +import org.pkl.commons.createParentDirectories +import org.pkl.commons.deleteRecursively + +object PackageServer { + private val keystore = javaClass.getResource("/localhost.p12")!! + + // When tests are run via Gradle (i.e. from ./gradlew check), resources are packaged into a jar. + // When run directly in IntelliJ, resources are just directories. + private val packagesDir: Path = let { + val uri = javaClass.getResource("packages")!!.toURI() + try { + Path.of(uri) + } catch (e: FileSystemNotFoundException) { + FileSystems.newFileSystem(uri, mapOf()) + Path.of(uri) + } + } + + fun populateCacheDir(cacheDir: Path) { + val basePath = cacheDir.resolve("package-1/localhost:$PORT") + basePath.deleteRecursively() + Files.walk(packagesDir).use { stream -> + stream.forEach { source -> + if (!source.isRegularFile()) return@forEach + val relativized = + source.toString().replaceFirst(packagesDir.toString(), "").drop(1).ifEmpty { + return@forEach + } + val dest = basePath.resolve(relativized) + dest.createParentDirectories() + Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING) + } + } + } + + private const val PORT = 12110 + private var started = false + + private val sslContext by lazy { + SSLContext.getInstance("SSL").apply { + val pass = "password".toCharArray() + val ks = KeyStore.getInstance("PKCS12").apply { load(keystore.openStream(), pass) } + val kmf = KeyManagerFactory.getInstance("SunX509").apply { init(ks, pass) } + init(kmf.keyManagers, null, null) + } + } + + private val engine by lazy { sslContext.createSSLEngine() } + + private val simpleHttpsConfigurator = + object : HttpsConfigurator(sslContext) { + override fun configure(params: HttpsParameters) { + params.needClientAuth = false + params.cipherSuites = engine.enabledCipherSuites + params.protocols = engine.enabledProtocols + params.setSSLParameters(sslContext.supportedSSLParameters) + } + } + + private val handler = HttpHandler { exchange -> + if (exchange.requestMethod != "GET") { + exchange.sendResponseHeaders(405, 0) + exchange.close() + return@HttpHandler + } + val path = exchange.requestURI.path + val localPath = + if (path.endsWith(".zip")) packagesDir.resolve(path.drop(1)) + else packagesDir.resolve("${path.drop(1)}${path}.json") + if (!Files.exists(localPath)) { + exchange.sendResponseHeaders(404, 0) + exchange.close() + return@HttpHandler + } + exchange.sendResponseHeaders(200, 0) + exchange.responseBody.use { outputStream -> Files.copy(localPath, outputStream) } + exchange.close() + } + + private val myExecutor = Executors.newFixedThreadPool(1) + + private val server by lazy { + HttpsServer.create().apply { + httpsConfigurator = simpleHttpsConfigurator + createContext("/", handler) + executor = myExecutor + } + } + + fun ensureStarted() = + synchronized(this) { + if (!started) { + // Crude hack to make sure that parrallel tests don't try and use each others mock server + // otherwise you get flaky tests when a server instance is shutdown by one set of tests + // while another set of tests is still relying on it. + // Side effect is that tests that spin up a mock package server are now serialised, rather + // than running in parrallel. But that seems like a reasonable tradeoff to avoid flaky + // tests. + for (i in 1..20) { + try { + server.bind(InetSocketAddress(PORT), 0) + server.start() + started = true + println("Mock package server started after $i attempt(s)") + return@synchronized + } catch (_: BindException) { + println( + "Port $PORT in use after $i/20 attempt(s), probably another test running in parrallel. Sleeping for 1 second and trying again" + ) + Thread.sleep(1000) + } + } + println("Unable to start package server! This will probably result in a test failures") + } + } +} diff --git a/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/PklAssertionFailedError.kt b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/PklAssertionFailedError.kt new file mode 100644 index 00000000..e5778de4 --- /dev/null +++ b/pkl-commons-test/src/main/kotlin/org/pkl/commons/test/PklAssertionFailedError.kt @@ -0,0 +1,33 @@ +/** + * 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.commons.test + +import org.assertj.core.util.diff.DiffUtils +import org.opentest4j.AssertionFailedError + +/** + * Makes up for the fact that [AssertionFailedError] doesn't print a diff, resulting in + * unintelligible errors outside IDEs (which show a diff dialog). + * https://github.com/ota4j-team/opentest4j/issues/59 + */ +class PklAssertionFailedError(message: String, expected: Any?, actual: Any?) : + AssertionFailedError(message, expected, actual) { + override fun toString(): String { + val patch = + DiffUtils.diff(expected.stringRepresentation.lines(), actual.stringRepresentation.lines()) + return patch.deltas.joinToString("\n\n") + } +} diff --git a/pkl-commons/gradle.lockfile b/pkl-commons/gradle.lockfile new file mode 100644 index 00000000..4170e1a9 --- /dev/null +++ b/pkl-commons/gradle.lockfile @@ -0,0 +1,30 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-commons/pkl-commons.gradle.kts b/pkl-commons/pkl-commons.gradle.kts new file mode 100644 index 00000000..8d3fdd93 --- /dev/null +++ b/pkl-commons/pkl-commons.gradle.kts @@ -0,0 +1,16 @@ +plugins { + pklAllProjects + pklKotlinLibrary + pklPublishLibrary +} + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-commons") + description.set("Internal utilities. NOT A PUBLIC API.") + } + } + } +} diff --git a/pkl-commons/src/main/kotlin/org/pkl/commons/Paths.kt b/pkl-commons/src/main/kotlin/org/pkl/commons/Paths.kt new file mode 100644 index 00000000..f2c0723a --- /dev/null +++ b/pkl-commons/src/main/kotlin/org/pkl/commons/Paths.kt @@ -0,0 +1,73 @@ +/** + * 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.commons + +import java.io.* +import java.nio.charset.Charset +import java.nio.file.* +import java.nio.file.attribute.FileAttribute +import java.util.stream.Stream +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteIfExists +import kotlin.io.path.exists +import kotlin.io.path.isSymbolicLink + +// not stored to avoid build-time initialization by native-image +val currentWorkingDir: Path + get() = System.getProperty("user.dir").toPath() + +// unlike `Path.resolve`, this works across file systems if `other` is absolute +fun Path.resolveSafely(other: Path): Path = if (other.isAbsolute) other else resolve(other) + +@Throws(IOException::class) +fun Path.walk(maxDepth: Int = Int.MAX_VALUE, vararg options: FileVisitOption): Stream = + Files.walk(this, maxDepth, *options) + +@Throws(IOException::class) +fun Path.createTempFile( + prefix: String? = null, + suffix: String? = null, + vararg attributes: FileAttribute<*> +): Path = Files.createTempFile(this, prefix, suffix, *attributes) + +@Throws(IOException::class) +fun Path.createParentDirectories(vararg attributes: FileAttribute<*>): Path = apply { + // Files.createDirectories will throw a FileAlreadyExistsException + // if the file exists and is not a directory and symlinks are never + // directories + if (parent?.isSymbolicLink() != true) { + parent?.createDirectories(*attributes) + } +} + +/** [Files.writeString] seems more efficient than [kotlin.io.path.writeText]. */ +@Throws(IOException::class) +fun Path.writeString( + text: String, + charset: Charset = Charsets.UTF_8, + vararg options: OpenOption +): Path = Files.writeString(this, text, charset, *options) + +/** [Files.readString] seems more efficient than [kotlin.io.path.readText]. */ +@Throws(IOException::class) +fun Path.readString(charset: Charset = Charsets.UTF_8): String = Files.readString(this, charset) + +@Throws(IOException::class) +fun Path.deleteRecursively() { + if (exists()) { + walk().use { paths -> paths.sorted(Comparator.reverseOrder()).forEach { it.deleteIfExists() } } + } +} diff --git a/pkl-commons/src/main/kotlin/org/pkl/commons/Strings.kt b/pkl-commons/src/main/kotlin/org/pkl/commons/Strings.kt new file mode 100644 index 00000000..cfe684d6 --- /dev/null +++ b/pkl-commons/src/main/kotlin/org/pkl/commons/Strings.kt @@ -0,0 +1,29 @@ +/** + * 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.commons + +import java.net.URI +import java.nio.file.Path + +fun String.toPath(): Path = Path.of(this) + +/** Copy of org.pkl.core.util.IoUtils.toUri */ +fun String.toUri(): URI = + if (contains(":")) { + URI(this) + } else { + URI(null, null, this, null) + } diff --git a/pkl-commons/src/main/kotlin/org/pkl/commons/Throwables.kt b/pkl-commons/src/main/kotlin/org/pkl/commons/Throwables.kt new file mode 100644 index 00000000..5df93776 --- /dev/null +++ b/pkl-commons/src/main/kotlin/org/pkl/commons/Throwables.kt @@ -0,0 +1,23 @@ +/** + * 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.commons + +import java.io.PrintWriter +import java.io.StringWriter + +/** Same as [Throwable.printStackTrace] except that it prints to a [String]. */ +fun Throwable.printStackTraceToString(): String = + StringWriter().also { printStackTrace(PrintWriter(it)) }.toString() diff --git a/pkl-commons/src/main/kotlin/org/pkl/commons/Uris.kt b/pkl-commons/src/main/kotlin/org/pkl/commons/Uris.kt new file mode 100644 index 00000000..c5aec330 --- /dev/null +++ b/pkl-commons/src/main/kotlin/org/pkl/commons/Uris.kt @@ -0,0 +1,21 @@ +/** + * 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.commons + +import java.net.URI +import java.nio.file.Path + +fun URI.toPath(): Path = Path.of(this) diff --git a/pkl-config-java/gradle.lockfile b/pkl-config-java/gradle.lockfile new file mode 100644 index 00000000..271ca491 --- /dev/null +++ b/pkl-config-java/gradle.lockfile @@ -0,0 +1,40 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.github.ajalt.clikt:clikt-jvm:3.5.1=pklCodegenJava +com.github.ajalt.clikt:clikt:3.5.1=pklCodegenJava +com.squareup:javapoet:1.13.0=pklCodegenJava +com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.14=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +javax.inject:javax.inject:1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:22.3.1=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenJava,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=default,pklCodegenJava,runtimeClasspath,testRuntimeClasspath +empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileOnly,compileOnlyDependenciesMetadata,fatJar,firstPartySourcesJars,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklCoreSourcesJar,runtime,runtimeOnlyDependenciesMetadata,shadow,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-config-java/pkl-config-java.gradle.kts b/pkl-config-java/pkl-config-java.gradle.kts new file mode 100644 index 00000000..7997eced --- /dev/null +++ b/pkl-config-java/pkl-config-java.gradle.kts @@ -0,0 +1,101 @@ +plugins { + pklAllProjects + pklJavaLibrary + pklFatJar + pklPublishLibrary + signing +} + +val pklCodegenJava: Configuration by configurations.creating +val firstPartySourcesJars by configurations.existing + +val generateTestConfigClasses by tasks.registering(JavaExec::class) { + outputs.dir("build/testConfigClasses") + inputs.dir("src/test/resources/codegenPkl") + + classpath = pklCodegenJava + mainClass.set("org.pkl.codegen.java.Main") + args("--output-dir", "build/testConfigClasses") + args("--generate-javadoc") + args(fileTree("src/test/resources/codegenPkl")) +} + +tasks.processTestResources { + dependsOn(generateTestConfigClasses) +} + +tasks.compileTestKotlin { + dependsOn(generateTestConfigClasses) +} + +val bundleTests by tasks.registering(Jar::class) { + from(sourceSets.test.get().output) +} + +// Runs unit tests using jar'd class files as a source. +// This is to test loading the ClassRegistry from within a jar, as opposed to directly from the file system. +val testFromJar by tasks.registering(Test::class) { + dependsOn(bundleTests) + + testClassesDirs = files(tasks.test.get().testClassesDirs) + + classpath = + // compiled test classes + bundleTests.get().outputs.files + + // fat Jar + tasks.shadowJar.get().outputs.files + + // test-only dependencies + // (test dependencies that are also main dependencies must already be contained in fat Jar; + // to verify that, we don't want to include them here) + (configurations.testRuntimeClasspath.get() - configurations.runtimeClasspath.get()) +} + +// TODO: the below snippet causes `./gradlew check` to fail specifically on `pkl-codegen-java:check`. Why? +//tasks.test { +// dependsOn(testFromJar) +//} + +sourceSets.getByName("test") { + java.srcDir("build/testConfigClasses/java") + resources.srcDir("build/testConfigClasses/resources") +} + +dependencies { + // "api" because ConfigEvaluator extends Evaluator + api(project(":pkl-core")) + + implementation(libs.geantyref) + + testImplementation(libs.javaxInject) + + firstPartySourcesJars(project(":pkl-core", "sourcesJar")) + + pklCodegenJava(project(":pkl-codegen-java")) +} + +tasks.shadowJar { + archiveBaseName.set("pkl-config-java-all") +} + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-config-java") + description.set("Java config library based on the Pkl config language.") + } + } + + named("fatJar") { + artifactId = "pkl-config-java-all" + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-config-java") + description.set("Shaded fat Jar for pkl-config-java, a Java config library based on the Pkl config language.") + } + } + } +} + +signing { + sign(publishing.publications["fatJar"]) +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/AbstractConfig.java b/pkl-config-java/src/main/java/org/pkl/config/java/AbstractConfig.java new file mode 100644 index 00000000..40e5177a --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/AbstractConfig.java @@ -0,0 +1,66 @@ +/** + * 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.config.java; + +import java.lang.reflect.Type; +import java.util.Map; +import org.pkl.config.java.mapper.ValueMapper; +import org.pkl.core.Composite; + +abstract class AbstractConfig implements Config { + protected final String qualifiedName; + protected final ValueMapper mapper; + + public AbstractConfig(String qualifiedName, ValueMapper mapper) { + this.qualifiedName = qualifiedName; + this.mapper = mapper; + } + + @Override + public String getQualifiedName() { + return qualifiedName; + } + + @Override + public Config get(String propertyName) { + var childValue = getRawChildValue(propertyName); + var childName = qualifiedName.isEmpty() ? propertyName : qualifiedName + '.' + propertyName; + if (childValue instanceof Composite) { + return new CompositeConfig(childName, mapper, (Composite) childValue); + } + if (childValue instanceof Map) { + return new MapConfig(childName, mapper, (Map) childValue); + } + return new LeafConfig(childName, mapper, childValue); + } + + @Override + public T as(Class type) { + return as((Type) type); + } + + @Override + public T as(Type type) { + return mapper.map(getRawValue(), type); + } + + @Override + public T as(JavaType javaType) { + return as(javaType.getType()); + } + + protected abstract Object getRawChildValue(String property); +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/CompositeConfig.java b/pkl-config-java/src/main/java/org/pkl/config/java/CompositeConfig.java new file mode 100644 index 00000000..ff33bbd4 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/CompositeConfig.java @@ -0,0 +1,48 @@ +/** + * 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.config.java; + +import org.pkl.config.java.mapper.ValueMapper; +import org.pkl.core.Composite; + +class CompositeConfig extends AbstractConfig { + private final Composite composite; + + CompositeConfig(String qualifiedName, ValueMapper mapper, Composite composite) { + super(qualifiedName, mapper); + this.composite = composite; + } + + @Override + public Object getRawValue() { + return composite; + } + + @Override + protected Object getRawChildValue(String propertyName) { + var result = composite.getPropertyOrNull(propertyName); + if (result != null) return result; + + throw new NoSuchChildException( + String.format( + "Node `%s` of type `%s` does not have a property named `%s`. Available properties: %s", + getQualifiedName(), + composite.getClassInfo().getQualifiedName(), + propertyName, + composite.getProperties().keySet()), + propertyName); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/Config.java b/pkl-config-java/src/main/java/org/pkl/config/java/Config.java new file mode 100644 index 00000000..09a12fb0 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/Config.java @@ -0,0 +1,70 @@ +/** + * 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.config.java; + +import java.lang.reflect.Type; +import org.pkl.config.java.mapper.ConversionException; +import org.pkl.core.Evaluator; + +/** + * A root, intermediate, or leaf node in a configuration tree. Child nodes can be obtained by name + * using {@link #get(String)}. To consume the node's composite or scalar value, convert the value to + * the desired Java type, using one of the provided {@link #as} methods. + */ +public interface Config { + /** + * The dot-separated name of this node. For example, the node reached using {@code + * rootNode.get("foo").get("bar")} has qualified name {@code foo.bar}. Returns the empty String + * for the root node itself. + */ + String getQualifiedName(); + + /** + * The raw value of this node, as provided by {@link Evaluator}. Typically, a node's value is not + * consumed directly, but converted to the desired Java type using {@link #as}. + */ + Object getRawValue(); + + /** + * Returns the child node with the given unqualified name. + * + * @throws NoSuchChildException if a child with the given name does not exist + */ + Config get(String childName); + + /** + * Converts this node's value to the given {@link Class}. + * + * @throws ConversionException if the value cannot be converted to the given type + */ + T as(Class type); + + /** + * Converts this node's value to the given {@link Type}. + * + *

Note that usages of this methods are not type safe. + * + * @throws ConversionException if the value cannot be converted to the given type + */ + T as(Type type); + + /** + * Converts this node's value to the given {@link JavaType}. + * + * @throws ConversionException if the value cannot be converted to the given type + */ + T as(JavaType type); +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluator.java b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluator.java new file mode 100644 index 00000000..2378d739 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluator.java @@ -0,0 +1,54 @@ +/** + * 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.config.java; + +import org.pkl.config.java.mapper.ValueMapper; +import org.pkl.core.ModuleSource; + +/** + * An evaluator that returns a {@link Config} tree. + * + *

Use {@link ConfigEvaluatorBuilder} to create instances of this type, configured according to + * your needs. + */ +public interface ConfigEvaluator extends AutoCloseable { + /** Shorthand for {@code ConfigEvaluatorBuilder.preconfigured().build()}. */ + static ConfigEvaluator preconfigured() { + return ConfigEvaluatorBuilder.preconfigured().build(); + } + + /** Returns the underlying value mapper of this evaluator. */ + ValueMapper getValueMapper(); + + /** + * Returns a new config evaluator with the same underlying evaluator and the given value mapper. + */ + ConfigEvaluator setValueMapper(ValueMapper mapper); + + /** Evaluates the given module source into a {@link Config} tree. */ + Config evaluate(ModuleSource moduleSource); + + /** + * Releases all resources held by this evaluator. If an {@code evaluate} method is currently + * executing, this method blocks until cancellation of that execution has completed. + * + *

Once an evaluator has been closed, it can no longer be used, and calling {@code evaluate} + * methods will throw {@link IllegalStateException}. However, objects previously returned by + * {@code evaluate} methods remain valid. + */ + @Override + void close(); +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorBuilder.java b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorBuilder.java new file mode 100644 index 00000000..c546a5f1 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorBuilder.java @@ -0,0 +1,323 @@ +/** + * 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.config.java; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.pkl.config.java.mapper.ValueMapperBuilder; +import org.pkl.core.EvaluatorBuilder; +import org.pkl.core.SecurityManager; +import org.pkl.core.StackFrameTransformer; +import org.pkl.core.project.DeclaredDependencies; +import org.pkl.core.project.Project; +import org.pkl.core.util.Nullable; + +/** A builder for {@link ConfigEvaluator}s. */ +@SuppressWarnings({"UnusedReturnValue", "unused"}) +public final class ConfigEvaluatorBuilder { + private EvaluatorBuilder evaluatorBuilder; + private ValueMapperBuilder mapperBuilder; + + private ConfigEvaluatorBuilder( + EvaluatorBuilder evaluatorBuilder, ValueMapperBuilder mapperBuilder) { + this.evaluatorBuilder = evaluatorBuilder; + this.mapperBuilder = mapperBuilder; + } + + /** Creates a builder with preconfigured module evaluator and value mapper builders. */ + public static ConfigEvaluatorBuilder preconfigured() { + return new ConfigEvaluatorBuilder( + EvaluatorBuilder.preconfigured(), ValueMapperBuilder.preconfigured()); + } + + /** Creates a builder with unconfigured module evaluator and value mapper builders. */ + public static ConfigEvaluatorBuilder unconfigured() { + return new ConfigEvaluatorBuilder( + EvaluatorBuilder.unconfigured(), ValueMapperBuilder.unconfigured()); + } + + /** + * Sets the underlying module evaluator builder. When a config evaluator is built, the underlying + * module evaluator comes from this builder. + */ + public ConfigEvaluatorBuilder setEvaluatorBuilder(EvaluatorBuilder evaluatorBuilder) { + this.evaluatorBuilder = evaluatorBuilder; + return this; + } + + /** Returns the currently set module evaluator builder. */ + public EvaluatorBuilder getEvaluatorBuilder() { + return evaluatorBuilder; + } + + /** + * Sets the underlying value mapper builder. When a config evaluator is built, the underlying + * value mapper comes from this builder. + */ + public ConfigEvaluatorBuilder setValueMapperBuilder(ValueMapperBuilder mapperBuilder) { + this.mapperBuilder = mapperBuilder; + return this; + } + + /** Returns the currently set value mapper builder. */ + public ValueMapperBuilder getValueMapperBuilder() { + return mapperBuilder; + } + + /** + * Adds the given environment variable, overriding any environment variable previously added under + * the same name. + * + *

Modules can read environment variables with {@code read("env:")}. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder addEnvironmentVariable(String name, String value) { + evaluatorBuilder.addEnvironmentVariable(name, value); + return this; + } + + /** + * Adds the given environment variables, overriding any environment variables previously added + * under the same name. + * + *

Modules can read environment variables with {@code read("env:")}. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder addEnvironmentVariables(Map envVars) { + evaluatorBuilder.addEnvironmentVariables(envVars); + return this; + } + + /** + * Removes any existing environment variables, then adds the given environment variables. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder setEnvironmentVariables(Map envVars) { + evaluatorBuilder.setEnvironmentVariables(envVars); + return this; + } + + /** + * Returns the currently set environment variables. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public Map getEnvironmentVariables() { + return evaluatorBuilder.getEnvironmentVariables(); + } + + /** + * Adds the given external property, overriding any property previously set under the same name. + * + *

Modules can read external properties with {@code read("prop:")}. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder addExternalProperty(String name, String value) { + evaluatorBuilder.addExternalProperty(name, value); + return this; + } + + /** + * Adds the given external properties, overriding any properties previously set under the same + * name. + * + *

Modules can read external properties with {@code read("prop:")}. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder addExternalProperties(Map properties) { + evaluatorBuilder.addExternalProperties(properties); + return this; + } + + /** + * Removes any existing external properties, then adds the given properties. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder setExternalProperties(Map properties) { + evaluatorBuilder.setExternalProperties(properties); + return this; + } + + /** + * Returns the currently set external properties. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public Map getExternalProperties() { + return evaluatorBuilder.getExternalProperties(); + } + + /** + * Sets the given security manager, replacing any previously set security manager. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder setSecurityManager(SecurityManager manager) { + evaluatorBuilder.setSecurityManager(manager); + return this; + } + + /** + * Returns the currently set security manager. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public @Nullable SecurityManager getSecurityManager() { + return evaluatorBuilder.getSecurityManager(); + } + + /** + * Sets the given stack frame transformer, replacing any previously set transformer. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder setStackFrameTransformer( + StackFrameTransformer stackFrameTransformer) { + evaluatorBuilder.setStackFrameTransformer(stackFrameTransformer); + return this; + } + + /** + * Returns the currently set stack frame transformer. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public @Nullable StackFrameTransformer getStackFrameTransformer() { + return evaluatorBuilder.getStackFrameTransformer(); + } + + /** + * Sets the project for the evaluator, without applying evaluator settings in the project. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder setProjectDependencies(DeclaredDependencies dependencies) { + evaluatorBuilder.setProjectDependencies(dependencies); + return this; + } + + /** + * Sets the project for the evaluator, and applies any settings if set. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public ConfigEvaluatorBuilder applyFromProject(Project project) { + evaluatorBuilder.applyFromProject(project); + return this; + } + + /** + * Sets an evaluation timeout to be enforced by the {@link ConfigEvaluator}'s {@code evaluate} + * methods. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder setTimeout(Duration timeout) { + evaluatorBuilder.setTimeout(timeout); + return this; + } + + /** + * Sets the set of URI patterns to be allowed when importing modules. + * + *

This is a convenieince method that delegates to the underlying evaluator builder. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public ConfigEvaluatorBuilder setAllowedModules(Collection patterns) { + evaluatorBuilder.setAllowedModules(patterns); + return this; + } + + /** + * Returns the set of patterns to be allowed when importing modules. + * + *

This is a convenieince method that delegates to the underlying evaluator builder. + */ + public List getAllowedModules() { + return evaluatorBuilder.getAllowedModules(); + } + + /** + * Sets the set of URI patterns to be allowed when reading resources. + * + *

This is a convenieince method that delegates to the underlying evaluator builder. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public ConfigEvaluatorBuilder setAllowedResources(Collection patterns) { + evaluatorBuilder.setAllowedResources(patterns); + return this; + } + + /** + * Returns the set of patterns to be allowed when reading resources. + * + *

This is a convenieince method that delegates to the underlying evaluator builder. + */ + public List getAllowedResources() { + return evaluatorBuilder.getAllowedResources(); + } + + /** + * Sets the root directory, which restricts access to file-based modules and resources located + * under this directory. + * + *

This is a convenieince method that delegates to the underlying evaluator builder. + */ + public ConfigEvaluatorBuilder setRootDir(@Nullable Path rootDir) { + evaluatorBuilder.setRootDir(rootDir); + return this; + } + + /** + * Returns the currently set root directory, if set. + * + *

This is a convenieince method that delegates to the underlying evaluator builder. + */ + public @Nullable Path getRootDir() { + return evaluatorBuilder.getRootDir(); + } + + /** + * Returns the currently set evaluation timeout. + * + *

This is a convenience method that delegates to the underlying evaluator builder. + */ + public @Nullable Duration getTimeout() { + return evaluatorBuilder.getTimeout(); + } + + /** + * Builds a config evaluator whose underlying module evaluator and value mapper is built using the + * configured builders. The same builder can be used to build multiple config evaluators. + */ + public ConfigEvaluator build() { + return new ConfigEvaluatorImpl(evaluatorBuilder.build(), mapperBuilder.build()); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorImpl.java b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorImpl.java new file mode 100644 index 00000000..481c2d8e --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/ConfigEvaluatorImpl.java @@ -0,0 +1,51 @@ +/** + * 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.config.java; + +import org.pkl.config.java.mapper.ValueMapper; +import org.pkl.core.Evaluator; +import org.pkl.core.ModuleSource; + +final class ConfigEvaluatorImpl implements ConfigEvaluator { + private final Evaluator evaluator; + private final ValueMapper mapper; + + ConfigEvaluatorImpl(Evaluator evaluator, ValueMapper mapper) { + this.evaluator = evaluator; + this.mapper = mapper; + } + + @Override + public Config evaluate(ModuleSource moduleSource) { + var module = evaluator.evaluate(moduleSource); + return new CompositeConfig("", mapper, module); + } + + @Override + public ValueMapper getValueMapper() { + return mapper; + } + + @Override + public ConfigEvaluator setValueMapper(ValueMapper mapper) { + return new ConfigEvaluatorImpl(evaluator, mapper); + } + + @Override + public void close() { + evaluator.close(); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/InvalidMappingException.java b/pkl-config-java/src/main/java/org/pkl/config/java/InvalidMappingException.java new file mode 100644 index 00000000..27070187 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/InvalidMappingException.java @@ -0,0 +1,53 @@ +/** + * 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.config.java; + +import java.lang.reflect.Type; + +/** + * Thrown by {@link Config#as(Type)} when the determined Java class for a Pkl value cannot be found + * on the classpath. + * + *

When this happens, the most likely explanation is that the generated code is not up-to-date. + */ +public class InvalidMappingException extends RuntimeException { + String pklName; + + String javaName; + + public InvalidMappingException(String pklName, String javaName, Exception cause) { + super(cause); + this.pklName = pklName; + this.javaName = javaName; + } + + @Override + public String getMessage() { + return "Did not find expected Java class `" + + javaName + + "` on the classpath for Pkl class `" + + pklName + + "`. Is your generated code up to date?"; + } + + public String getPklName() { + return pklName; + } + + public String getJavaName() { + return javaName; + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/JavaType.java b/pkl-config-java/src/main/java/org/pkl/config/java/JavaType.java new file mode 100644 index 00000000..7fc0166a --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/JavaType.java @@ -0,0 +1,179 @@ +/** + * 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.config.java; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import org.pkl.config.java.mapper.Types; +import org.pkl.core.Pair; +import org.pkl.core.util.Nullable; + +/** + * Runtime representation of a possibly parameterized Java type. Factory methods are provided to + * ease construction of commonly used Java standard library types. For example, a {@code JavaType} + * for {@code List} can be constructed using {@code JavaType.listOf(String.class)}. + * + *

Parameterizations of other types can be constructed using the super type token idiom: + * + *

+ * + *

{@code
+ * class Mapping {} // some user-defined type
+ * Config config = ...
+ *
+ * Mapping container = config.as(
+ *   // construct super type token for Mapping
+ *   new JavaType>() {}
+ * );
+ * }
+ * + * @param the type reified by this {@code JavaType} instance + */ +@SuppressWarnings("unused") +public class JavaType { + private final Type type; + + protected JavaType() { + var superclass = getClass().getGenericSuperclass(); + if (superclass instanceof Class) { + throw new IllegalStateException("JavaType token must be parameterized."); + } + type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; + } + + private JavaType(Type type) { + this.type = type; + } + + /** Creates a {@code JavaType} for the given {@code Class}. */ + public static JavaType of(Class type) { + return new JavaType<>(type); + } + + /** + * Creates a {@code JavaType} for the given {@code Type}. + * + *

Note: This method is not type safe, and should be used with care. + */ + public static JavaType of(Type type) { + return new JavaType<>(type); + } + + /** Creates an {@link Optional} type with the given element type. */ + public static JavaType> optionalOf(Class elementType) { + return JavaType.of(Types.optionalOf(elementType)); + } + + /** Creates an {@link Optional} type with the given element type. */ + public static JavaType> optionalOf(JavaType elementType) { + return JavaType.of(Types.optionalOf(elementType.type)); + } + + /** Creates a {@link Pair} type with the given first and second element types. */ + public static JavaType> pairOf(Class firstType, Class secondType) { + return JavaType.of(Types.pairOf(firstType, secondType)); + } + + /** Creates a {@link Pair} type with the given first and second element types. */ + public static JavaType> pairOf(JavaType firstType, JavaType secondType) { + return JavaType.of(Types.pairOf(firstType.type, secondType.type)); + } + + /** Creates an array type with the given element type. */ + public static JavaType arrayOf(Class elementType) { + return JavaType.of(Types.arrayOf(elementType)); + } + + /** Creates an array type with the given element type. */ + public static JavaType arrayOf(JavaType elementType) { + return JavaType.of(Types.arrayOf(elementType.type)); + } + + /** Creates an {@link Iterable} type with the given element type. */ + public static JavaType> iterableOf(Class elementType) { + return JavaType.of(Types.iterableOf(elementType)); + } + + /** Creates an {@link Iterable} type with the given element type. */ + public static JavaType> iterableOf(JavaType elementType) { + return JavaType.of(Types.iterableOf(elementType.type)); + } + + /** Creates a {@link Collection} type with the given element type. */ + public static JavaType> collectionOf(Class elementType) { + return JavaType.of(Types.collectionOf(elementType)); + } + + /** Creates a {@link Collection} type with the given element type. */ + public static JavaType> collectionOf(JavaType elementType) { + return JavaType.of(Types.collectionOf(elementType.type)); + } + + /** Creates a {@link List} type with the given element type. */ + public static JavaType> listOf(Class elementType) { + return JavaType.of(Types.listOf(elementType)); + } + + /** Creates a {@link List} type with the given element type. */ + public static JavaType> listOf(JavaType elementType) { + return JavaType.of(Types.listOf(elementType.type)); + } + + /** Creates a {@link Set} type with the given element type. */ + public static JavaType> setOf(Class elementType) { + return JavaType.of(Types.setOf(elementType)); + } + + /** Creates a {@link Set} type with the given element type. */ + public static JavaType> setOf(JavaType elementType) { + return JavaType.of(Types.setOf(elementType.type)); + } + + /** Creates a {@link Map} type with the given key and value types. */ + public static JavaType> mapOf(Class keyType, Class valueType) { + return JavaType.of(Types.mapOf(keyType, valueType)); + } + + /** Creates a {@link Map} type with the given key and value types. */ + public static JavaType> mapOf(JavaType keyType, JavaType valueType) { + return JavaType.of(Types.mapOf(keyType.type, valueType.type)); + } + + /** Returns the underlying {@link Type}. */ + public Type getType() { + return type; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof JavaType)) return false; + + var other = (JavaType) obj; + return type.equals(other.type); + } + + @Override + public int hashCode() { + return type.hashCode(); + } + + @Override + public String toString() { + return type.toString(); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/LeafConfig.java b/pkl-config-java/src/main/java/org/pkl/config/java/LeafConfig.java new file mode 100644 index 00000000..9e974fab --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/LeafConfig.java @@ -0,0 +1,42 @@ +/** + * 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.config.java; + +import org.pkl.config.java.mapper.ValueMapper; +import org.pkl.core.PClassInfo; + +class LeafConfig extends AbstractConfig { + private final Object value; + + LeafConfig(String qualifiedName, ValueMapper mapper, Object value) { + super(qualifiedName, mapper); + this.value = value; + } + + @Override + public Object getRawValue() { + return value; + } + + @Override + protected Object getRawChildValue(String propertyName) { + throw new NoSuchChildException( + String.format( + "Leaf node `%s` of type `%s` does not have a child named `%s`.", + qualifiedName, PClassInfo.forValue(value).getQualifiedName(), propertyName), + propertyName); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/MapConfig.java b/pkl-config-java/src/main/java/org/pkl/config/java/MapConfig.java new file mode 100644 index 00000000..6e19a1a0 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/MapConfig.java @@ -0,0 +1,46 @@ +/** + * 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.config.java; + +import java.util.Map; +import org.pkl.config.java.mapper.ValueMapper; +import org.pkl.core.PClassInfo; + +class MapConfig extends AbstractConfig { + private final Map map; + + MapConfig(String qualifiedName, ValueMapper mapper, Map map) { + super(qualifiedName, mapper); + this.map = map; + } + + @Override + public Object getRawValue() { + return map; + } + + @Override + protected Object getRawChildValue(String propertyName) { + var result = map.get(propertyName); + if (result != null) return result; + + throw new NoSuchChildException( + String.format( + "Node `%s` of type `%s` does not have a key named `%s`. Available keys: %s", + getQualifiedName(), PClassInfo.Map.getQualifiedName(), propertyName, map.keySet()), + propertyName); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/NoSuchChildException.java b/pkl-config-java/src/main/java/org/pkl/config/java/NoSuchChildException.java new file mode 100644 index 00000000..af42dab4 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/NoSuchChildException.java @@ -0,0 +1,33 @@ +/** + * 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.config.java; + +/** + * Thrown by {@link Config#get} when a child node with the given name does not exist, or when the + * current config node is a leaf node. + */ +public class NoSuchChildException extends RuntimeException { + private final String childName; + + public NoSuchChildException(String message, String childName) { + super(message); + this.childName = childName; + } + + public String getChildName() { + return childName; + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ClassRegistry.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ClassRegistry.java new file mode 100644 index 00000000..e1e44786 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ClassRegistry.java @@ -0,0 +1,91 @@ +/** + * 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.config.java.mapper; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import org.pkl.config.java.InvalidMappingException; +import org.pkl.core.PClassInfo; +import org.pkl.core.util.Nullable; + +/** + * Describes mappings of Pkl class names to their corresponding Java classes. + * + *

This is used by {@link ValueMapper} to pick the correct Java class when mapping Pkl into Java. + * + *

Mappings are determined by scanning the + * /META-INF/org/pkl/config/java/mapper/classes directory for properties files. + * + *

Property files should be in the form of + * org.pkl.config.java.mapper.[PKL_CLASS_NAME]=[JAVA_REFLECTION_CLASS_NAME] + * + *

Mappings are optional, and only required if Pkl types are polymorphic. + * + *

They are generated by the Java and Kotlin code generators, and can be handwritten if not using + * codegen. + */ +public class ClassRegistry { + + private static final Properties classMappings = new Properties(); + + private static final Object lock = new Object(); + + private static final String CLASSES_DIRECTORY = "/META-INF/org/pkl/config/java/mapper/classes"; + + private static final String PREFIX = "org.pkl.config.java.mapper."; + + private static final Set loadedModules = new HashSet<>(); + + private ClassRegistry() {} + + static @Nullable Class get(PClassInfo pklClassInfo) { + var pklModuleName = pklClassInfo.getModuleName(); + var pklClassName = pklClassInfo.getQualifiedName(); + initClassMappings(pklModuleName); + var javaName = classMappings.getProperty(PREFIX + pklClassInfo.getQualifiedName()); + if (javaName == null) { + return null; + } + try { + return Class.forName(javaName); + } catch (ClassNotFoundException e) { + throw new InvalidMappingException(pklClassName, javaName, e); + } + } + + private static void initClassMappings(String pklModuleName) { + synchronized (lock) { + if (loadedModules.contains(pklModuleName)) { + return; + } + loadedModules.add(pklModuleName); + var url = + ClassRegistry.class.getResourceAsStream( + CLASSES_DIRECTORY + "/" + pklModuleName + ".properties"); + if (url == null) { + return; + } + try { + classMappings.load(url); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Conversion.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Conversion.java new file mode 100644 index 00000000..4fad2316 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Conversion.java @@ -0,0 +1,57 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Type; +import org.pkl.core.PClassInfo; + +/** + * Describes a conversion from a Pkl source type to a (possibly parameterized) Java target type, + * performed by the given {@link Converter}. + * + * @param Java type representing the Pkl source type + * @param Java target type + */ +public final class Conversion { + public final PClassInfo sourceType; + public final Type targetType; + public final Converter converter; + + private Conversion( + PClassInfo sourceType, Type targetType, Converter converter) { + this.sourceType = sourceType; + this.targetType = targetType; + this.converter = converter; + } + + /** + * Creates a conversion from the given Pkl source type to the given (possibly parameterized) Java + * type, using the given converter. + */ + public static Conversion of( + PClassInfo sourceType, Type targetType, Converter converter) { + return new Conversion<>(sourceType, targetType, converter); + } + + /** + * Creates a conversion from the given Pkl source type to the given non-parameterized Java type, + * using the given converter. This overload is provided to allow for better type inference. + */ + public static Conversion of( + PClassInfo sourceType, Class targetType, Converter converter) { + return new Conversion<>(sourceType, targetType, converter); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConversionException.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConversionException.java new file mode 100644 index 00000000..33298685 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConversionException.java @@ -0,0 +1,27 @@ +/** + * 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.config.java.mapper; + +/** Thrown when a {@link ValueMapper} conversion fails. */ +public class ConversionException extends RuntimeException { + public ConversionException(String message) { + super(message); + } + + public ConversionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Conversions.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Conversions.java new file mode 100644 index 00000000..cb8f0f09 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Conversions.java @@ -0,0 +1,337 @@ +/** + * 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.config.java.mapper; + +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.*; +import org.pkl.core.*; + +/** Predefined conversions for scalar types. */ +public final class Conversions { + private Conversions() {} + + /** + * Conversion from {@code pkl.base#Int} to {@link Byte}. Throws {@link ConversionException} if the + * value is too large. + */ + public static final Conversion pIntToByte = + Conversion.of( + PClassInfo.Int, + byte.class, + (value, mapper) -> { + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new ConversionException( + String.format( + "Cannot convert pkl.base#Int `%s` to java.lang.Byte because it is outside range `%s..%s`", + value, Byte.MIN_VALUE, Byte.MAX_VALUE)); + } + return value.byteValue(); + }); + + /** + * Conversion from {@code pkl.base#Int} to {@link Short}. Throws {@link ConversionException} if + * the value is too large. + */ + public static final Conversion pIntToShort = + Conversion.of( + PClassInfo.Int, + short.class, + (value, mapper) -> { + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new ConversionException( + String.format( + "Cannot convert pkl.base#Int `%s` to java.lang.Short because it is outside range `%s..%s`", + value, Short.MIN_VALUE, Short.MAX_VALUE)); + } + return value.shortValue(); + }); + + /** + * Conversion from {@code pkl.base#Int} to {@link Integer}. Throws {@link ConversionException} if + * the value is too large. + */ + public static final Conversion pIntToInteger = + Conversion.of( + PClassInfo.Int, + int.class, + (value, mapper) -> { + if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { + throw new ConversionException( + String.format( + "Cannot convert pkl.base#Int `%s` to java.lang.Integer because it is outside range `%s..%s`", + value, Integer.MIN_VALUE, Integer.MAX_VALUE)); + } + return value.intValue(); + }); + + /** Conversion from {@code pkl.base#Int} to {@link Float}. May lose precision. */ + public static final Conversion pIntToFloat = + Conversion.of(PClassInfo.Int, float.class, (value, mapper) -> value.floatValue()); + + /** Conversion from {@code pkl.base#Int} to {@link Double}. May lose precision. */ + public static final Conversion pIntToDouble = + Conversion.of(PClassInfo.Int, double.class, (value, mapper) -> value.doubleValue()); + + /** Conversion from {@code pkl.base#Int} to {@link BigInteger}. */ + public static final Conversion pIntToBigInteger = + Conversion.of(PClassInfo.Int, BigInteger.class, (value, mapper) -> BigInteger.valueOf(value)); + + /** Conversion from {@code pkl.base#Int} to {@link BigDecimal}. */ + public static final Conversion pIntToBigDecimal = + Conversion.of(PClassInfo.Int, BigDecimal.class, (value, mapper) -> BigDecimal.valueOf(value)); + + /** Conversion from {@code pkl.base#Float} to {@link Float}. May lose precision. */ + public static final Conversion pFloatToFloat = + Conversion.of(PClassInfo.Float, float.class, (value, mapper) -> value.floatValue()); + + /** Conversion from {@code pkl.base#Float} to {@link BigDecimal}. */ + public static final Conversion pFloatToBigDecimal = + Conversion.of( + PClassInfo.Float, BigDecimal.class, (value, mapper) -> BigDecimal.valueOf(value)); + + /** + * Conversion from {@code pkl.base#String} to {@link Character}. Throws {@link + * ConversionException} if the String value is not of length one. + */ + public static final Conversion pStringToCharacter = + Conversion.of( + PClassInfo.String, + Character.class, + (value, mapper) -> { + if (value.length() != 1) { + throw new ConversionException( + String.format( + "Cannot convert pkl.base#String `%s` to java.lang.Character because it is not of length 1.", + value)); + } + return value.charAt(0); + }); + + /** + * Conversion from {@code pkl.base#String} to {@link URI}. Throws {@link ConversionException} if + * the String value is not a syntactically valid URI. + */ + public static final Conversion pStringToURI = + Conversion.of( + PClassInfo.String, + URI.class, + (value, mapper) -> { + try { + return new URI(value); + } catch (URISyntaxException e) { + throw new ConversionException( + "Failed to convert `pkl.base#String` to `java.net.URI`.", e); + } + }); + + /** + * Conversion from {@code pkl.base#String} to {@link URL}. Throws {@link ConversionException} if + * the String value is not a syntactically valid URL. + */ + public static final Conversion pStringToURL = + Conversion.of( + PClassInfo.String, + URL.class, + (value, mapper) -> { + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new ConversionException( + "Failed to convert `pkl.base#String` to `java.net.URL`.", e); + } + }); + + /** Conversion from {@code pkl.base#String} to {@link File}. */ + public static final Conversion pStringToFile = + Conversion.of(PClassInfo.String, File.class, (value, mapper) -> new File(value)); + + /** + * Conversion from {@code pkl.base#String} to {@link Path}. Throws {@link ConversionException} if + * the String value is not a syntactically valid path. + */ + public static final Conversion pStringToPath = + Conversion.of( + PClassInfo.String, + Path.class, + (value, mapper) -> { + try { + return Path.of(value); + } catch (InvalidPathException e) { + throw new ConversionException( + "Failed to convert `pkl.base#String` to `java.nio.file.Path`.", e); + } + }); + + /** Conversion from {@code pkl.base#String} to {@link Pattern}. */ + public static final Conversion pStringToPattern = + Conversion.of( + PClassInfo.String, + Pattern.class, + (value, mapper) -> { + try { + return Pattern.compile(value); + } catch (PatternSyntaxException e) { + throw new ConversionException( + "Failed to convert `pkl.base#String` to `java.util.regex.Pattern`.", e); + } + }); + + /** Conversion from {@code pkl.base#Regex} to {@link String}. */ + public static final Conversion pRegexToString = + Conversion.of(PClassInfo.Regex, String.class, (value, mapper) -> value.pattern()); + + /** Conversion from {@code pkl.base#Duration} to {@link java.time.Duration}. */ + public static final Conversion pDurationToDuration = + Conversion.of( + PClassInfo.Duration, java.time.Duration.class, (value, mapper) -> value.toJavaDuration()); + + /** Conversion from {@code pkl.semver#Version} to {@link Version}. */ + // Cannot leave this to `ConverterFactories.pObjectToDataObject` + // because `Version` is part of pkl-core and thus cannot be annotated with `@Named`. + public static final Conversion pVersionToVersion = + Conversion.of( + PClassInfo.Version, + Version.class, + (value, mapper) -> { + try { + return new Version( + Math.toIntExact((Long) value.getProperty("major")), + Math.toIntExact((Long) value.getProperty("minor")), + Math.toIntExact((Long) value.getProperty("patch")), + (String) value.get("preRelease"), + (String) value.get("build")); + } catch (ArithmeticException e) { + throw new ConversionException( + "Failed to convert `pkl.semver#Version` to `org.pkl.core.Version`.", e); + } + }); + + public static final Conversion pVersionToString = + Conversion.of( + PClassInfo.Version, + String.class, + (value, mapper) -> { + var builder = new StringBuilder(); + builder.append(value.get("major")); + builder.append('.'); + builder.append(value.get("minor")); + builder.append('.'); + builder.append(value.get("patch")); + var preRelease = value.get("preRelease"); + if (preRelease != null) { + builder.append('-'); + builder.append(preRelease); + } + var build = value.get("build"); + if (build != null) { + builder.append('+'); + builder.append(build); + } + return builder.toString(); + }); + + public static final Conversion pStringToVersion = + Conversion.of( + PClassInfo.String, + Version.class, + (value, mapper) -> { + try { + return Version.parse(value); + } catch (IllegalArgumentException e) { + throw new ConversionException( + "Failed to convert `pkl.base#String` to `org.pkl.core.Version`.", e); + } + }); + + /** + * Identity conversions used when the Java representation of the Pkl type matches the target type + * or when the target type is {@link Object}. + */ + public static final Collection> identities = + List.of( + Conversion.of(PClassInfo.Boolean, boolean.class, Converter.identity()), + Conversion.of(PClassInfo.Boolean, Object.class, Converter.identity()), + Conversion.of(PClassInfo.String, String.class, Converter.identity()), + Conversion.of(PClassInfo.String, Object.class, Converter.identity()), + Conversion.of(PClassInfo.Int, long.class, Converter.identity()), + Conversion.of(PClassInfo.Int, Number.class, Converter.identity()), + Conversion.of(PClassInfo.Int, Object.class, Converter.identity()), + Conversion.of(PClassInfo.Float, double.class, Converter.identity()), + Conversion.of(PClassInfo.Float, Number.class, Converter.identity()), + Conversion.of(PClassInfo.Float, Object.class, Converter.identity()), + Conversion.of(PClassInfo.Duration, Duration.class, Converter.identity()), + Conversion.of(PClassInfo.Duration, Object.class, Converter.identity()), + Conversion.of(PClassInfo.DataSize, DataSize.class, Converter.identity()), + Conversion.of(PClassInfo.DataSize, Object.class, Converter.identity()), + Conversion.of(PClassInfo.Module, PModule.class, Converter.identity()), + Conversion.of(PClassInfo.Module, Object.class, Converter.identity()), + Conversion.of(PClassInfo.Class, PClass.class, Converter.identity()), + Conversion.of(PClassInfo.Class, Object.class, Converter.identity()), + Conversion.of(PClassInfo.Regex, Pattern.class, Converter.identity()), + Conversion.of(PClassInfo.Regex, Object.class, Converter.identity()), + Conversion.of(PClassInfo.Null, PNull.class, Converter.identity()) + // PClassInfo.Null -> Object.class is covered by PNullToAny (returns null rather than + // PNull.getInstance()) + ); + + /** Numeric conversions. Does not include identity conversions. */ + public static final Collection> numeric = + List.of( + pIntToByte, + pIntToShort, + pIntToInteger, + pIntToFloat, + pIntToDouble, + pIntToBigInteger, + pIntToBigDecimal, + pFloatToFloat, + pFloatToBigDecimal); + + /** Conversions that don't fit any other category. */ + public static final Collection> misc = + List.of( + pStringToCharacter, + pStringToURI, + pStringToURL, + pStringToFile, + pStringToPath, + pStringToPattern, + pRegexToString, + pDurationToDuration, + pVersionToVersion, + pVersionToString, + pStringToVersion); + + /** All conversions defined in this class. */ + public static final Collection> all = collectAll(); + + private static Collection> collectAll() { + var result = new ArrayList>(identities.size() + numeric.size() + misc.size()); + result.addAll(identities); + result.addAll(numeric); + result.addAll(misc); + return Collections.unmodifiableList(result); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Converter.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Converter.java new file mode 100644 index 00000000..80c714ec --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Converter.java @@ -0,0 +1,36 @@ +/** + * 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.config.java.mapper; + +/** + * Converter for a particular source and target type. + * + * @param the converter's source type + * @param the converter's target type + */ +@FunctionalInterface +public interface Converter { + /** + * Converts the given value. The given {@link ValueMapper} can be used to convert nested values of + * composite values (objects, collections, etc.). + */ + T convert(S value, ValueMapper valueMapper); + + /** Returns an identity converter for the requested type. */ + static Converter identity() { + return (value, valueMapper) -> value; + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConverterFactories.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConverterFactories.java new file mode 100644 index 00000000..0f0cd317 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConverterFactories.java @@ -0,0 +1,103 @@ +/** + * 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.config.java.mapper; + +import java.util.*; +import org.pkl.core.PObject; +import org.pkl.core.Pair; + +/** Predefined conversions for composite types (objects, collections, etc.). */ +public final class ConverterFactories { + private ConverterFactories() {} + + /** + * Conversion from {@code pkl.base#Null} to any non-primitive type. The conversion result is + * always {@code null}. + */ + public static final ConverterFactory pNullToAny = new PNullToAny(); + + /** Identity conversion for {@link PObject}. */ + public static final ConverterFactory pObjectToPObject = new PObjectToPObject(); + + /** + * Conversion from {@code pkl.base#String} to Java Enum type. If there is no exact match between + * string and enum value, some variations are tried. For example, both {@code "house-of-cards"} + * and {@code "house of cards"} will be successfully matched to enum value {@code HOUSE_OF_CARDS}. + */ + public static final ConverterFactory pStringToEnum = new PStringToEnum(); + + /** + * Conversion from any Pkl value to {@link java.util.Optional}. Returns an empty optional for + * {@code pkl.base#Null} and a present optional otherwise. + */ + public static final ConverterFactory pAnyToOptional = new PAnyToOptional(); + + /** Conversion from {@code pkl.base#Collection} to Java primitive or object array. */ + public static final ConverterFactory pCollectionToArray = new PCollectionToArray(); + + /** + * Conversion from {@code pkl.base#Collection} to {@link Collection}. The concrete implementation + * type is determined using {@link TypeMapping}s. + */ + public static final ConverterFactory pCollectionToCollection = new PCollectionToCollection(); + + /** + * Conversion from {@code pkl.base#Map} to {@link Map}. The concrete implementation type is + * determined using {@link TypeMapping}s. + */ + public static final ConverterFactory pMapToMap = new PMapToMap(); + + /** + * Conversion from Pkl module or object to Java data object. The conversion is performed as + * follows: + * + *

+ * + *

    + *
  1. Find the Java class constructor with the highest number of parameters. + *
  2. Correlate constructor parameters with Pkl object properties by name. + *
  3. Convert each Pkl property value to the corresponding constructor parameter's type. + *
  4. Invoke the constructor. + *
+ * + *

Dynamic and class based Pkl objects are equally supported. The Pkl object must contain all + * properties defined by the Java class constructor. Any additional Pkl object properties are + * ignored. + * + *

Unless the Java 8+ compiler option {@code -parameters} is set, constructor parameters must + * be annotated with {@link Named} or {@code javax.inject.Named}. + */ + public static final ConverterFactory pObjectToDataObject = new PObjectToDataObject(); + + public static final ConverterFactory pObjectToMap = new PObjectToMap(); + + /** Conversion from {@code pkl.base#Pair} to {@link Pair}. */ + public static final ConverterFactory pPairToPair = new PPairToPair(); + + /** All conversions defined in this class. */ + public static final Collection all = + List.of( + pAnyToOptional, + pNullToAny, + pObjectToPObject, + pStringToEnum, + pCollectionToArray, + pCollectionToCollection, + pMapToMap, + pObjectToDataObject, + pObjectToMap, + pPairToPair); +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConverterFactory.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConverterFactory.java new file mode 100644 index 00000000..7846a6ee --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ConverterFactory.java @@ -0,0 +1,51 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Type; +import java.util.Optional; +import java.util.function.Predicate; +import org.pkl.core.PClassInfo; + +/** + * A factory for {@link Converter}s. Used to implement conversions to generic Java classes. In such + * a case a single {@link Converter} does not suffice. Instead the factory creates a new converter + * for every parameterization of the target type. Once created, the converter is cached for later + * use, and the factory is never again invoked for the same parameterized target type. + * + *

For best performace, all introspection of target types (for example using {@link Reflection}) + * should happen in the factory rather then the returned converters. + */ +@FunctionalInterface +public interface ConverterFactory { + /** + * Returns a converter for the given source and target types, or {@code Optional.empty()} if the + * factory cannot handle the requested types. + */ + // idea: return Success/Failure providing an explanation of why this factory wasn't applicable + Optional> create(PClassInfo sourceType, Type targetType); + + /** + * Returns a new factory that restricts use of this factory to target types for which the given + * predicate holds. + */ + default ConverterFactory when(Predicate predicate) { + return (sourceType, targetType) -> { + if (!predicate.test(targetType)) return Optional.empty(); + return create(sourceType, targetType); + }; + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Named.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Named.java new file mode 100644 index 00000000..2482636d --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Named.java @@ -0,0 +1,32 @@ +/** + * 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.config.java.mapper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Names a constructor parameter for Pkl-to-Java object mapping. Alternatively, the {@code + * javax.inject.Named} annotation can be used, or parameter names can be retained by setting the + * Java 8+ compiler option {@code -parameters}. + */ +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Named { + String value(); +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/NonNull.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/NonNull.java new file mode 100644 index 00000000..a5001f3b --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/NonNull.java @@ -0,0 +1,28 @@ +/** + * 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.config.java.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Indicates that a type does not accept {@code null} as a value. */ +@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.CLASS) +@Documented +public @interface NonNull {} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PAnyToOptional.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PAnyToOptional.java new file mode 100644 index 00000000..08dddec8 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PAnyToOptional.java @@ -0,0 +1,52 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Optional; +import org.pkl.core.PClassInfo; +import org.pkl.core.PNull; + +final class PAnyToOptional implements ConverterFactory { + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + if (!(Reflection.toRawType(targetType) == Optional.class)) { + return Optional.empty(); + } + + // may seem redundant but is used to handle case where targetType is erased + var optionalType = (ParameterizedType) Reflection.getExactSupertype(targetType, Optional.class); + + var elementType = optionalType.getActualTypeArguments()[0]; + return Optional.of(new ConverterImpl(elementType)); + } + + private static class ConverterImpl implements Converter> { + private final Type elementType; + + public ConverterImpl(Type elementType) { + this.elementType = elementType; + } + + @Override + public Optional convert(Object value, ValueMapper valueMapper) { + return value instanceof PNull + ? Optional.empty() + : Optional.of(valueMapper.map(value, elementType)); + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PCollectionToArray.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PCollectionToArray.java new file mode 100644 index 00000000..4f5ef476 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PCollectionToArray.java @@ -0,0 +1,266 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Optional; +import org.pkl.core.PClassInfo; +import org.pkl.core.util.Nullable; + +final class PCollectionToArray implements ConverterFactory { + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + var targetClass = Reflection.toRawType(targetType); + if (!(sourceType.isConcreteCollectionClass() && targetClass.isArray())) { + return Optional.empty(); + } + + if (targetClass.getComponentType().isPrimitive()) { + if (targetClass == boolean[].class) { + return Optional.of(new BooleanArrayConverterImpl()); + } + if (targetClass == char[].class) { + return Optional.of(new CharArrayConverterImpl()); + } + if (targetClass == long[].class) { + return Optional.of(new LongArrayConverterImpl()); + } + if (targetClass == int[].class) { + return Optional.of(new IntArrayConverterImpl()); + } + if (targetClass == short[].class) { + return Optional.of(new ShortArrayConverterImpl()); + } + if (targetClass == byte[].class) { + return Optional.of(new ByteArrayConverterImpl()); + } + if (targetClass == double[].class) { + return Optional.of(new DoubleArrayConverterImpl()); + } + if (targetClass == float[].class) { + return Optional.of(new FloatArrayConverterImpl()); + } + throw new AssertionError("unreachable code"); + } + + var elementType = Reflection.getArrayElementType(targetType); + return Optional.of(new ObjectArrayConverterImpl<>(elementType)); + } + + // having a separate converter for each primitive array type + // saves some reflection at the expense of some code duplication + private static final class BooleanArrayConverterImpl + implements Converter, boolean[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public boolean[] convert(Collection value, ValueMapper valueMapper) { + var result = new boolean[value.size()]; + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, boolean.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class CharArrayConverterImpl + implements Converter, char[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public char[] convert(Collection value, ValueMapper valueMapper) { + var result = new char[value.size()]; + var i = 0; + for (var elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, char.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class ByteArrayConverterImpl + implements Converter, byte[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public byte[] convert(Collection value, ValueMapper valueMapper) { + var result = new byte[value.size()]; + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, byte.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class ShortArrayConverterImpl + implements Converter, short[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public short[] convert(Collection value, ValueMapper valueMapper) { + var result = new short[value.size()]; + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, short.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class IntArrayConverterImpl implements Converter, int[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public int[] convert(Collection value, ValueMapper valueMapper) { + var result = new int[value.size()]; + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, int.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class LongArrayConverterImpl + implements Converter, long[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public long[] convert(Collection value, ValueMapper valueMapper) { + var result = new long[value.size()]; + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, long.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class FloatArrayConverterImpl + implements Converter, float[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public float[] convert(Collection value, ValueMapper valueMapper) { + var result = new float[value.size()]; + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, float.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class DoubleArrayConverterImpl + implements Converter, double[]> { + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + @Override + public double[] convert(Collection value, ValueMapper valueMapper) { + var result = new double[value.size()]; + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, double.class); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } + + private static final class ObjectArrayConverterImpl + implements Converter, T[]> { + private final Type componentType; + private final Class rawComponentType; + + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + private ObjectArrayConverterImpl(Type componentType) { + this.componentType = componentType; + + @SuppressWarnings("unchecked") + var rawComponentType = (Class) Reflection.toRawType(componentType); + this.rawComponentType = rawComponentType; + } + + @Override + public T[] convert(Collection value, ValueMapper valueMapper) { + @SuppressWarnings("unchecked") + var result = (T[]) Array.newInstance(rawComponentType, value.size()); + var i = 0; + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, componentType); + } + assert cachedConverter != null; + result[i++] = cachedConverter.convert(elem, valueMapper); + } + return result; + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PCollectionToCollection.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PCollectionToCollection.java new file mode 100644 index 00000000..7291a798 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PCollectionToCollection.java @@ -0,0 +1,133 @@ +/** + * 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.config.java.mapper; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; +import org.pkl.core.PClassInfo; +import org.pkl.core.util.Nullable; + +class PCollectionToCollection implements ConverterFactory { + private static final Lookup lookup = MethodHandles.lookup(); + + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + var targetClass = Reflection.toRawType(targetType); + if (!(sourceType.isConcreteCollectionClass() + && Collection.class.isAssignableFrom(targetClass))) { + return Optional.empty(); + } + + var iterableType = + (ParameterizedType) Reflection.getExactSupertype(targetType, Collection.class); + var elementType = Reflection.normalize(iterableType.getActualTypeArguments()[0]); + + return createInstantiator(targetClass) + .map(instantiator -> new ConverterImpl<>(instantiator, elementType)); + } + + private Optional>> createInstantiator(Class clazz) { + try { + try { + // constructor with capacity and load factor parameters, e.g. HashSet + var ctor2 = + lookup.findConstructor( + clazz, MethodType.methodType(void.class, int.class, float.class)); + return Optional.of( + length -> { + try { + //noinspection unchecked + return (Collection) ctor2.invoke((int) (length / .75f) + 1, .75f); + } catch (Throwable t) { + throw new ConversionException( + String.format("Error invoking constructor of class `%s`.", clazz), t); + } + }); + } catch (NoSuchMethodException e2) { + try { + // constructor with size parameter, e.g. ArrayList + var ctor1 = lookup.findConstructor(clazz, MethodType.methodType(void.class, int.class)); + return Optional.of( + length -> { + try { + //noinspection unchecked + return (Collection) ctor1.invoke(length); + } catch (Throwable t) { + throw new ConversionException( + String.format("Error invoking constructor of class `%s`.", clazz), t); + } + }); + } catch (NoSuchMethodException e1) { + try { + // default constructor + var ctor0 = lookup.findConstructor(clazz, MethodType.methodType(void.class)); + return Optional.of( + length -> { + try { + //noinspection unchecked + return (Collection) ctor0.invoke(); + } catch (Throwable t) { + throw new ConversionException( + String.format("Error invoking constructor of class `%s`.", clazz), t); + } + }); + } catch (NoSuchMethodException e0) { + return Optional.empty(); + } + } + } + } catch (IllegalAccessException e) { + throw new ConversionException( + String.format("Error accessing constructor of class `%s`.", clazz), e); + } + } + + private static class ConverterImpl implements Converter, Collection> { + private final Function> targetInstantiator; + private final Type targetElementType; + + private PClassInfo cachedElementType = PClassInfo.Unavailable; + private @Nullable Converter cachedConverter; + + private ConverterImpl( + Function> targetInstantiator, Type targetElementType) { + this.targetInstantiator = targetInstantiator; + this.targetElementType = targetElementType; + } + + @Override + public Collection convert(Collection value, ValueMapper valueMapper) { + var result = targetInstantiator.apply(value.size()); + + for (Object elem : value) { + if (!cachedElementType.isExactClassOf(elem)) { + cachedElementType = PClassInfo.forValue(elem); + cachedConverter = valueMapper.getConverter(cachedElementType, targetElementType); + } + assert cachedConverter != null; + result.add(cachedConverter.convert(elem, valueMapper)); + } + + return result; + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PMapToMap.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PMapToMap.java new file mode 100644 index 00000000..34b2bd1a --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PMapToMap.java @@ -0,0 +1,140 @@ +/** + * 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.config.java.mapper; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.function.Function; +import org.pkl.core.PClassInfo; +import org.pkl.core.util.Nullable; + +class PMapToMap implements ConverterFactory { + private static final Lookup lookup = MethodHandles.lookup(); + + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + var targetClass = Reflection.toRawType(targetType); + if (!(sourceType == PClassInfo.Map && Map.class.isAssignableFrom(targetClass))) { + return Optional.empty(); + } + + ParameterizedType mapType; + if (Properties.class.isAssignableFrom(targetClass)) { + // Properties is-a Map but is supposed to only contain String keys/values + mapType = Types.mapOf(String.class, String.class); + } else { + mapType = (ParameterizedType) Reflection.getExactSupertype(targetType, Map.class); + } + var typeArguments = mapType.getActualTypeArguments(); + var keyType = Reflection.normalize(typeArguments[0]); + var valueType = Reflection.normalize(typeArguments[1]); + return createInstantiator(targetClass) + .map(instantiator -> new ConverterImpl<>(instantiator, keyType, valueType)); + } + + private Optional>> createInstantiator(Class clazz) { + try { + // constructor with capacity and load factor arguments + var ctor2 = + lookup.findConstructor(clazz, MethodType.methodType(void.class, int.class, float.class)); + return Optional.of( + length -> { + try { + //noinspection unchecked + return (Map) ctor2.invoke((int) (length / .75f) + 1, .75f); + } catch (Throwable t) { + throw new ConversionException( + String.format("Error invoking constructor of class `%s`.", clazz), t); + } + }); + } catch (NoSuchMethodException e2) { + try { + // default constructor + var ctor0 = lookup.findConstructor(clazz, MethodType.methodType(void.class)); + return Optional.of( + length -> { + try { + //noinspection unchecked + return (Map) ctor0.invoke(); + } catch (Throwable t) { + throw new ConversionException( + String.format("Error invoking constructor of class `%s`.", clazz), t); + } + }); + } catch (NoSuchMethodException e0) { + return Optional.empty(); + } catch (IllegalAccessException e) { + throw new ConversionException( + String.format("Error accessing constructor of class `%s`.", clazz), e); + } + } catch (IllegalAccessException e) { + throw new ConversionException( + String.format("Error accessing constructor of class `%s`.", clazz), e); + } + } + + private static class ConverterImpl implements Converter, Map> { + private final Function> targetInstantiator; + private final Type targetKeyType; + private final Type targetValueType; + + private PClassInfo cachedKeyType = PClassInfo.Unavailable; + private @Nullable Converter cachedKeyConverter; + + private PClassInfo cachedValueType = PClassInfo.Unavailable; + private @Nullable Converter cachedValueConverter; + + private ConverterImpl( + Function> targetInstantiator, Type targetKeyType, Type targetValueType) { + this.targetInstantiator = targetInstantiator; + this.targetKeyType = targetKeyType; + this.targetValueType = targetValueType; + } + + @Override + public Map convert(Map map, ValueMapper valueMapper) { + var result = targetInstantiator.apply(map.size()); + + for (Map.Entry entry : map.entrySet()) { + var key = entry.getKey(); + if (!cachedKeyType.isExactClassOf(key)) { + cachedKeyType = PClassInfo.forValue(key); + cachedKeyConverter = valueMapper.getConverter(cachedKeyType, targetKeyType); + } + assert cachedKeyConverter != null; + + var value = entry.getValue(); + if (!cachedValueType.isExactClassOf(value)) { + cachedValueType = PClassInfo.forValue(value); + cachedValueConverter = valueMapper.getConverter(cachedValueType, targetValueType); + } + assert cachedValueConverter != null; + + result.put( + cachedKeyConverter.convert(key, valueMapper), + cachedValueConverter.convert(value, valueMapper)); + } + + return result; + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PNullToAny.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PNullToAny.java new file mode 100644 index 00000000..fd8fb234 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PNullToAny.java @@ -0,0 +1,33 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Type; +import java.util.Optional; +import org.pkl.core.PClassInfo; + +final class PNullToAny implements ConverterFactory { + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + if (sourceType != PClassInfo.Null + || (targetType instanceof Class && ((Class) targetType).isPrimitive())) { + return Optional.empty(); + } + + //noinspection ConstantConditions + return Optional.of((value, valueMapper) -> null); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToDataObject.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToDataObject.java new file mode 100644 index 00000000..6ed722b5 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToDataObject.java @@ -0,0 +1,232 @@ +/** + * 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.config.java.mapper; + +import java.beans.ConstructorProperties; +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.*; +import java.util.*; +import org.pkl.core.Composite; +import org.pkl.core.PClassInfo; +import org.pkl.core.PObject; +import org.pkl.core.util.Nullable; + +public class PObjectToDataObject implements ConverterFactory { + private static final Lookup lookup = MethodHandles.lookup(); + + @SuppressWarnings("unchecked") + private static final @Nullable Class javaxInjectNamedClass = + (Class) Reflection.tryLoadClass("javax.inject.Named"); + + private static final @Nullable Method javaxInjectNamedValueMethod; + + static { + try { + javaxInjectNamedValueMethod = + javaxInjectNamedClass == null ? null : javaxInjectNamedClass.getMethod("value"); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + protected PObjectToDataObject() {} + + @Override + public final Optional> create(PClassInfo sourceType, Type targetType) { + if (!(sourceType == PClassInfo.Module || sourceType.getJavaClass() == PObject.class)) { + return Optional.empty(); + } + + return selectConstructor(Reflection.toRawType(targetType)) + .flatMap( + constructor -> + getParameters(constructor, targetType) + .map( + parameters -> { + try { + return new ConverterImpl<>( + targetType, lookup.unreflectConstructor(constructor), parameters); + } catch (IllegalAccessException e) { + throw new ConversionException( + String.format("Error accessing constructor `%s`.", constructor), e); + } + })); + } + + protected Optional> selectConstructor(Class clazz) { + return Arrays.stream(clazz.getDeclaredConstructors()) + .max(Comparator.comparingInt(Constructor::getParameterCount)); + } + + protected Optional> getParameterNames(Constructor constructor) { + var paramNames = new ArrayList(constructor.getParameterCount()); + + var properties = getAnnotation(constructor, ConstructorProperties.class); + if (properties != null) { + return Optional.of(Arrays.asList(properties.value())); + } + + for (Parameter parameter : constructor.getParameters()) { + var name = getParameterName(parameter); + if (name == null) return Optional.empty(); + paramNames.add(name); + } + return Optional.of(paramNames); + } + + private Optional>> getParameters( + Constructor constructor, Type targetType) { + return getParameterNames(constructor) + .map( + paramNames -> { + var paramTypes = Reflection.getExactParameterTypes(constructor, targetType); + var parameters = new ArrayList>(paramNames.size()); + for (int i = 0; i < paramNames.size(); i++) { + var name = paramNames.get(i); + parameters.add(Tuple2.of(name, paramTypes[i])); + } + return parameters; + }); + } + + private static @Nullable String getParameterName(Parameter parameter) { + if (parameter.isNamePresent()) { + return parameter.getName(); + } + + Named named = getAnnotation(parameter, Named.class); + if (named != null) { + return named.value(); + } + + if (javaxInjectNamedClass != null) { + assert javaxInjectNamedValueMethod != null; + var ann = getAnnotation(parameter, javaxInjectNamedClass); + if (ann != null) { + try { + return (String) javaxInjectNamedValueMethod.invoke(ann); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new ConversionException("Failed to invoke `javax.inject.Named.value()`.", e); + } + } + } + + return null; + } + + private static @Nullable T getAnnotation( + Constructor constructor, Class annotationClass) { + try { + return constructor.getAnnotation(annotationClass); + } catch (IndexOutOfBoundsException e) { + // workaround for https://bugs.openjdk.java.net/browse/JDK-8025806 + return null; + } + } + + private static @Nullable T getAnnotation( + Parameter parameter, Class annotationClass) { + try { + return parameter.getAnnotation(annotationClass); + } catch ( + IndexOutOfBoundsException + e) { // workaround for https://bugs.openjdk.java.net/browse/JDK-8025806 + return null; + } + } + + private static class ConverterImpl implements Converter { + private final Type targetType; + private final MethodHandle constructorHandle; + private final Collection> parameters; + private final PClassInfo[] cachedPropertyTypes; + private final Converter[] cachedConverters; + + ConverterImpl( + Type targetType, + MethodHandle constructorHandle, + Collection> parameters) { + this.targetType = targetType; + this.constructorHandle = constructorHandle; + this.parameters = parameters; + + @SuppressWarnings("unchecked") + PClassInfo[] cachedPropertyTypes = new PClassInfo[parameters.size()]; + this.cachedPropertyTypes = cachedPropertyTypes; + Arrays.fill(cachedPropertyTypes, PClassInfo.Unavailable); + + @SuppressWarnings("unchecked") + Converter[] cachedConverters = new Converter[parameters.size()]; + this.cachedConverters = cachedConverters; + } + + @Override + public T convert(Composite value, ValueMapper valueMapper) { + var properties = value.getProperties(); + var args = new Object[parameters.size()]; + var i = 0; + + for (var param : parameters) { + var property = properties.get(param.first); + if (property == null) { + var message = + String.format( + "Cannot convert Pkl object to Java object." + + "%nPkl type : %s" + + "%nJava type : %s" + + "%nMissing Pkl property : %s" + + "%nActual Pkl properties: %s", + value.getClassInfo(), targetType.getTypeName(), param.first, properties.keySet()); + throw new ConversionException(message); + } + + try { + var cachedPropertyType = cachedPropertyTypes[i]; + if (!cachedPropertyType.isExactClassOf(property)) { + cachedPropertyType = PClassInfo.forValue(property); + cachedPropertyTypes[i] = cachedPropertyType; + cachedConverters[i] = valueMapper.getConverter(cachedPropertyType, param.second); + } + assert cachedConverters[i] != null; + args[i] = cachedConverters[i].convert(property, valueMapper); + i += 1; + } catch (ConversionException e) { + throw new ConversionException( + String.format( + "Error converting property `%s` in Pkl object of type `%s` " + + "to equally named constructor parameter in Java class `%s`: " + + e.getMessage(), + param.first, + value.getClassInfo(), + Reflection.toRawType(targetType).getTypeName()), + e.getCause()); + } + } + + try { + @SuppressWarnings("unchecked") + var result = (T) constructorHandle.invokeWithArguments(args); + return result; + } catch (Throwable t) { + throw new ConversionException( + String.format("Error invoking constructor `%s`.", constructorHandle), t); + } + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToMap.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToMap.java new file mode 100644 index 00000000..f5fe1694 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToMap.java @@ -0,0 +1,44 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.*; +import java.util.*; +import org.pkl.core.PClassInfo; +import org.pkl.core.PObject; + +public final class PObjectToMap implements ConverterFactory { + private final ConverterFactory pMapToMap = new PMapToMap(); + + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + var targetClass = Reflection.toRawType(targetType); + + if (!((sourceType == PClassInfo.Module || sourceType.getJavaClass() == PObject.class) + && Map.class.isAssignableFrom(targetClass))) { + return Optional.empty(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + Optional, Map>> underlying = + (Optional) pMapToMap.create(PClassInfo.Map, targetType); + + return underlying.map( + converter -> + (Converter>) + (value, valueMapper) -> converter.convert(value.getProperties(), valueMapper)); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToPObject.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToPObject.java new file mode 100644 index 00000000..828ae59f --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PObjectToPObject.java @@ -0,0 +1,32 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Type; +import java.util.Optional; +import org.pkl.core.PClassInfo; +import org.pkl.core.PObject; + +final class PObjectToPObject implements ConverterFactory { + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + if (!(sourceType.getJavaClass() == PObject.class + && (targetType == Object.class || targetType == PObject.class))) { + return Optional.empty(); + } + return Optional.of(Converter.identity()); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PPairToPair.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PPairToPair.java new file mode 100644 index 00000000..106b102a --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PPairToPair.java @@ -0,0 +1,77 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Optional; +import org.pkl.core.PClassInfo; +import org.pkl.core.Pair; +import org.pkl.core.util.Nullable; + +final class PPairToPair implements ConverterFactory { + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + if (sourceType != PClassInfo.Pair) return Optional.empty(); + + var targetClass = Reflection.toRawType(targetType); + if (!Pair.class.isAssignableFrom(targetClass)) { + return Optional.empty(); + } + + var pairType = (ParameterizedType) Reflection.getExactSupertype(targetType, Pair.class); + return Optional.of( + new ConverterImpl<>( + pairType.getActualTypeArguments()[0], pairType.getActualTypeArguments()[1])); + } + + private static class ConverterImpl implements Converter, Pair> { + private final Type firstTargetType; + private final Type secondTargetType; + + private PClassInfo firstCachedType = PClassInfo.Unavailable; + private @Nullable Converter firstCachedConverter; + + private PClassInfo secondCachedType = PClassInfo.Unavailable; + private @Nullable Converter secondCachedConverter; + + public ConverterImpl(Type firstTargetType, Type secondTargetType) { + this.firstTargetType = firstTargetType; + this.secondTargetType = secondTargetType; + } + + @Override + public Pair convert(Pair value, ValueMapper valueMapper) { + var first = value.getFirst(); + if (!firstCachedType.isExactClassOf(first)) { + firstCachedType = PClassInfo.forValue(first); + firstCachedConverter = valueMapper.getConverter(firstCachedType, firstTargetType); + } + + var second = value.getSecond(); + if (!secondCachedType.isExactClassOf(second)) { + secondCachedType = PClassInfo.forValue(second); + secondCachedConverter = valueMapper.getConverter(secondCachedType, secondTargetType); + } + + assert firstCachedConverter != null; + assert secondCachedConverter != null; + return new Pair<>( + firstCachedConverter.convert(first, valueMapper), + secondCachedConverter.convert(second, valueMapper)); + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PStringToEnum.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PStringToEnum.java new file mode 100644 index 00000000..267d60c0 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/PStringToEnum.java @@ -0,0 +1,81 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Optional; +import org.pkl.core.DataSizeUnit; +import org.pkl.core.DurationUnit; +import org.pkl.core.PClassInfo; +import org.pkl.core.util.CodeGeneratorUtils; +import org.pkl.core.util.CollectionUtils; + +final class PStringToEnum implements ConverterFactory { + @Override + public Optional> create(PClassInfo sourceType, Type targetType) { + var rawTargetType = Reflection.toRawType(targetType); + if (sourceType != PClassInfo.String || !rawTargetType.isEnum()) return Optional.empty(); + + return Optional.of(new ConverterImpl(rawTargetType)); + } + + private static final class ConverterImpl implements Converter> { + private final Class enumType; + private final Map> enumValuesByName; + + private ConverterImpl(Class enumType) { + this.enumType = enumType; + var values = (Enum[]) enumType.getEnumConstants(); + enumValuesByName = CollectionUtils.newConcurrentHashMap(values.length); + // special-case: enums in the standard library have a different name compared to the Pkl + // string + if (enumType == DataSizeUnit.class) { + for (var value : values) { + var unit = (DataSizeUnit) value; + enumValuesByName.put(CodeGeneratorUtils.toEnumConstantName(unit.getSymbol()), value); + } + } else if (enumType == DurationUnit.class) { + for (var value : values) { + var unit = (DurationUnit) value; + enumValuesByName.put(CodeGeneratorUtils.toEnumConstantName(unit.getSymbol()), value); + } + } else { + for (Enum value : values) { + enumValuesByName.put(value.name(), value); + } + } + } + + @Override + public Enum convert(String value, ValueMapper valueMapper) { + var enumValue = enumValuesByName.get(value); + if (enumValue == null) { + enumValue = enumValuesByName.get(CodeGeneratorUtils.toEnumConstantName(value)); + if (enumValue != null) { + enumValuesByName.put(value, enumValue); + } + } + if (enumValue != null) { + return enumValue; + } + throw new ConversionException( + String.format( + "Cannot convert String `%s` to Enum value of type `%s`.", + value, enumType.getTypeName())); + } + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Reflection.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Reflection.java new file mode 100644 index 00000000..e07c3f05 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Reflection.java @@ -0,0 +1,162 @@ +/** + * 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.config.java.mapper; + +import static java.util.Arrays.stream; + +import io.leangen.geantyref.CaptureType; +import io.leangen.geantyref.GenericTypeReflector; +import java.lang.reflect.*; +import org.pkl.core.util.Nullable; + +/** + * Reflection utilities for implementing {@link ConverterFactory}s. Mostly covers introspection of + * parameterized types, which is not covered by the {@code java.util.reflect} API. + * + *

The heavy lifting under the covers is done by the excellent ge(a)ntyref library. + */ +public final class Reflection { + private Reflection() {} + + /** + * Returns the class with the given fully qualified name, or {@code null} if a class with the + * given name cannot be found. + */ + public static @Nullable Class tryLoadClass(String qualifiedName) { + try { + // use Class.forName() rather than ClassLoader.loadClass() + // because Class.getClassLoader() is not supported by AOT + return Class.forName(qualifiedName); + } catch (ClassNotFoundException e) { + return null; + } + } + + public static boolean isMissingTypeArguments(Type type) { + if (type instanceof WildcardType) { + var wildcardType = (WildcardType) type; + var baseType = + wildcardType.getLowerBounds().length > 0 + ? wildcardType.getLowerBounds()[0] + : wildcardType.getUpperBounds()[0]; + return isMissingTypeArguments(baseType); + } + return GenericTypeReflector.isMissingTypeParameters(type); + } + + /** + * Returns the normalized form of the given type. A normalized type is concrete (no wildcards) and + * instantiable (not an interface or abstract class). + */ + public static Type normalize(Type type) { + if (type instanceof WildcardType) { + var wcType = (WildcardType) type; + var bounds = wcType.getLowerBounds(); + if (bounds.length > 0) return bounds[0]; + bounds = wcType.getUpperBounds(); + if (bounds.length > 0) return bounds[0]; + } + return getExactSupertype(type, toRawType(type)); + } + + /** + * Returns the raw (erased) type for the given parameterized type, type bound for the given + * wildcard type, or the given type otherwise. + */ + public static Class toRawType(Type type) { + return GenericTypeReflector.erase(type); + } + + /** + * Returns the wrapper type for the given primitive type. If the given type is not a primitive + * type, returns the given type. + */ + @SuppressWarnings("unchecked") + // casts are safe as (say) boolean.class and Boolean.class are both of type Class + public static Class toWrapperType(Class type) { + if (type == boolean.class) return (Class) Boolean.class; + if (type == char.class) return (Class) Character.class; + if (type == long.class) return (Class) Long.class; + if (type == int.class) return (Class) Integer.class; + if (type == short.class) return (Class) Short.class; + if (type == byte.class) return (Class) Byte.class; + if (type == double.class) return (Class) Double.class; + if (type == float.class) return (Class) Float.class; + + return type; + } + + /** Returns the (possibly parameterized) element type for the given array type. */ + public static Type getArrayElementType(Type type) { + return GenericTypeReflector.getArrayComponentType(type); + } + + /** + * Returns a parameterization of the given raw supertype, taking into account type arguments of + * the given subtype. For example, @{code getExactSupertype(listOf(String.class), + * Collection.class)} will return @{code collectionOf(String.class)}. If the given subtype is not + * a parameterized type, returns the given raw supertype. If the given types have no inheritance + * relationship, returns {@code null}. + */ + // call sites typically know that the given types have an inheritance relationship + // annotating the return type with @Nullable would lead to annoying IDE + // nullability warnings in these cases + public static Type getExactSupertype(Type type, Class rawSupertype) { + return uncapture(GenericTypeReflector.getExactSuperType(type, rawSupertype)); + } + + /** + * Returns a parameterization of the given raw subtype, taking into account type arguments of the + * given supertype. For example, @{code getExactSubtype(collectionOf(String.class), List.class)} + * will return @{code listOf(String.class)}. If the given supertype is not a parameterized type, + * returns the given raw subtype. If the given types have no inheritance relationship, returns + * {@code null}. + */ + // call sites typically know that the given types have an inheritance relationship + // annotating the return type with @Nullable would lead to annoying IDE + // nullability warnings in these cases + public static Type getExactSubtype(Type type, Class rawSubtype) { + return uncapture(GenericTypeReflector.getExactSubType(type, rawSubtype)); + } + + /** + * Returns the exact parameter types of the given method or constructor, taking into account type + * arguments of the given declaring type. For example, {@code + * getExactParameterTypes(List.class.getDeclaredMethod("get"), listOf(optionalOf(String.class)} + * will return {@code optionalOf(String.class)}. Throws {@link IllegalArgumentException} if the + * given method or constructor is not declared by the given type. + */ + public static Type[] getExactParameterTypes(Executable m, Type declaringType) { + return stream( + GenericTypeReflector.getExactParameterTypes( + m, GenericTypeReflector.annotate(declaringType))) + .map(annType -> uncapture(annType.getType())) + .toArray(Type[]::new); + } + + /** + * Undoes the capture of a wildcard type, or returns the given type otherwise. Unlike wildcard + * types, capture types are not represented in the Java reflection API, but may be returned by + * geantyref's getExactXXX methods. This leads to problems, which is why our getExactXXX methods + * eliminate them. + */ + private static Type uncapture(Type type) { + if (type instanceof CaptureType) { + return ((CaptureType) type).getWildcardType(); + } + return type; + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Tuple2.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Tuple2.java new file mode 100644 index 00000000..9610a842 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Tuple2.java @@ -0,0 +1,54 @@ +/** + * 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.config.java.mapper; + +import java.util.Objects; +import org.pkl.core.util.Nullable; + +// avoid name clash with org.pkl.core.Pair +final class Tuple2 { + final S first; + final T second; + + private Tuple2(S first, T second) { + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); + } + + static Tuple2 of(S first, T second) { + return new Tuple2<>(first, second); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Tuple2)) return false; + + var other = (Tuple2) obj; + + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return 31 * first.hashCode() + second.hashCode(); + } + + @Override + public String toString() { + return "Tuple2(" + first + ", " + second + ")"; + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/TypeMapping.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/TypeMapping.java new file mode 100644 index 00000000..4cfff622 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/TypeMapping.java @@ -0,0 +1,78 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Modifier; +import org.pkl.core.util.Nullable; + +/** + * Maps a type requested during conversion to the implementation type to be instantiated. The + * requested type is often an interface type. The implementation type is always a class type. A + * typical example is mapping {@link java.util.List} to {@link java.util.ArrayList}. + */ +// kept simple for now +// if we wanted to have more sophisticated mappings, we could: +// * support mapping factories/strategies (cf. ConverterFactory/Converter) +// * support parameterized mappings (e.g. Set -> EnumSet) +public final class TypeMapping { + public final Class requestedType; + public final Class implementationType; + + private TypeMapping(Class requestedType, Class implementationType) { + if (Modifier.isAbstract(implementationType.getModifiers())) { + throw new IllegalArgumentException( + String.format( + "`implementationType` must not be abstract, but `%s` is.", + implementationType.getTypeName())); + } + if (!requestedType.isAssignableFrom(implementationType)) { + throw new IllegalArgumentException( + String.format( + "`implementationType` must be assignable to `requestedType`, but `%s` is not assignable to `%s`.", + implementationType.getTypeName(), requestedType.getTypeName())); + } + if (requestedType.isArray() || implementationType.isArray()) { + throw new IllegalArgumentException("Type mappings are not supported for array types."); + } + this.requestedType = requestedType; + this.implementationType = implementationType; + } + + public static TypeMapping of( + Class requestedType, Class implementationType) { + return new TypeMapping<>(requestedType, implementationType); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof TypeMapping)) return false; + + var other = (TypeMapping) obj; + return requestedType == other.requestedType && implementationType == other.implementationType; + } + + @Override + public int hashCode() { + return requestedType.hashCode() * 31 + implementationType.hashCode(); + } + + @Override + public String toString() { + return String.format( + "TypeMapping(%s, %s)", requestedType.getTypeName(), implementationType.getTypeName()); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/TypeMappings.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/TypeMappings.java new file mode 100644 index 00000000..58f6f3a4 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/TypeMappings.java @@ -0,0 +1,39 @@ +/** + * 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.config.java.mapper; + +import java.util.*; + +/** Predefined type mappings that can be registered with {@link ValueMapperBuilder}. */ +public final class TypeMappings { + private TypeMappings() {} + + /** Type mappings for collection types. */ + public static final Collection> collections = + List.of( + TypeMapping.of(Iterable.class, ArrayList.class), + TypeMapping.of(Collection.class, ArrayList.class), + TypeMapping.of(List.class, ArrayList.class), + TypeMapping.of(Set.class, HashSet.class), + TypeMapping.of(Map.class, HashMap.class), + TypeMapping.of(SortedSet.class, TreeSet.class), + TypeMapping.of(SortedMap.class, TreeMap.class), + TypeMapping.of(Queue.class, ArrayDeque.class), + TypeMapping.of(Deque.class, ArrayDeque.class)); + + /** All type mappings defined in this class. */ + public static final Collection> all = collections; +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Types.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Types.java new file mode 100644 index 00000000..c51f0324 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/Types.java @@ -0,0 +1,100 @@ +/** + * 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.config.java.mapper; + +import io.leangen.geantyref.TypeArgumentNotInBoundException; +import io.leangen.geantyref.TypeFactory; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import org.pkl.core.Pair; + +/** + * A factory for parameterized type literals such as {@code List} or {@code MyClass}. Used to express the desired target type in {@link ValueMapper#map(Object, Type)}. + */ +public final class Types { + private Types() {} + + public static ParameterizedType parameterizedType(Class rawType, Type... typeArguments) { + var typeParamsCount = rawType.getTypeParameters().length; + if (typeParamsCount == 0) { + throw new IllegalArgumentException( + String.format( + "Cannot parameterize `%s` because it does not have any type parameters.", + rawType.getTypeName())); + } + if (typeArguments.length != typeParamsCount) { + throw new IllegalArgumentException( + String.format( + "Expected %d type arguments for `%s`, but got %d.", + typeParamsCount, rawType.getTypeName(), typeArguments.length)); + } + for (Type arg : typeArguments) { + if (arg instanceof Class) { + var clazz = (Class) arg; + if (clazz.isPrimitive()) { + throw new IllegalArgumentException( + String.format( + "`%s.class` is not a valid type argument. Did you mean `%s.class`?", + clazz, Reflection.toWrapperType(clazz).getSimpleName())); + } + } + } + try { + return (ParameterizedType) TypeFactory.parameterizedClass(rawType, typeArguments); + } catch (TypeArgumentNotInBoundException e) { + throw new IllegalArgumentException( + String.format( + "Type argument `%s` for type parameter `%s` is not within bound `%s`.", + e.getArgument().getTypeName(), + e.getParameter().getTypeName(), + e.getBound().getTypeName())); + } + } + + public static ParameterizedType optionalOf(Type elementType) { + return parameterizedType(Optional.class, elementType); + } + + public static Type arrayOf(Type elementType) { + return TypeFactory.arrayOf(elementType); + } + + public static ParameterizedType pairOf(Type firstType, Type secondType) { + return parameterizedType(Pair.class, firstType, secondType); + } + + public static ParameterizedType iterableOf(Type elementType) { + return parameterizedType(Iterable.class, elementType); + } + + public static ParameterizedType collectionOf(Type elementType) { + return parameterizedType(Collection.class, elementType); + } + + public static ParameterizedType listOf(Type elementType) { + return parameterizedType(List.class, elementType); + } + + public static ParameterizedType setOf(Type elementType) { + return parameterizedType(Set.class, elementType); + } + + public static ParameterizedType mapOf(Type keyType, Type valueType) { + return parameterizedType(Map.class, keyType, valueType); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapper.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapper.java new file mode 100644 index 00000000..d26a5777 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapper.java @@ -0,0 +1,74 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Type; +import org.pkl.core.PClassInfo; +import org.pkl.core.PModule; + +/** + * Automatically converts Pkl objects to Java objects. Use {@link ValueMapperBuilder} to create an + * instance of this type, configured according to your needs. + */ +public interface ValueMapper { + /** Shorthand for {@code ValueMapperBuilder.preconfigured().build()}. */ + static ValueMapper preconfigured() { + return ValueMapperBuilder.preconfigured().build(); + } + + /** + * Converts the given Pkl object to the given Java target type. The Pkl object can be an entire + * {@link PModule} or any value contained therein. See {@link PClassInfo#forValue} for which Java + * types are used to represent Pkl objects. + * + *

When mapping to a generic target type, a fully parameterized type needs to be passed, e.g. + * {@code List}. Parameterized type literals can be created using {@link Types}, e.g. + * {@code Types.listOf(String.class)}. + * + *

If an error occurs during conversion, or if {@link ValueMapper} does not know how to convert + * from the given object to the given target type, a {@link ConversionException} is thrown. + */ + T map(S model, Type targetType); + + /** + * Same as {@link #map(Object, Type)}, except that the target type is narrowed from {@link Type} + * to {@link Class} to allow for better type inference. + */ + default T map(S model, Class targetType) { + return map(model, (Type) targetType); + } + + /** + * Returns the converter with the given source and target types. Throws {@link + * ConversionException} if no such converter exists. + */ + Converter getConverter(PClassInfo sourceType, Type targetType); + + /** + * Same as {@link #getConverter(PClassInfo, Type)}, except that the target type is narrowed from + * {@link Type} to {@link Class} to allow for better type inference. + */ + default Converter getConverter(PClassInfo sourceType, Class targetType) { + return getConverter(sourceType, (Type) targetType); + } + + /** + * Returns a value mapper builder that, unless further configured, will build value mappers with + * the same configuration as this value mapper. In other words, this is the inverse operation of + * {@link ValueMapperBuilder#build()}, except that a new builder is returned. + */ + ValueMapperBuilder toBuilder(); +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapperBuilder.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapperBuilder.java new file mode 100644 index 00000000..d2b37ea1 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapperBuilder.java @@ -0,0 +1,182 @@ +/** + * 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.config.java.mapper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Builds a {@link ValueMapper} configured with appropriate conversions, converter factories, and + * type mappings. + */ +@SuppressWarnings("UnusedReturnValue") +public final class ValueMapperBuilder { + private final List> conversions = new ArrayList<>(); + private final List factories = new ArrayList<>(); + private final List> mappings = new ArrayList<>(); + + private ValueMapperBuilder() {} + + /** + * Creates a builder without any preconfigured conversions, converter factories, or type mappings. + * + * @return a builder without any preconfigured conversions, converter factories, or type mappings + */ + public static ValueMapperBuilder unconfigured() { + return new ValueMapperBuilder(); + } + + /** + * Creates a builder preconfigured with all conversions, converter factories, and type mappings + * defined in this module. + * + * @return a builder preconfigured with all conversions, converter factories, and type mappings + * defined in this module + */ + public static ValueMapperBuilder preconfigured() { + return unconfigured() + .addConversions(Conversions.all) + .addConverterFactories(ConverterFactories.all) + .addTypeMappings(TypeMappings.all); + } + + /** + * Adds the given conversion. For conversions to a primitive type, a conversion to the + * corresponding wrapper type is automatically added. + * + * @param conversion the conversion to be added + * @return this + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public ValueMapperBuilder addConversion(Conversion conversion) { + conversions.add(conversion); + if (conversion.targetType instanceof Class) { + var clazz = (Class) conversion.targetType; + if (clazz.isPrimitive()) { + conversions.add( + Conversion.of( + conversion.sourceType, + Reflection.toWrapperType(clazz), + (Converter) conversion.converter)); + } + } + return this; + } + + /** + * Adds the given conversions. For conversions to a primitive type, a conversion to the + * corresponding wrapper type is automatically added. + * + * @param conversions the conversions to be added + * @return this + */ + public ValueMapperBuilder addConversions(Collection> conversions) { + conversions.forEach(this::addConversion); + return this; + } + + /** Removes any existing conversions, then adds the given conversions. */ + public ValueMapperBuilder setConversions(Collection> conversions) { + this.conversions.clear(); + return addConversions(conversions); + } + + /** Returns the currently set conversions. */ + public List> getConversions() { + return conversions; + } + + /** + * Adds the given converter factory. Factories will be queried for converters in the same order as + * they are being added to this builder. + * + * @param factory the converter factory to be added + * @return this + */ + public ValueMapperBuilder addConverterFactory(ConverterFactory factory) { + factories.add(factory); + return this; + } + + /** + * Adds the given converter factories. Factories will be queried for converters in the same order + * as they are being added to this builder. + * + * @param factories the converter factories to be added + * @return this + */ + public ValueMapperBuilder addConverterFactories(Collection factories) { + factories.forEach(this::addConverterFactory); + return this; + } + + /** Removes any existing converter factories, then adds the given factories. */ + public ValueMapperBuilder setConverterFactories(Collection factories) { + this.factories.clear(); + return addConverterFactories(factories); + } + + /** Returns the currently set converter factories. */ + public List getConverterFactories() { + return factories; + } + + /** + * Adds the given type mapping. + * + * @param mapping the type mapping to be added + * @return this + */ + public ValueMapperBuilder addTypeMapping(TypeMapping mapping) { + mappings.add(mapping); + return this; + } + + /** + * Adds the given type mappings. + * + * @param mappings the type mappings to be added + * @return this + */ + public ValueMapperBuilder addTypeMappings(Collection> mappings) { + mappings.forEach(this::addTypeMapping); + return this; + } + + /** Removes any existing type mappings, then adds the given mappings. */ + public ValueMapperBuilder setTypeMappings(Collection> mappings) { + this.mappings.clear(); + return addTypeMappings(mappings); + } + + /** Returns the currently set type mappings. */ + public List> getTypeMappings() { + return mappings; + } + + /** + * Builds a mapper with the configured conversions, converter factories, and type mappings. If + * desired, the same builder can be used to build multiple mappers. + * + * @return a mapper with the configured conversions, converter factories, and type mappings + */ + public ValueMapper build() { + return new ValueMapperImpl( + // copy to shield against subsequent modification through builder + new ArrayList<>(conversions), new ArrayList<>(factories), new ArrayList<>(mappings)); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapperImpl.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapperImpl.java new file mode 100644 index 00000000..d2ba1dcf --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/ValueMapperImpl.java @@ -0,0 +1,127 @@ +/** + * 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.config.java.mapper; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import org.pkl.core.PClassInfo; +import org.pkl.core.util.CollectionUtils; + +class ValueMapperImpl implements ValueMapper { + private final Collection> conversions; + private final Collection factories; + private final Collection> typeMappings; + + private final Map, Type>, Converter> convertersMap; + private final Map, Class> typeMappingsMap; + + ValueMapperImpl( + Collection> conversions, + Collection factories, + Collection> typeMappings) { + this.conversions = conversions; + this.factories = factories; + this.typeMappings = typeMappings; + + convertersMap = CollectionUtils.newHashMap(conversions.size()); + for (var conversion : conversions) { + convertersMap.put( + Tuple2.of(conversion.sourceType, conversion.targetType), conversion.converter); + } + + this.typeMappingsMap = CollectionUtils.newHashMap(typeMappings.size()); + for (var mapping : typeMappings) { + this.typeMappingsMap.put(mapping.requestedType, mapping.implementationType); + } + } + + @Override + @SuppressWarnings("unchecked") + public T map(S model, Type targetType) { + var sourceType = PClassInfo.forValue(model); + return (T) getConverter(sourceType, targetType).convert(model, this); + } + + private Class getTargetType(PClassInfo sourceType, Type targetType) { + var rawTargetType = Reflection.toRawType(targetType); + var determinedClass = ClassRegistry.get(sourceType); + if (determinedClass == null) { + return rawTargetType; + } + var rawRegisteredSchema = Reflection.toRawType(determinedClass); + if (rawTargetType.isAssignableFrom(rawRegisteredSchema)) { + return rawRegisteredSchema; + } + return rawTargetType; + } + + @Override + public Converter getConverter(PClassInfo sourceType, Type targetType) { + Tuple2, Type> key = Tuple2.of(sourceType, targetType); + + @SuppressWarnings("unchecked") + Converter converter = (Converter) convertersMap.get(key); + if (converter != null) return converter; + + if (Reflection.isMissingTypeArguments(targetType)) { + throw new IllegalArgumentException( + String.format("Target type `%s` is missing type arguments.", targetType)); + } + + Class rawTargetType = getTargetType(sourceType, targetType); + @SuppressWarnings("unchecked") + var rawImplType = (Class) typeMappingsMap.getOrDefault(rawTargetType, rawTargetType); + var implType = Reflection.getExactSubtype(targetType, rawImplType); + + // look up implType converter + // because implType has wildcards removed, conversion to + // `? extends Number` or `? super Number` finds converter for `Number` + // (assuming `converters` has such a converter) + var implKey = Tuple2.of(sourceType, implType); + @SuppressWarnings("unchecked") + var implConverter = (Converter) convertersMap.get(implKey); + if (implConverter != null) { + // TODO: give converter a chance to copy itself to avoid pollution of its inline caches + convertersMap.put(key, implConverter); + return implConverter; + } + + // create implType converter + for (ConverterFactory factory : factories) { + @SuppressWarnings({"unchecked", "rawtypes"}) + Optional> newConverter = (Optional) factory.create(sourceType, implType); + if (newConverter.isPresent()) { + convertersMap.put(key, newConverter.get()); + return newConverter.get(); + } + } + + throw new ConversionException( + String.format( + "Cannot convert `%s` to `%s` because no conversion was found.", + sourceType.getQualifiedName(), targetType.getTypeName())); + } + + @Override + public ValueMapperBuilder toBuilder() { + return ValueMapperBuilder.unconfigured() + .setConversions(conversions) + .setConverterFactories(factories) + .setTypeMappings(typeMappings); + } +} diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/mapper/package-info.java b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/package-info.java new file mode 100644 index 00000000..150ba450 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/mapper/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.config.java.mapper; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-config-java/src/main/java/org/pkl/config/java/package-info.java b/pkl-config-java/src/main/java/org/pkl/config/java/package-info.java new file mode 100644 index 00000000..e08115b8 --- /dev/null +++ b/pkl-config-java/src/main/java/org/pkl/config/java/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.config.java; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorBuilderTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorBuilderTest.java new file mode 100644 index 00000000..1749f31a --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigEvaluatorBuilderTest.java @@ -0,0 +1,144 @@ +/** + * 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.config.java; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.*; +import org.junit.jupiter.api.Test; +import org.pkl.config.java.mapper.ConverterFactories; +import org.pkl.core.SecurityManagers; + +public class ConfigEvaluatorBuilderTest { + @Test + public void preconfiguredBuilderHasPreconfiguredUnderlyingBuilders() { + var builder = ConfigEvaluatorBuilder.preconfigured(); + + var evaluatorBuilder = builder.getEvaluatorBuilder(); + assertThat(evaluatorBuilder).isNotNull(); + assertThat(evaluatorBuilder.getEnvironmentVariables()).isEqualTo(System.getenv()); + assertThat(evaluatorBuilder.getExternalProperties()).isEqualTo(System.getProperties()); + + var mapperBuilder = builder.getValueMapperBuilder(); + assertThat(mapperBuilder).isNotNull(); + assertThat(mapperBuilder.getConverterFactories()).isEqualTo(ConverterFactories.all); + } + + @Test + public void unconfiguredBuilderHasUnconfiguredUnderlyingBuilders() { + var builder = ConfigEvaluatorBuilder.unconfigured(); + + var evaluatorBuilder = builder.getEvaluatorBuilder(); + assertThat(evaluatorBuilder).isNotNull(); + assertThat(evaluatorBuilder.getEnvironmentVariables()).isEmpty(); + assertThat(evaluatorBuilder.getExternalProperties()).isEmpty(); + + var mapperBuilder = builder.getValueMapperBuilder(); + assertThat(mapperBuilder).isNotNull(); + assertThat(mapperBuilder.getConverterFactories()).isEmpty(); + } + + @Test + public void preconfiguredBuilderContainsProcessEnvironmentVariables() { + var builder = ConfigEvaluatorBuilder.preconfigured(); + assertThat(builder.getEnvironmentVariables()).isEqualTo(System.getenv()); + } + + @Test + public void unconfiguredBuilderContainsNoEnvironmentVariables() { + var builder = ConfigEvaluatorBuilder.unconfigured(); + assertThat(builder.getEnvironmentVariables()).isEmpty(); + } + + @Test + public void addEnvironmentVariables() { + var builder = ConfigEvaluatorBuilder.unconfigured(); + builder.addEnvironmentVariable("ONE", "one"); + var envVars = Map.of("TWO", "two", "THREE", "three"); + builder.addEnvironmentVariables(envVars); + + assertThat(builder.getEnvironmentVariables()).hasSize(3); + assertThat(builder.getEnvironmentVariables()).containsEntry("ONE", "one"); + assertThat(builder.getEnvironmentVariables()).containsAllEntriesOf(envVars); + } + + @Test + public void overrideEnvironmentVariables() { + var builder = ConfigEvaluatorBuilder.unconfigured(); + + var envVars1 = Map.of("TWO", "two", "THREE", "three"); + builder.addEnvironmentVariables(envVars1); + + var envVars2 = Map.of("FOUR", "four", "FIVE", "five"); + builder.setEnvironmentVariables(envVars2); + + assertThat(builder.getEnvironmentVariables()).hasSize(2); + assertThat(builder.getEnvironmentVariables()).containsAllEntriesOf(envVars2); + } + + @Test + public void preconfiguredBuilderContainsSystemProperties() { + var builder = ConfigEvaluatorBuilder.preconfigured(); + assertThat(builder.getExternalProperties()).isEqualTo(System.getProperties()); + } + + @Test + public void unconfiguredBuilderContainsNoExternalProperties() { + var builder = ConfigEvaluatorBuilder.unconfigured(); + assertThat(builder.getExternalProperties()).isEmpty(); + } + + @Test + public void addExternalProperties() { + var builder = ConfigEvaluatorBuilder.unconfigured(); + builder.addExternalProperty("ONE", "one"); + var properties = Map.of("TWO", "two", "THREE", "three"); + builder.addExternalProperties(properties); + + assertThat(builder.getExternalProperties()).hasSize(3); + assertThat(builder.getExternalProperties()).containsEntry("ONE", "one"); + assertThat(builder.getExternalProperties()).containsAllEntriesOf(properties); + } + + @Test + public void overrideExternalProperties() { + var builder = ConfigEvaluatorBuilder.unconfigured(); + + var properties1 = Map.of("TWO", "two", "THREE", "three"); + builder.addExternalProperties(properties1); + + var properties2 = Map.of("FOUR", "four", "FIVE", "five"); + builder.setExternalProperties(properties2); + + assertThat(builder.getExternalProperties()).hasSize(2); + assertThat(builder.getExternalProperties()).containsAllEntriesOf(properties2); + } + + @Test + public void setSecurityManager() { + var builder = ConfigEvaluatorBuilder.preconfigured(); + + assertThat(builder.getAllowedModules()).isEqualTo(SecurityManagers.defaultAllowedModules); + assertThat(builder.getAllowedResources()).isEqualTo(SecurityManagers.defaultAllowedResources); + + var manager = + SecurityManagers.standard(List.of(), List.of(), SecurityManagers.defaultTrustLevels, null); + + builder = ConfigEvaluatorBuilder.preconfigured().setSecurityManager(manager); + + assertThat(builder.getSecurityManager()).isSameAs(manager); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/ConfigTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigTest.java new file mode 100644 index 00000000..420e538c --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/ConfigTest.java @@ -0,0 +1,179 @@ +/** + * 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.config.java; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.text; + +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.pkl.config.java.mapper.Named; +import org.pkl.config.java.mapper.Types; +import org.pkl.core.PObject; + +public class ConfigTest { + private final ConfigEvaluator evaluator = ConfigEvaluator.preconfigured(); + + private final Config pigeonConfig = + evaluator.evaluate( + text( + "pigeon { age = 30; friends = List(\"john\", \"mary\"); address { street = \"Fuzzy St.\" } }")); + + private final Config pigeonModuleConfig = + evaluator.evaluate( + text("age = 30; friends = List(\"john\", \"mary\"); address { street = \"Fuzzy St.\" }")); + + private final Config pairConfig = + evaluator.evaluate(text("x { first = \"file/path\"; second = 42 }")); + + private final Config mapConfig = evaluator.evaluate(text("x = Map(\"one\", 1, \"two\", 2)")); + + @Test + public void navigate() { + var pigeon = pigeonConfig.get("pigeon"); + assertThat(pigeon.getQualifiedName()).isEqualTo("pigeon"); + assertThat(pigeon.getRawValue()).isInstanceOf(PObject.class); + + var address = pigeon.get("address"); + assertThat(address.getQualifiedName()).isEqualTo("pigeon.address"); + assertThat(address.getRawValue()).isInstanceOf(PObject.class); + + var street = address.get("street"); + assertThat(street.getQualifiedName()).isEqualTo("pigeon.address.street"); + assertThat(street.getRawValue()).isInstanceOf(String.class); + + assertThat(street.as(String.class)).isEqualTo("Fuzzy St."); + } + + @Test + public void navigateToNonExistingObjectChild() { + var pigeon = pigeonConfig.get("pigeon"); + var t = catchThrowable(() -> pigeon.get("non-existing")); + + assertThat(t) + .isInstanceOf(NoSuchChildException.class) + .hasMessageStartingWith( + "Node `pigeon` of type `pkl.base#Dynamic` " + + "does not have a property named `non-existing`."); + } + + @Test + public void navigateToNonExistingMapChild() { + var map = mapConfig.get("x"); + var t = catchThrowable(() -> map.get("non-existing")); + + assertThat(t) + .isInstanceOf(NoSuchChildException.class) + .hasMessageStartingWith( + "Node `x` of type `pkl.base#Map` " + "does not have a key named `non-existing`."); + } + + @Test + public void navigateToNonExistingLeafChild() { + var age = pigeonConfig.get("pigeon").get("age"); + var t = catchThrowable(() -> age.get("non-existing")); + + assertThat(t) + .isInstanceOf(NoSuchChildException.class) + .hasMessageStartingWith( + "Leaf node `pigeon.age` of type `pkl.base#Int` " + + "does not have a child named `non-existing`."); + } + + @Test + public void convertObjectToPojoByType() { + Person pigeon = pigeonConfig.get("pigeon").as(Person.class); + checkPigeon(pigeon); + } + + @Test + public void convertObjectToPojoByJavaType() { + var pigeon = pigeonConfig.get("pigeon").as(JavaType.of(Person.class)); + checkPigeon(pigeon); + } + + @Test + public void convertModuleToPojoByType() { + var pigeon = pigeonModuleConfig.as(Person.class); + checkPigeon(pigeon); + } + + @Test + public void convertModuleToPojoByJavaType() { + var pigeon = pigeonModuleConfig.as(JavaType.of(Person.class)); + checkPigeon(pigeon); + } + + private void checkPigeon(Person pigeon) { + assertThat(pigeon).isNotNull(); + assertThat(pigeon.age).isEqualTo(30); + assertThat(pigeon.friends).containsExactly("john", "mary"); + assertThat(pigeon.address.street).isEqualTo("Fuzzy St."); + } + + @Test + public void convertToParameterizedTypeByType() { + Pair pair = + pairConfig.get("x").as(Types.parameterizedType(Pair.class, Path.class, Integer.class)); + checkPair(pair); + } + + @Test + public void convertToParameterizedTypeByJavaType() { + var pair = pairConfig.get("x").as(new JavaType>() {}); + checkPair(pair); + } + + private void checkPair(Pair pair) { + assertThat(pair).isNotNull(); + assertThat(pair.first).isEqualTo(Path.of("file/path")); + assertThat(pair.second).isEqualTo(42); + } + + public static class Person { + final int age; + final List friends; + final Address address; + + public Person( + @Named("age") int age, + @Named("friends") List friends, + @Named("address") Address address) { + this.age = age; + this.friends = friends; + this.address = address; + } + } + + public static class Address { + final String street; + + public Address(@Named("street") String street) { + this.street = street; + } + } + + public static class Pair { + final S first; + final T second; + + public Pair(@Named("first") S first, @Named("second") T second) { + this.first = first; + this.second = second; + } + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/JavaTypeTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/JavaTypeTest.java new file mode 100644 index 00000000..b17b46fc --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/JavaTypeTest.java @@ -0,0 +1,128 @@ +/** + * 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.config.java; + +import static org.assertj.core.api.Assertions.*; + +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.util.*; +import org.junit.jupiter.api.Test; +import org.pkl.config.java.mapper.Reflection; +import org.pkl.config.java.mapper.Types; + +public class JavaTypeTest { + @Test + public void constructOptionalType() { + var type = JavaType.optionalOf(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.optionalOf(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Optional.class); + } + + @Test + public void constructArrayType() { + var type = JavaType.arrayOf(String.class); + assertThat(type).isEqualTo(new JavaType() {}); + assertThat(type).isEqualTo(JavaType.arrayOf(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType()).isArray()).isTrue(); + } + + @Test + public void constructIterableType() { + var type = JavaType.iterableOf(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.iterableOf(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Iterable.class); + } + + @Test + public void constructCollectionType() { + var type = JavaType.collectionOf(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.collectionOf(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Collection.class); + } + + @Test + public void constructListType() { + var type = JavaType.listOf(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.listOf(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(List.class); + } + + @Test + public void constructSetType() { + var type = JavaType.setOf(String.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.setOf(JavaType.of(String.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Set.class); + } + + @Test + public void constructMapType() { + var type = JavaType.mapOf(String.class, URI.class); + assertThat(type).isEqualTo(new JavaType>() {}); + assertThat(type).isEqualTo(JavaType.mapOf(JavaType.of(String.class), JavaType.of(URI.class))); + assertThat(Reflection.toRawType(type.getType())).isEqualTo(Map.class); + } + + @Test + public void usageAsTypeToken() { + var javaType = new JavaType>>() {}; + + assertThat(javaType.getType()).isEqualTo(Types.mapOf(String.class, Types.listOf(URI.class))); + } + + @Test + public void sameTypesConstructedInDifferentWaysAreEqual() { + var type1 = JavaType.mapOf(JavaType.of(String.class), JavaType.listOf(URI.class)); + var type2 = new JavaType>>() {}; + var type3 = JavaType.of(Types.mapOf(String.class, Types.listOf(URI.class))); + + assertThat(type1).isEqualTo(type1); + assertThat(type2).isEqualTo(type1); + assertThat(type3).isEqualTo(type2); + + assertThat(type2.hashCode()).isEqualTo(type1.hashCode()); + assertThat(type3.hashCode()).isEqualTo(type2.hashCode()); + } + + @Test + public void differentTypesAreNotEqual() { + var type1 = JavaType.mapOf(JavaType.of(String.class), JavaType.listOf(URI.class)); + var type2 = new JavaType>>() {}; + var type3 = JavaType.of(Types.mapOf(String.class, Types.listOf(Path.class))); + + assertThat(type2).isNotEqualTo(type1); + assertThat(type3).isNotEqualTo(type1); + assertThat(type2).isNotEqualTo(type3); + + // hopefully + assertThat(type2.hashCode()).isNotEqualTo(type1.hashCode()); + assertThat(type3.hashCode()).isNotEqualTo(type1.hashCode()); + assertThat(type3.hashCode()).isNotEqualTo(type2.hashCode()); + } + + @Test + public void sameStringRepresentationAsJavaLangReflectType() { + var type = JavaType.mapOf(String.class, URI.class); + assertThat(type.toString()).isEqualTo("java.util.Map"); + assertThat(type.toString()).isEqualTo(type.getType().toString()); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/ConversionsTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/ConversionsTest.java new file mode 100644 index 00000000..9bb77162 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/ConversionsTest.java @@ -0,0 +1,73 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; + +import java.io.File; +import java.nio.file.Path; +import java.time.temporal.ChronoUnit; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; +import org.pkl.core.Duration; +import org.pkl.core.DurationUnit; + +public class ConversionsTest { + @Test + public void pStringToFile() { + var file = Conversions.pStringToFile.converter.convert("relative/path", null); + assertThat(file).isEqualTo(new File("relative/path")); + + var file2 = Conversions.pStringToFile.converter.convert("/absolute/path", null); + assertThat(file2).isEqualTo(new File("/absolute/path")); + + var file3 = Conversions.pStringToFile.converter.convert("", null); + assertThat(file3).isEqualTo(new File("")); + } + + @Test + public void pStringToPath() { + var path = Conversions.pStringToPath.converter.convert("relative/path", null); + assertThat(path).isEqualTo(Path.of("relative/path")); + + var path2 = Conversions.pStringToPath.converter.convert("/absolute/path", null); + assertThat(path2).isEqualTo(Path.of("/absolute/path")); + + var path3 = Conversions.pStringToPath.converter.convert("", null); + assertThat(path3).isEqualTo(Path.of("")); + } + + @Test + public void pStringToPattern() { + var str = "(?i)\\w*"; + var pattern = Conversions.pStringToPattern.converter.convert(str, null); + assertThat(pattern.pattern()).isEqualTo(str); + } + + @Test + public void pRegexToString() { + var regex = Pattern.compile("(?i)\\w*"); + var str = Conversions.pRegexToString.converter.convert(regex, null); + assertThat(str).isEqualTo("(?i)\\w*"); + } + + @Test + public void pDurationToDuration() { + var pDuration = new Duration(100, DurationUnit.MINUTES); + var duration = Conversions.pDurationToDuration.converter.convert(pDuration, null); + assertThat(duration).isEqualTo(java.time.Duration.of(100, ChronoUnit.MINUTES)); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PAnyToOptionalTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PAnyToOptionalTest.java new file mode 100644 index 00000000..9825c030 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PAnyToOptionalTest.java @@ -0,0 +1,63 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; + +public class PAnyToOptionalTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PAnyToOptionalTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + Optional mapped = mapper.map(ex1, Types.optionalOf(String.class)); + + assertThat(mapped).isEmpty(); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + Optional mapped = mapper.map(ex2, Types.optionalOf(String.class)); + + assertThat(mapped).contains("str"); + } + + @Test + public void ex3() { + var ex3 = module.getProperty("ex3"); + Optional> mapped = mapper.map(ex3, Types.optionalOf(Types.listOf(Integer.class))); + + assertThat(mapped).contains(List.of(1, 2, 3)); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PCollectionToArrayTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PCollectionToArrayTest.java new file mode 100644 index 00000000..27d65868 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PCollectionToArrayTest.java @@ -0,0 +1,85 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; + +public class PCollectionToArrayTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PCollectionToArrayTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + assertThat(mapper.map(ex1, byte[].class)).isEqualTo(new byte[0]); + assertThat(mapper.map(ex1, short[].class)).isEqualTo(new short[0]); + assertThat(mapper.map(ex1, int[].class)).isEqualTo(new int[0]); + assertThat(mapper.map(ex1, long[].class)).isEqualTo(new long[0]); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + assertThat(mapper.map(ex2, byte[].class)).isEqualTo(new byte[] {1, 2, 3}); + assertThat(mapper.map(ex2, short[].class)).isEqualTo(new short[] {1, 2, 3}); + assertThat(mapper.map(ex2, int[].class)).isEqualTo(new int[] {1, 2, 3}); + assertThat(mapper.map(ex2, long[].class)).isEqualTo(new long[] {1, 2, 3}); + } + + @Test + public void ex3() { + var ex3 = module.getProperty("ex3"); + assertThat(mapper.map(ex3, byte[].class)).isEqualTo(new byte[] {1, 2, 3}); + assertThat(mapper.map(ex3, short[].class)).isEqualTo(new short[] {1, 2, 3}); + assertThat(mapper.map(ex3, int[].class)).isEqualTo(new int[] {1, 2, 3}); + assertThat(mapper.map(ex3, long[].class)).isEqualTo(new long[] {1, 2, 3}); + } + + @Test + public void ex4() { + var ex4 = module.getProperty("ex4"); + assertThat(mapper.map(ex4, float[].class)).isEqualTo(new float[] {1f, 2f, 3.3f}); + assertThat(mapper.map(ex4, double[].class)).isEqualTo(new double[] {1d, 2d, 3.3d}); + } + + @Test + public void ex5() { + var ex5 = module.getProperty("ex5"); + assertThat(mapper.map(ex5, boolean[].class)).isEqualTo(new boolean[] {true, false, true}); + } + + @Test + public void ex6() { + var ex6 = module.getProperty("ex6"); + assertThat(mapper.map(ex6, Person[].class)) + .isEqualTo(new Person[] {new Person("pigeon", 40), new Person("parrot", 30)}); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PCollectionToCollectionTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PCollectionToCollectionTest.java new file mode 100644 index 00000000..eead0eda --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PCollectionToCollectionTest.java @@ -0,0 +1,117 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; + +public class PCollectionToCollectionTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PCollectionToCollectionTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + + List mapped1 = mapper.map(ex1, Types.listOf(Byte.class)); + assertThat(mapped1).isEmpty(); + + List mapped2 = mapper.map(ex1, Types.listOf(Short.class)); + assertThat(mapped2).isEmpty(); + + List mapped3 = mapper.map(ex1, Types.listOf(Integer.class)); + assertThat(mapped3).isEmpty(); + + List mapped4 = mapper.map(ex1, Types.listOf(Long.class)); + assertThat(mapped4).isEmpty(); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + + List mapped1 = mapper.map(ex2, Types.listOf(Byte.class)); + assertThat(mapped1).containsExactly((byte) 1, (byte) 2, (byte) 3); + + List mapped2 = mapper.map(ex2, Types.listOf(Short.class)); + assertThat(mapped2).containsExactly((short) 1, (short) 2, (short) 3); + + List mapped3 = mapper.map(ex2, Types.listOf(Integer.class)); + assertThat(mapped3).containsExactly(1, 2, 3); + + List mapped4 = mapper.map(ex2, Types.listOf(Long.class)); + assertThat(mapped4).containsExactly(1L, 2L, 3L); + } + + @Test + public void ex3() { + var ex3 = module.getProperty("ex3"); + + List mapped1 = mapper.map(ex3, Types.listOf(Byte.class)); + assertThat(mapped1).containsExactly((byte) 1, (byte) 2, (byte) 3); + + List mapped2 = mapper.map(ex3, Types.listOf(Short.class)); + assertThat(mapped2).containsExactly((short) 1, (short) 2, (short) 3); + + List mapped3 = mapper.map(ex3, Types.listOf(Integer.class)); + assertThat(mapped3).containsExactly(1, 2, 3); + + List mapped4 = mapper.map(ex3, Types.listOf(Long.class)); + assertThat(mapped4).containsExactly(1L, 2L, 3L); + } + + @Test + public void ex4() { + var ex4 = module.getProperty("ex4"); + + List mapped1 = mapper.map(ex4, Types.listOf(Float.class)); + assertThat(mapped1).containsExactly(1f, 2f, 3.3f); + + List mapped2 = mapper.map(ex4, Types.listOf(Double.class)); + assertThat(mapped2).containsExactly(1d, 2d, 3.3d); + } + + @Test + public void ex5() { + var ex5 = module.getProperty("ex5"); + List mapped = mapper.map(ex5, Types.listOf(Boolean.class)); + assertThat(mapped).containsExactly(true, false, true); + } + + @Test + public void ex6() { + var ex6 = module.getProperty("ex6"); + List mapped = mapper.map(ex6, Types.listOf(Person.class)); + Assertions.assertThat(mapped) + .containsExactly(new Person("pigeon", 40), new Person("parrot", 30)); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PMapToMapTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PMapToMapTest.java new file mode 100644 index 00000000..289a7c45 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PMapToMapTest.java @@ -0,0 +1,115 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.*; + +public class PMapToMapTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PMapToMapTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + Map mapped = mapper.map(ex1, Types.mapOf(Integer.class, Integer.class)); + + assertThat(mapped).isEmpty(); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + Map mapped = mapper.map(ex2, Types.mapOf(Integer.class, Integer.class)); + assertThat(mapped).containsOnly(entry(1, 2), entry(2, 4), entry(3, 6)); + + Map mapped2 = mapper.map(ex2, Types.mapOf(Byte.class, Double.class)); + assertThat(mapped2).containsOnly(entry((byte) 1, 2d), entry((byte) 2, 4d), entry((byte) 3, 6d)); + } + + @Test + public void ex3() { + var ex3 = module.getProperty("ex3"); + Map mapped = mapper.map(ex3, Types.mapOf(Integer.class, Double.class)); + + assertThat(mapped).containsOnly(entry(1, 2d), entry(2, 4d), entry(3, 6.6d)); + } + + @Test + public void ex4() { + var ex4 = module.getProperty("ex4"); + + Map> mapped = + mapper.map(ex4, Types.mapOf(String.class, Types.mapOf(String.class, Object.class))); + + Map pigeon = Map.of("name", "pigeon", "age", 40L); + Map parrot = Map.of("name", "parrot", "age", 30L); + + assertThat(mapped).containsOnly(entry("pigeon", pigeon), entry("parrot", parrot)); + } + + @Test + public void ex5() { + var ex5 = module.getProperty("ex5"); + Map mapped = mapper.map(ex5, Types.mapOf(String.class, Person.class)); + + assertThat(mapped) + .containsOnly( + entry("pigeon", new Person("pigeon", 40)), entry("parrot", new Person("parrot", 30))); + } + + @Test + public void ex6() { + var ex6 = module.getProperty("ex6"); + Map mapped = mapper.map(ex6, Types.mapOf(Person.class, String.class)); + + assertThat(mapped) + .containsOnly( + entry(new Person("pigeon", 40), "pigeon"), entry(new Person("parrot", 30), "parrot")); + } + + @Test + public void ex7() { + var mapper = + ValueMapperBuilder.preconfigured() + .addConversion( + Conversion.of(PClassInfo.Int, String.class, (num, mapper2) -> String.valueOf(num))) + .build(); + var ex7 = module.getProperty("ex7"); + // conversion from PInt to String kicks in because PMapToMap treats Properties as + // Map + var properties = mapper.map(ex7, Properties.class); + + assertThat(properties).hasSize(2); + assertThat(properties.getProperty("1")).isEqualTo("2"); + assertThat(properties.getProperty("2")).isEqualTo("4"); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PModuleToDataObjectTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PModuleToDataObjectTest.java new file mode 100644 index 00000000..1de953bf --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PModuleToDataObjectTest.java @@ -0,0 +1,52 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.config.java.mapper.PObjectToDataObjectTest.Address; +import org.pkl.config.java.mapper.PObjectToDataObjectTest.Hobby; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; + +public class PModuleToDataObjectTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PModuleToDataObjectTest.pkl")); + PObjectToDataObjectTest.Person pigeon = + new PObjectToDataObjectTest.Person( + "pigeon", + 40, + EnumSet.of(Hobby.SURFING, Hobby.SWIMMING), + new Address("sesame street", 94105)); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void doit() { + assertThat(mapper.map(module, PObjectToDataObjectTest.Person.class)).isEqualTo(pigeon); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PNullToAnyTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PNullToAnyTest.java new file mode 100644 index 00000000..865f579a --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PNullToAnyTest.java @@ -0,0 +1,41 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.pkl.core.PNull; + +public class PNullToAnyTest { + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @Test + public void test() { + // due to Conversions.identities + assertThat(mapper.map(PNull.getInstance(), PNull.class)).isEqualTo(PNull.getInstance()); + + assertThat(mapper.map(PNull.getInstance(), String.class)).isNull(); + assertThat(mapper.map(PNull.getInstance(), Person.class)).isNull(); + assertThat(mapper.map(PNull.getInstance(), Integer.class)).isNull(); + + assertThatThrownBy(() -> mapper.map(PNull.getInstance(), int.class)) + .isInstanceOf(ConversionException.class); + } + + public static class Person {} +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectJavaxInjectTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectJavaxInjectTest.java new file mode 100644 index 00000000..fc34dcf4 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectJavaxInjectTest.java @@ -0,0 +1,162 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; +import javax.inject.Named; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; +import org.pkl.core.util.Nullable; + +public class PObjectToDataObjectJavaxInjectTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PObjectToDataObjectTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + private static final Person pigeon = + new Person( + "pigeon", + 40, + EnumSet.of(Hobby.SURFING, Hobby.SWIMMING), + new Address("sesame street", 94105)); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + + assertThat(mapper.map(ex1, Person.class)).isEqualTo(pigeon); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + + assertThat(mapper.map(ex2, Person.class)).isEqualTo(pigeon); + } + + @Test + public void ex3() { + var ex3 = module.getProperty("ex3"); + Object mapped = + mapper.map(ex3, Types.parameterizedType(Pair.class, String.class, Integer.class)); + + assertThat(mapped).isEqualTo(new Pair<>("foo", 42)); + } + + static class Person { + final String name; + final int age; + final Set hobbies; + final Address address; + + Person( + @Named("name") String name, + @Named("age") int age, + @Named("hobbies") Set hobbies, + @Named("address") Address address) { + this.name = name; + this.age = age; + this.hobbies = hobbies; + this.address = address; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Person)) return false; + + var other = (Person) obj; + return name.equals(other.name) + && age == other.age + && hobbies.equals(other.hobbies) + && address.equals(other.address); + } + + @Override + public int hashCode() { + return Objects.hash(name, age, hobbies, address); + } + } + + static class Address { + final String street; + final int zip; + + Address(@Named("street") String street, @Named("zip") int zip) { + this.street = street; + this.zip = zip; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Address)) return false; + + var other = (Address) obj; + return street.equals(other.street) && zip == other.zip; + } + + @Override + public int hashCode() { + return Objects.hash(street, zip); + } + } + + public enum Hobby { + SWIMMING, + SURFING, + READING + } + + public static class Pair { + public final S first; + public final T second; + + public Pair(@Named("first") S first, @Named("second") T second) { + this.first = first; + this.second = second; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Pair)) return false; + + var other = (Pair) obj; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return Objects.hash(first, second); + } + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectOverriddenPropertyTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectOverriddenPropertyTest.java new file mode 100644 index 00000000..6e60bd46 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectOverriddenPropertyTest.java @@ -0,0 +1,37 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.OverriddenProperty; +import org.junit.jupiter.api.Test; +import org.pkl.config.java.ConfigEvaluator; +import org.pkl.core.ModuleSource; + +class PObjectToDataObjectOverriddenPropertyTest { + @Test + void overriddenProperty() { + try (var evaluator = ConfigEvaluator.preconfigured()) { + var result = + evaluator + .evaluate(ModuleSource.modulePath("/codegenPkl/OverriddenProperty.pkl")) + .as(OverriddenProperty.class); + assertThat(result.theClass.bar.get(0).prop1).isEqualTo("hello"); + assertThat(result.theClass.bar.get(0).prop2).isEqualTo("hello again"); + } + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectTest.java new file mode 100644 index 00000000..8cb7022d --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToDataObjectTest.java @@ -0,0 +1,255 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.beans.ConstructorProperties; +import java.util.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; +import org.pkl.core.util.Nullable; + +public class PObjectToDataObjectTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PObjectToDataObjectTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + private static final Person pigeon = + new Person( + "pigeon", + 40, + EnumSet.of(Hobby.SURFING, Hobby.SWIMMING), + new Address("sesame street", 94105)); + + private static final PersonConstructoProperties pigeon2 = + new PersonConstructoProperties( + "pigeon", + 40, + EnumSet.of(Hobby.SURFING, Hobby.SWIMMING), + new Address("sesame street", 94105)); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + + assertThat(mapper.map(ex1, Person.class)).isEqualTo(pigeon); + } + + @Test + public void ex1_constructor_properties() { + var ex1 = module.getProperty("ex1"); + assertThat(mapper.map(ex1, PersonConstructoProperties.class)).isEqualTo(pigeon2); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + + assertThat(mapper.map(ex2, Person.class)).isEqualTo(pigeon); + } + + @Test + public void ex3() { + var ex3 = module.getProperty("ex3"); + Object mapped = + mapper.map(ex3, Types.parameterizedType(Pair.class, String.class, Integer.class)); + + assertThat(mapped).isEqualTo(new Pair<>("foo", 42)); + } + + @Test + public void ex4() { + var ex4 = module.getProperty("ex4"); + var t = catchThrowable(() -> mapper.map(ex4, Address.class)); + + assertThat(t).isInstanceOf(ConversionException.class); + } + + @Test + public void ex5() { + var ex5 = module.getProperty("ex5"); + + assertThat(mapper.map(ex5, UpperBounds.class).numbers).isEqualTo(List.of(1L, 2L, 3L)); + assertThat(mapper.map(ex5, LowerBounds.class).numbers).isEqualTo(List.of(1, 2, 3)); + } + + @Test + public void errorMessageNamesPropertyWhoseConversionFailed() { + var ex3 = module.getProperty("ex3"); + + var t = + catchThrowable( + () -> + mapper.map(ex3, Types.parameterizedType(Pair.class, Integer.class, Integer.class))); + + assertThat(t).isInstanceOf(ConversionException.class); + assertThat(t.getMessage()) + .startsWith( + "Error converting property `first` in Pkl object of type `Dynamic` " + + "to equally named constructor parameter in Java class " + + "`org.pkl.config.java.mapper." + + "PObjectToDataObjectTest$Pair`:"); + } + + static class Person { + final String name; + final int age; + final Set hobbies; + final Address address; + + Person( + @Named("name") String name, + @Named("age") int age, + @Named("hobbies") Set hobbies, + @Named("address") Address address) { + this.name = name; + this.age = age; + this.hobbies = hobbies; + this.address = address; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Person)) return false; + + var other = (Person) obj; + return name.equals(other.name) + && age == other.age + && hobbies.equals(other.hobbies) + && address.equals(other.address); + } + + @Override + public int hashCode() { + return Objects.hash(name, age, hobbies, address); + } + } + + static class PersonConstructoProperties { + final String name; + final int age; + final Set hobbies; + final Address address; + + @ConstructorProperties({"name", "age", "hobbies", "address"}) + PersonConstructoProperties(String name, int age, Set hobbies, Address address) { + this.name = name; + this.age = age; + this.hobbies = hobbies; + this.address = address; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof PersonConstructoProperties)) return false; + + var other = (PersonConstructoProperties) obj; + return name.equals(other.name) + && age == other.age + && hobbies.equals(other.hobbies) + && address.equals(other.address); + } + + @Override + public int hashCode() { + return Objects.hash(name, age, hobbies, address); + } + } + + static class Address { + final String street; + final int zip; + + Address(@Named("street") String street, @Named("zip") int zip) { + this.street = street; + this.zip = zip; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Address)) return false; + + var other = (Address) obj; + return street.equals(other.street) && zip == other.zip; + } + + @Override + public int hashCode() { + return Objects.hash(street, zip); + } + } + + enum Hobby { + SWIMMING, + SURFING, + READING + } + + static class Pair { + final S first; + final T second; + + Pair(@Named("first") S first, @Named("second") T second) { + this.first = first; + this.second = second; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Pair)) return false; + + var other = (Pair) obj; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return Objects.hash(first, second); + } + } + + static class UpperBounds { + final List numbers; + + UpperBounds(@Named("numbers") List numbers) { + this.numbers = numbers; + } + } + + static class LowerBounds { + final List numbers; + + LowerBounds(@Named("numbers") List numbers) { + this.numbers = numbers; + } + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToInnerClassTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToInnerClassTest.java new file mode 100644 index 00000000..b0040b74 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToInnerClassTest.java @@ -0,0 +1,49 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.text; + +import org.junit.jupiter.api.Test; +import org.pkl.config.java.ConfigEvaluator; + +public class PObjectToInnerClassTest { + @SuppressWarnings("InnerClassMayBeStatic") + public class InnerConfig { + final String text; + + public InnerConfig(@Named("text") String text) { + this.text = text; + } + } + + // verify that a workaround for https://bugs.openjdk.java.net/browse/JDK-8025806 is in place + // conversion to inner class is still expected to fail but with the usual `ConversionException` + @Test + public void attemptToConvertToInnerClassDoesNotFailWithIndexOutOfBoundsException() { + try (var evaluator = ConfigEvaluator.preconfigured()) { + var config = + evaluator.evaluate( + text("class Inner {\n" + " text: String = \"Bar\"\n" + "}\n" + "inner: Inner")); + + assertThatExceptionOfType(ConversionException.class) + .isThrownBy(() -> config.get("inner").as(InnerConfig.class)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToPObjectTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToPObjectTest.java new file mode 100644 index 00000000..122e2b31 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PObjectToPObjectTest.java @@ -0,0 +1,67 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pkl.core.*; + +public class PObjectToPObjectTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PObjectToPObjectTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + private PObject pigeon; + private PObject parrot; + + @BeforeEach + public void before() { + Map pigeonProps = Map.of("name", "pigeon", "age", 40L); + pigeon = new PObject(PClassInfo.Dynamic, pigeonProps); + + Map parrotProps = Map.of("name", "parrot", "age", 30L); + parrot = new PObject(PClassInfo.Dynamic, parrotProps); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + + assertThat(mapper.map(ex1, PObject.class)).isEqualTo(pigeon); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + List mapped = mapper.map(ex2, Types.listOf(PObject.class)); + + assertThat(mapped).containsExactly(pigeon, parrot); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PPairToPairTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PPairToPairTest.java new file mode 100644 index 00000000..30020fec --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PPairToPairTest.java @@ -0,0 +1,57 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.pkl.core.ModuleSource.modulePath; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.*; + +public class PPairToPairTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PPairToPairTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.getProperty("ex1"); + Pair mapped = mapper.map(ex1, Types.pairOf(Integer.class, Duration.class)); + assertThat(mapped).isEqualTo(new Pair<>(1, new Duration(3.0, DurationUnit.SECONDS))); + } + + @Test + public void ex2() { + var ex2 = module.getProperty("ex2"); + Pair mapped = mapper.map(ex2, Types.pairOf(PObject.class, PObject.class)); + + assertThat(mapped.getFirst().getProperties()) + .containsOnly(entry("name", "pigeon"), entry("age", 40L)); + + assertThat(mapped.getSecond().getProperties()) + .containsOnly(entry("name", "parrot"), entry("age", 30L)); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PStringToEnumTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PStringToEnumTest.java new file mode 100644 index 00000000..cc3c34de --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PStringToEnumTest.java @@ -0,0 +1,114 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.DataSizeUnit; +import org.pkl.core.DurationUnit; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; + +public class PStringToEnumTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PStringToEnumTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + public enum Hobby { + READING, + SWIMMING, + COUCH_SURFING + } + + @Test + public void ex1() { + assertThat(mapper.map(module.getProperty("ex1"), Hobby.class)).isEqualTo(Hobby.COUCH_SURFING); + } + + @Test + public void ex2() { + assertThat(mapper.map(module.getProperty("ex2"), Hobby.class)).isEqualTo(Hobby.COUCH_SURFING); + } + + @Test + public void ex3() { + assertThat(mapper.map(module.getProperty("ex3"), Hobby.class)).isEqualTo(Hobby.COUCH_SURFING); + } + + @Test + public void ex4() { + assertThat(mapper.map(module.getProperty("ex4"), Hobby.class)).isEqualTo(Hobby.COUCH_SURFING); + } + + @Test + public void ex5() { + assertThat(mapper.map(module.getProperty("ex5"), Hobby.class)).isEqualTo(Hobby.COUCH_SURFING); + } + + @Test + public void ex6() { + assertThat(mapper.map(module.getProperty("ex6"), Hobby.class)).isEqualTo(Hobby.COUCH_SURFING); + } + + @Test + public void ex7() { + List mapped = mapper.map(module.getProperty("ex7"), Types.listOf(Hobby.class)); + + assertThat(mapped).containsExactly(Hobby.SWIMMING, Hobby.READING, Hobby.COUCH_SURFING); + } + + @Test + public void ex8() { + List mapped = mapper.map(module.getProperty("ex8"), Types.listOf(Hobby.class)); + + assertThat(mapped).containsExactly(Hobby.COUCH_SURFING, Hobby.COUCH_SURFING); + } + + @Test + public void ex9() { + var t = catchThrowable(() -> mapper.map(module.getProperty("ex9"), Types.listOf(Hobby.class))); + + assertThat(t) + .isInstanceOf(ConversionException.class) + .hasMessage( + "Cannot convert String `other` to Enum value " + + "of type `org.pkl.config.java.mapper.PStringToEnumTest$Hobby`."); + } + + @Test + public void ex11() { + var unit = mapper.map(module.getProperty("ex11"), DurationUnit.class); + assertThat(unit).isEqualTo(DurationUnit.MINUTES); + } + + @Test + public void ex12() { + var unit = mapper.map(module.getProperty("ex12"), DataSizeUnit.class); + assertThat(unit).isEqualTo(DataSizeUnit.GIGABYTES); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PStringToVersionTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PStringToVersionTest.java new file mode 100644 index 00000000..c693e4ca --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PStringToVersionTest.java @@ -0,0 +1,96 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.pkl.core.ModuleSource.modulePath; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; +import org.pkl.core.Version; + +public class PStringToVersionTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PStringToVersionTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.get("ex1"); + assert ex1 != null; + var mapped = mapper.map(ex1, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3")); + } + + @Test + public void ex2() { + var ex2 = module.get("ex2"); + assert ex2 != null; + var mapped = mapper.map(ex2, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3-rc.1")); + } + + @Test + public void ex3() { + var ex3 = module.get("ex3"); + assert ex3 != null; + var mapped = mapper.map(ex3, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3+456.789")); + } + + @Test + public void ex4() { + var ex4 = module.get("ex4"); + assert ex4 != null; + var mapped = mapper.map(ex4, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3-rc.1+456.789")); + } + + @Test + public void ex5() { + var ex5 = module.get("ex5"); + assertThatThrownBy( + () -> { + assert ex5 != null; + mapper.map(ex5, Version.class); + }) + .isInstanceOf(ConversionException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void ex6() { + var ex6 = module.get("ex6"); + assertThatThrownBy( + () -> { + assert ex6 != null; + mapper.map(ex6, Version.class); + }) + .isInstanceOf(ConversionException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PVersionToStringTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PVersionToStringTest.java new file mode 100644 index 00000000..b4ee89c2 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PVersionToStringTest.java @@ -0,0 +1,78 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.pkl.core.ModuleSource.modulePath; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.Evaluator; +import org.pkl.core.PModule; + +public class PVersionToStringTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PVersionToVersionTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.get("ex1"); + assert ex1 != null; + var mapped = mapper.map(ex1, String.class); + assertThat(mapped).isEqualTo("1.2.3"); + } + + @Test + public void ex2() { + var ex2 = module.get("ex2"); + assert ex2 != null; + var mapped = mapper.map(ex2, String.class); + assertThat(mapped).isEqualTo("1.2.3-rc.1"); + } + + @Test + public void ex3() { + var ex3 = module.get("ex3"); + assert ex3 != null; + var mapped = mapper.map(ex3, String.class); + assertThat(mapped).isEqualTo("1.2.3+456.789"); + } + + @Test + public void ex4() { + var ex4 = module.get("ex4"); + assert ex4 != null; + var mapped = mapper.map(ex4, String.class); + assertThat(mapped).isEqualTo("1.2.3-rc.1+456.789"); + } + + @Test + public void ex5() { + var ex5 = module.get("ex5"); + assert ex5 != null; + var mapped = mapper.map(ex5, String.class); + assertThat(mapped).isEqualTo("999999999999999.0.0"); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PVersionToVersionTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PVersionToVersionTest.java new file mode 100644 index 00000000..c3b7dca4 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PVersionToVersionTest.java @@ -0,0 +1,81 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; +import static org.pkl.core.ModuleSource.modulePath; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.pkl.core.*; + +public class PVersionToVersionTest { + private static final Evaluator evaluator = Evaluator.preconfigured(); + + private static final PModule module = + evaluator.evaluate(modulePath("org/pkl/config/java/mapper/PVersionToVersionTest.pkl")); + + private static final ValueMapper mapper = ValueMapperBuilder.preconfigured().build(); + + @AfterAll + public static void afterAll() { + evaluator.close(); + } + + @Test + public void ex1() { + var ex1 = module.get("ex1"); + assert ex1 != null; + var mapped = mapper.map(ex1, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3")); + } + + @Test + public void ex2() { + var ex2 = module.get("ex2"); + assert ex2 != null; + var mapped = mapper.map(ex2, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3-rc.1")); + } + + @Test + public void ex3() { + var ex3 = module.get("ex3"); + assert ex3 != null; + var mapped = mapper.map(ex3, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3+456.789")); + } + + @Test + public void ex4() { + var ex4 = module.get("ex4"); + assert ex4 != null; + var mapped = mapper.map(ex4, Version.class); + assertThat(mapped).isEqualTo(Version.parse("1.2.3-rc.1+456.789")); + } + + @Test + public void ex5() { + var ex5 = module.get("ex5"); + assertThatThrownBy( + () -> { + assert ex5 != null; + mapper.map(ex5, Version.class); + }) + .isInstanceOf(ConversionException.class) + .hasCauseInstanceOf(ArithmeticException.class); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/Person.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/Person.java new file mode 100644 index 00000000..2731f330 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/Person.java @@ -0,0 +1,42 @@ +/** + * 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.config.java.mapper; + +import org.pkl.core.util.Nullable; + +public class Person { + public final String name; + public final int age; + + public Person(@Named("name") String name, @Named("age") int age) { + this.name = name; + this.age = age; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Person)) return false; + + var other = (Person) obj; + return name.equals(other.name) && age == other.age; + } + + @Override + public int hashCode() { + return name.hashCode() * 31 + Integer.hashCode(age); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PolymorphicTest.kt b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PolymorphicTest.kt new file mode 100644 index 00000000..22f1b839 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/PolymorphicTest.kt @@ -0,0 +1,22 @@ +package org.pkl.config.java.mapper + +import org.pkl.config.java.ConfigEvaluator +import org.pkl.core.ModuleSource +import com.example.Lib +import com.example.PolymorphicModuleTest +import com.example.PolymorphicModuleTest.Strudel +import com.example.PolymorphicModuleTest.TurkishDelight +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class PolymorphicTest { + @Test + fun `deserializing polymorphic objects`() { + val evaluator = ConfigEvaluator.preconfigured() + val module = evaluator.evaluate(ModuleSource.modulePath("/codegenPkl/PolymorphicModuleTest.pkl")).`as`(PolymorphicModuleTest::class.java) + assertThat(module.desserts[0]).isInstanceOf(Strudel::class.java) + assertThat(module.desserts[1]).isInstanceOf(TurkishDelight::class.java) + assertThat(module.planes[0]).isInstanceOf(Lib.Jet::class.java) + assertThat(module.planes[1]).isInstanceOf(Lib.Propeller::class.java) + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/ReflectionTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/ReflectionTest.java new file mode 100644 index 00000000..49fb2f07 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/ReflectionTest.java @@ -0,0 +1,97 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class ReflectionTest { + @SuppressWarnings("unused") + static class Container { + Container(T element) {} + + void setElement(T element) {} + } + + static class Person {} + + @Test + public void isMissingTypeArguments() { + assertThat( + Reflection.isMissingTypeArguments( + Types.parameterizedType(Container.class, Person.class))) + .isFalse(); + assertThat(Reflection.isMissingTypeArguments(Container.class)).isTrue(); + assertThat(Reflection.isMissingTypeArguments(Person.class)).isFalse(); + } + + @Test + public void toRawType() { + var type = Types.listOf(Person.class); + + assertThat(Reflection.toRawType(type)).isEqualTo(List.class); + assertThat(Reflection.toRawType(List.class)).isEqualTo(List.class); + } + + @Test + public void toWrapperType() { + assertThat(Reflection.toWrapperType(float.class)).isEqualTo(Float.class); + assertThat(Reflection.toWrapperType(Person.class)).isEqualTo(Person.class); + } + + @Test + public void getArrayElementType() { + assertThat(Reflection.getArrayElementType(int[].class)).isEqualTo(int.class); + assertThat(Reflection.getArrayElementType(Person[].class)).isEqualTo(Person.class); + + var containerOfPerson = Types.parameterizedType(Container.class, Person.class); + assertThat(Reflection.getArrayElementType(Types.arrayOf(containerOfPerson))) + .isEqualTo(containerOfPerson); + } + + @Test + public void getExactSupertype() { + assertThat( + Reflection.getExactSupertype( + Types.parameterizedType(ArrayList.class, Person.class), Collection.class)) + .isEqualTo(Types.parameterizedType(Collection.class, Person.class)); + } + + @Test + public void getExactSubtype() { + assertThat( + Reflection.getExactSubtype( + Types.parameterizedType(Collection.class, Person.class), ArrayList.class)) + .isEqualTo(Types.parameterizedType(ArrayList.class, Person.class)); + } + + @Test + public void getExactParameterTypes() { + var type = Types.parameterizedType(Container.class, Person.class); + + var ctor = Container.class.getDeclaredConstructors()[0]; + assertThat(Reflection.getExactParameterTypes(ctor, type)).isEqualTo(new Type[] {Person.class}); + + var method = Container.class.getDeclaredMethods()[0]; + assertThat(Reflection.getExactParameterTypes(method, type)) + .isEqualTo(new Type[] {Person.class}); + } +} diff --git a/pkl-config-java/src/test/java/org/pkl/config/java/mapper/TypesTest.java b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/TypesTest.java new file mode 100644 index 00000000..39d78ed5 --- /dev/null +++ b/pkl-config-java/src/test/java/org/pkl/config/java/mapper/TypesTest.java @@ -0,0 +1,123 @@ +/** + * 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.config.java.mapper; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URL; +import java.util.*; +import org.junit.jupiter.api.Test; + +public class TypesTest { + @Test + public void createParameterizedType() {} + + @Test + public void createParameterizedTypeForClassWithoutTypeParameters() { + var t = catchThrowable(() -> Types.parameterizedType(String.class)); + + assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Cannot parameterize `java.lang.String` " + + "because it does not have any type parameters."); + } + + @Test + public void createParameterizedTypeWithWrongNumberOfTypeArguments() { + var t = + catchThrowable( + () -> Types.parameterizedType(Map.class, Integer.class, String.class, URL.class)); + + assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Expected 2 type arguments for `java.util.Map`, but got 3."); + } + + @Test + public void createParameterizedTypeWithPrimitiveTypeArgument() { + Throwable t = catchThrowable(() -> Types.parameterizedType(List.class, int.class)); + + assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("`int.class` is not a valid type argument. Did you mean `Integer.class`?"); + } + + @SuppressWarnings("unused") + static class Foo {} + + static class Bar {} + + @Test + public void createParameterizedTypeWithIncompatibleTypeArgument() { + Throwable t = catchThrowable(() -> Types.parameterizedType(Foo.class, String.class)); + + assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Type argument `java.lang.String` for type parameter `T` is " + + "not within bound `org.pkl.config.java.mapper.TypesTest$Bar`."); + } + + @Test + public void createPrimitiveArrayType() { + assertThat(Types.arrayOf(int.class)).isEqualTo(int[].class); + } + + static class Person {} + + @Test + public void createObjectArrayType() { + assertThat(Types.arrayOf(Person.class)).isEqualTo(Person[].class); + } + + @Test + public void createIterableType() { + ParameterizedType type = Types.iterableOf(Person.class); + assertThat(type.getRawType()).isEqualTo(Iterable.class); + assertThat(type.getActualTypeArguments()).isEqualTo(new Type[] {Person.class}); + } + + @Test + public void createCollectionType() { + ParameterizedType type = Types.collectionOf(Person.class); + assertThat(type.getRawType()).isEqualTo(Collection.class); + assertThat(type.getActualTypeArguments()).isEqualTo(new Type[] {Person.class}); + } + + @Test + public void createListType() { + ParameterizedType type = Types.listOf(Person.class); + assertThat(type.getRawType()).isEqualTo(List.class); + assertThat(type.getActualTypeArguments()).isEqualTo(new Type[] {Person.class}); + } + + @Test + public void createSetType() { + ParameterizedType type = Types.setOf(Person.class); + assertThat(type.getRawType()).isEqualTo(Set.class); + assertThat(type.getActualTypeArguments()).isEqualTo(new Type[] {Person.class}); + } + + @Test + public void createMapType() { + ParameterizedType type = Types.mapOf(String.class, Person.class); + assertThat(type.getRawType()).isEqualTo(Map.class); + assertThat(type.getActualTypeArguments()).isEqualTo(new Type[] {String.class, Person.class}); + } +} diff --git a/pkl-config-java/src/test/resources/codegenPkl/OverriddenProperty.pkl b/pkl-config-java/src/test/resources/codegenPkl/OverriddenProperty.pkl new file mode 100644 index 00000000..c78a5d0a --- /dev/null +++ b/pkl-config-java/src/test/resources/codegenPkl/OverriddenProperty.pkl @@ -0,0 +1,28 @@ +module com.example.OverriddenProperty + +abstract class BaseClass { + fixed bar: Listing = new { + new { + prop1 = "hello" + } + } +} + +theClass: TheClass + +class TheClass extends BaseClass { + fixed bar: Listing = new { + new { + prop1 = "hello" + prop2 = "hello again" + } + } +} + +open class BaseBar { + prop1: String +} + +class Bar extends BaseBar { + prop2: String +} diff --git a/pkl-config-java/src/test/resources/codegenPkl/PolymorphicLib.pkl b/pkl-config-java/src/test/resources/codegenPkl/PolymorphicLib.pkl new file mode 100644 index 00000000..0cd25149 --- /dev/null +++ b/pkl-config-java/src/test/resources/codegenPkl/PolymorphicLib.pkl @@ -0,0 +1,14 @@ +module com.example.lib + +open class Airplane { + name: String + numSeats: Int +} + +class Jet extends Airplane { + isSuperSonic: Boolean +} + +class Propeller extends Airplane { + isTurboprop: Boolean +} diff --git a/pkl-config-java/src/test/resources/codegenPkl/PolymorphicModuleTest.pkl b/pkl-config-java/src/test/resources/codegenPkl/PolymorphicModuleTest.pkl new file mode 100644 index 00000000..17c45558 --- /dev/null +++ b/pkl-config-java/src/test/resources/codegenPkl/PolymorphicModuleTest.pkl @@ -0,0 +1,24 @@ +/// Gets generated into a Java via Gradle task `generateTestConfigClasses`. +module com.example.PolymorphicModuleTest + +import "PolymorphicLib.pkl" + +abstract class Dessert + +class Strudel extends Dessert { + numberOfRolls: Int +} + +class TurkishDelight extends Dessert { + isOfferedToEdmund: Boolean +} + +desserts: Listing = new { + new Strudel { numberOfRolls = 3 } + new TurkishDelight { isOfferedToEdmund = true } +} + +planes: Listing = new { + new PolymorphicLib.Jet { name = "Concorde"; numSeats = 128; isSuperSonic = true } + new PolymorphicLib.Propeller { name = "Cessna 172"; numSeats = 4; isTurboprop = true } +} diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PAnyToOptionalTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PAnyToOptionalTest.pkl new file mode 100644 index 00000000..ebca34b4 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PAnyToOptionalTest.pkl @@ -0,0 +1,4 @@ +ex1 = null +ex2 = "str" +ex3 = List(1, 2, 3) + diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PCollectionToArrayTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PCollectionToArrayTest.pkl new file mode 100644 index 00000000..48580701 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PCollectionToArrayTest.pkl @@ -0,0 +1,6 @@ +ex1 = List() +ex2 = List(1, 2, 3) +ex3 = Set(1, 2, 3) +ex4 = List(1, 2, 3.3) +ex5 = List(true, false, true) +ex6 = List(new Dynamic { name = "pigeon"; age = 40 }, new Dynamic { name = "parrot"; age = 30 }) diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PCollectionToCollectionTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PCollectionToCollectionTest.pkl new file mode 100644 index 00000000..48580701 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PCollectionToCollectionTest.pkl @@ -0,0 +1,6 @@ +ex1 = List() +ex2 = List(1, 2, 3) +ex3 = Set(1, 2, 3) +ex4 = List(1, 2, 3.3) +ex5 = List(true, false, true) +ex6 = List(new Dynamic { name = "pigeon"; age = 40 }, new Dynamic { name = "parrot"; age = 30 }) diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PMapToMapTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PMapToMapTest.pkl new file mode 100644 index 00000000..81435daa --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PMapToMapTest.pkl @@ -0,0 +1,7 @@ +ex1 = Map() +ex2 = Map(1, 2, 2, 4, 3, 6) +ex3 = Map(1, 2, 2, 4, 3, 6.6) +ex4 = Map("pigeon", Map("name", "pigeon", "age", 40), "parrot", Map("name", "parrot", "age", 30)) +ex5 = Map("pigeon", new Dynamic { name = "pigeon"; age = 40 }, "parrot", new Dynamic { name = "parrot"; age = 30 }) +ex6 = Map(new Dynamic { name = "pigeon"; age = 40 }, "pigeon", new Dynamic { name = "parrot"; age = 30 }, "parrot") +ex7 = Map(1, 2, 2, 4) diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PModuleToDataObjectTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PModuleToDataObjectTest.pkl new file mode 100644 index 00000000..b16ec2fa --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PModuleToDataObjectTest.pkl @@ -0,0 +1,7 @@ +name = "pigeon" +age = 40 +hobbies = List("swimming", "surfing") +address { + street = "sesame street" + zip = 94105 +} diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToDataObjectPolymorphismTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToDataObjectPolymorphismTest.pkl new file mode 100644 index 00000000..5cf6aee5 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToDataObjectPolymorphismTest.pkl @@ -0,0 +1,14 @@ +module org.pkl.test.PObjectToDataObject + +abstract class Thing { + isThing: Boolean +} + +class Person extends Thing { + name: String +} + +thing: Thing = new Person { + name = "Bob" + isThing = true +} diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToDataObjectTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToDataObjectTest.pkl new file mode 100644 index 00000000..957c0d11 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToDataObjectTest.pkl @@ -0,0 +1,45 @@ +ex1 { + name = "pigeon" + age = 40 + hobbies = List("swimming", "surfing") + address { + street = "sesame street" + zip = 94105 + } +} + +ex2 = new Person { + name = "pigeon" + age = 40 + hobbies = List("swimming", "surfing") + address { + street = "sesame street" + zip = 94105 + } +} + +ex3 { + first = "foo" + second = 42 +} + +ex4 { + street = "sesame street" + zipp = 94105 // intentional typo +} + +ex5 { + numbers = List(1, 2, 3) +} + +class Person { + name: String + age: Int + hobbies: List + address: Address +} + +class Address { + street: String + zip: Int +} diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToPObjectTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToPObjectTest.pkl new file mode 100644 index 00000000..ec0c1126 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PObjectToPObjectTest.pkl @@ -0,0 +1,2 @@ +ex1 = new { name = "pigeon"; age = 40 } +ex2 = List(new Dynamic { name = "pigeon"; age = 40 }, new Dynamic { name = "parrot"; age = 30 }) diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PPairToPairTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PPairToPairTest.pkl new file mode 100644 index 00000000..3dd6c3e6 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PPairToPairTest.pkl @@ -0,0 +1,7 @@ +class Person { + name: String + age: Int +} + +ex1 = Pair(1, 3.s) +ex2 = Pair(new Person { name = "pigeon"; age = 40 }, new Dynamic { name = "parrot"; age = 30 }) diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PStringToEnumTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PStringToEnumTest.pkl new file mode 100644 index 00000000..680dadf3 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PStringToEnumTest.pkl @@ -0,0 +1,12 @@ +ex1 = "couch surfing" +ex2 = "couch_surfing" +ex3 = "COUCH SURFING" +ex4 = "COUCH_SURFING" +ex5 = "couchSurfing" +ex6 = "couch Surfing" +ex7 = List("swimming", "reading", "couch surfing") +ex8 = List("couch surfing", "COUCH_SURFING") +ex9 = List("couch surfing", "other") +ex10 = "couchSurfing" +ex11 = "min" +ex12 = "gb" diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PStringToVersionTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PStringToVersionTest.pkl new file mode 100644 index 00000000..ff0ea6d6 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PStringToVersionTest.pkl @@ -0,0 +1,8 @@ +import "pkl:semver" + +ex1 = "1.2.3" +ex2 = "1.2.3-rc.1" +ex3 = "1.2.3+456.789" +ex4 = "1.2.3-rc.1+456.789" +ex5 = "999999999999999.0.0" +ex6 = "not a version number" diff --git a/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PVersionToVersionTest.pkl b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PVersionToVersionTest.pkl new file mode 100644 index 00000000..b41e5315 --- /dev/null +++ b/pkl-config-java/src/test/resources/org/pkl/config/java/mapper/PVersionToVersionTest.pkl @@ -0,0 +1,7 @@ +import "pkl:semver" + +ex1 = semver.Version("1.2.3") +ex2 = semver.Version("1.2.3-rc.1") +ex3 = semver.Version("1.2.3+456.789") +ex4 = semver.Version("1.2.3-rc.1+456.789") +ex5 = semver.Version("999999999999999.0.0") diff --git a/pkl-config-kotlin/gradle.lockfile b/pkl-config-kotlin/gradle.lockfile new file mode 100644 index 00000000..681ecdbb --- /dev/null +++ b/pkl-config-kotlin/gradle.lockfile @@ -0,0 +1,39 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.github.ajalt.clikt:clikt-jvm:3.5.1=pklCodegenKotlin +com.github.ajalt.clikt:clikt:3.5.1=pklCodegenKotlin +com.squareup:kotlinpoet:1.6.0=pklCodegenKotlin +com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.14=default,pklConfigJava,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:22.3.1=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,pklCodegenKotlin,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.5=default,pklCodegenKotlin,pklConfigJava,runtimeClasspath,testRuntimeClasspath +empty=annotationProcessor,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklConfigJavaAll,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-config-kotlin/pkl-config-kotlin.gradle.kts b/pkl-config-kotlin/pkl-config-kotlin.gradle.kts new file mode 100644 index 00000000..6ee31ca2 --- /dev/null +++ b/pkl-config-kotlin/pkl-config-kotlin.gradle.kts @@ -0,0 +1,88 @@ +plugins { + pklAllProjects + pklKotlinLibrary + pklPublishLibrary +} + +val pklConfigJava: Configuration by configurations.creating + +val pklConfigJavaAll: Configuration by configurations.creating + +val pklCodegenKotlin: Configuration by configurations.creating + +// Ideally, api would extend pklConfigJavaAll, +// instead of extending pklConfigJava and then patching test task and POM. +// However, this wouldn't work for IntelliJ. +configurations.api.get().extendsFrom(pklConfigJava) + +dependencies { + pklConfigJava(project(":pkl-config-java")) + + pklConfigJavaAll(project(":pkl-config-java", "fatJar")) + + pklCodegenKotlin(project(":pkl-codegen-kotlin")) + + implementation(libs.kotlinReflect) + + testImplementation(libs.geantyref) +} + +val generateTestConfigClasses by tasks.registering(JavaExec::class) { + outputs.dir("build/testConfigClasses") + inputs.dir("src/test/resources/codegenPkl") + + classpath = pklCodegenKotlin + mainClass.set("org.pkl.codegen.kotlin.Main") + args("--output-dir", "build/testConfigClasses") + args(fileTree("src/test/resources/codegenPkl")) +} + +sourceSets.getByName("test") { + java.srcDir("build/testConfigClasses/kotlin") + resources.srcDir("build/testConfigClasses/resources") +} + +tasks.processTestResources { + dependsOn(generateTestConfigClasses) +} + +tasks.compileTestKotlin { + dependsOn(generateTestConfigClasses) +} + +// use pkl-config-java-all for testing (same as for publishing) +tasks.test { + classpath = classpath - pklConfigJava + pklConfigJavaAll +} + +// disable publishing of .module until we find a way to manipulate it like POM (or ideally both together) +tasks.withType { + enabled = false +} + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-config-kotlin") + description.set("Kotlin extensions for pkl-config-java, a Java config library based on the Pkl config language.") + + // change dependency pkl-config-java to pkl-config-java-all + withXml { + val projectElement = asElement() + val dependenciesElement = projectElement.getElementsByTagName("dependencies").item(0) as org.w3c.dom.Element + val dependencyElements = dependenciesElement.getElementsByTagName("dependency") + for (idx in 0 until dependencyElements.length) { + val dependencyElement = dependencyElements.item(idx) as org.w3c.dom.Element + val artifactIdElement = dependencyElement.getElementsByTagName("artifactId").item(0) as org.w3c.dom.Element + if (artifactIdElement.textContent == "pkl-config-java") { + artifactIdElement.textContent = "pkl-config-java-all" + return@withXml + } + } + throw GradleException("Failed to edit POM of module `pkl-config-kotlin`.") + } + } + } + } +} diff --git a/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/ConfigExtensions.kt b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/ConfigExtensions.kt new file mode 100644 index 00000000..1a2c14ca --- /dev/null +++ b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/ConfigExtensions.kt @@ -0,0 +1,65 @@ +/** + * 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.config.kotlin + +import org.pkl.config.java.Config +import org.pkl.config.java.ConfigEvaluator +import org.pkl.config.java.ConfigEvaluatorBuilder +import org.pkl.config.java.JavaType +import org.pkl.config.java.mapper.ConversionException +import org.pkl.config.java.mapper.ValueMapperBuilder +import org.pkl.config.kotlin.mapper.KotlinConversions +import org.pkl.config.kotlin.mapper.KotlinConverterFactories + +/** + * Converts this [Config] node to type [T] using the configured + * [org.pkl.config.java.mapper.ValueMapper]. + * + * To allow `null` values, specify a nullable type, for example `to()`. + * + * Kotlin code should prefer this method over [Config. as] for the following reasons: + * * does not clash with Kotlin's `as` keyword + * * throws [ConversionException] if conversion to non-nullable type returns `null` + * * easier to use with parameterized types: `to>()` vs. + * `as(JavaType.listOf(String::class.java))` + */ +inline fun Config.to(): T { + val javaType = object : JavaType() {} + val result = `as`(javaType.type) + if (result == null && null !is T) { + throw ConversionException( + "Expected a non-null value but got `null`. " + + "To allow null values, convert to a nullable Kotlin type, for example `String?`." + ) + } + return result +} + +/** + * Configures this [ValueMapperBuilder] with conversions and converter factories for Kotlin types. + */ +fun ValueMapperBuilder.forKotlin(): ValueMapperBuilder = + addConversions(KotlinConversions.all).addConverterFactories(KotlinConverterFactories.all) + +/** + * Configures this [ConfigEvaluatorBuilder] with conversions and converter factories for Kotlin + * types. + */ +fun ConfigEvaluatorBuilder.forKotlin(): ConfigEvaluatorBuilder = + setValueMapperBuilder(valueMapperBuilder.forKotlin()) + +fun ConfigEvaluator.forKotlin(): ConfigEvaluator = + setValueMapper(valueMapper.toBuilder().forKotlin().build()) diff --git a/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/KotlinConversions.kt b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/KotlinConversions.kt new file mode 100644 index 00000000..20d09e9d --- /dev/null +++ b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/KotlinConversions.kt @@ -0,0 +1,90 @@ +/** + * 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.config.kotlin.mapper + +import java.util.* +import java.util.regex.Pattern +import org.pkl.config.java.mapper.Conversion +import org.pkl.config.java.mapper.ConversionException +import org.pkl.core.PClassInfo + +object KotlinConversions { + val pIntToULong: Conversion = + Conversion.of(PClassInfo.Int, ULong::class.java) { value, _ -> + if (value < 0) { + throw ConversionException( + "Cannot convert pkl.base#Int `$value` to kotlin.ULong " + + // use Long.MAX_VALUE instead of ULong.MAX_VALUE + "because it is outside range `0...${Long.MAX_VALUE}`" + ) + } + value.toULong() + } + + val pIntToUInt: Conversion = + Conversion.of(PClassInfo.Int, UInt::class.java) { value, _ -> + val max = 0xFFFFFFFF // use literal instead of `UInt.MAX_VALUE.toLong()` + if (value < 0 || value > max) { + throw ConversionException( + "Cannot convert pkl.base#Int `$value` to kotlin.UInt " + + "because it is outside range `0...$max`" + ) + } + value.toUInt() + } + + val pIntToUShort: Conversion = + Conversion.of(PClassInfo.Int, UShort::class.java) { value, _ -> + val max = 0xFFFF // use literal instead of `UShort.MAX_VALUE.toLong()` + if (value < 0 || value > max) { + throw ConversionException( + "Cannot convert pkl.base#Int `$value` to kotlin.UShort " + + "because it is outside range `0...$max`" + ) + } + value.toUShort() + } + + val pIntToUByte: Conversion = + Conversion.of(PClassInfo.Int, UByte::class.java) { value, _ -> + val max = 0xFF // use literal instead of `UByte.MAX_VALUE.toLong()` + if (value < 0 || value > max) { + throw ConversionException( + "Cannot convert pkl.base#Int `$value` to kotlin.UByte " + + "because it is outside range `0...$max`" + ) + } + value.toUByte() + } + + val pStringToKotlinRegex: Conversion = + Conversion.of(PClassInfo.String, Regex::class.java) { value, _ -> Regex(value) } + + val pRegexToKotlinRegex: Conversion = + Conversion.of(PClassInfo.Regex, Regex::class.java) { value, _ -> value.toRegex() } + + val all: Collection> = + Collections.unmodifiableList( + listOf( + pIntToULong, + pIntToUInt, + pIntToUShort, + pIntToUByte, + pStringToKotlinRegex, + pRegexToKotlinRegex + ) + ) +} diff --git a/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/KotlinConverterFactories.kt b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/KotlinConverterFactories.kt new file mode 100644 index 00000000..d99864c3 --- /dev/null +++ b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/KotlinConverterFactories.kt @@ -0,0 +1,51 @@ +/** + * 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.config.kotlin.mapper + +import java.lang.reflect.Constructor +import java.util.* +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.javaConstructor +import kotlin.reflect.jvm.kotlinFunction +import org.pkl.config.java.mapper.ConverterFactories +import org.pkl.config.java.mapper.ConverterFactory +import org.pkl.config.java.mapper.Named +import org.pkl.config.java.mapper.PObjectToDataObject + +/** [ConverterFactory]s for use with Kotlin. */ +object KotlinConverterFactories { + /** + * Variation of [ConverterFactories.pObjectToDataObject] for Kotlin objects. Uses the primary + * constructor of the Kotlin target class. Constructor parameters do *not* need to be annotated + * with [Named]. Supports both regular Kotlin classes and Kotlin data classes. + */ + val pObjectToDataObject: ConverterFactory = + object : PObjectToDataObject() { + override fun selectConstructor(clazz: Class<*>): Optional> = + Optional.ofNullable(clazz.kotlin.primaryConstructor?.javaConstructor) + + override fun getParameterNames(constructor: Constructor<*>): Optional> { + val params = constructor.kotlinFunction?.parameters + val paramNames = params?.mapNotNull { it.name } + return Optional.ofNullable(paramNames?.takeIf { it.size == params.size }) + } + } + + val pPairToKotlinPair: ConverterFactory = PPairToKotlinPair() + + val all: Collection = + Collections.unmodifiableList(listOf(pObjectToDataObject, pPairToKotlinPair)) +} diff --git a/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/PPairToKotlinPair.kt b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/PPairToKotlinPair.kt new file mode 100644 index 00000000..2f224788 --- /dev/null +++ b/pkl-config-kotlin/src/main/kotlin/org/pkl/config/kotlin/mapper/PPairToKotlinPair.kt @@ -0,0 +1,75 @@ +/** + * 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.config.kotlin.mapper + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.util.* +import kotlin.Any +import kotlin.Pair +import org.pkl.config.java.mapper.Converter +import org.pkl.config.java.mapper.ConverterFactory +import org.pkl.config.java.mapper.Reflection +import org.pkl.config.java.mapper.ValueMapper +import org.pkl.core.PClassInfo +import org.pkl.core.Pair as PPair + +internal class PPairToKotlinPair : ConverterFactory { + override fun create(sourceType: PClassInfo<*>, targetType: Type): Optional> { + if (sourceType !== PClassInfo.Pair) return Optional.empty() + + val targetClass = Reflection.toRawType(targetType) + if (!Pair::class.java.isAssignableFrom(targetClass)) { + return Optional.empty() + } + + val pairType = Reflection.getExactSupertype(targetType, Pair::class.java) as ParameterizedType + return Optional.of( + ConverterImpl(pairType.actualTypeArguments[0], pairType.actualTypeArguments[1]) + ) + } + + private class ConverterImpl( + private val firstTargetType: Type, + private val secondTargetType: Type + ) : Converter, Pair> { + + private var firstCachedType = PClassInfo.Unavailable + private var firstCachedConverter: Converter? = null + + private var secondCachedType = PClassInfo.Unavailable + private var secondCachedConverter: Converter? = null + + override fun convert(value: PPair, valueMapper: ValueMapper): Pair { + val first = value.first + if (!firstCachedType.isExactClassOf(first)) { + firstCachedType = PClassInfo.forValue(first) + firstCachedConverter = valueMapper.getConverter(firstCachedType, firstTargetType) + } + + val second = value.second + if (!secondCachedType.isExactClassOf(second)) { + secondCachedType = PClassInfo.forValue(second) + secondCachedConverter = valueMapper.getConverter(secondCachedType, secondTargetType) + } + + return Pair( + firstCachedConverter!!.convert(first, valueMapper), + secondCachedConverter!!.convert(second, valueMapper) + ) + } + } +} diff --git a/pkl-config-kotlin/src/test/java/org/pkl/config/kotlin/JavaPerson.java b/pkl-config-kotlin/src/test/java/org/pkl/config/kotlin/JavaPerson.java new file mode 100644 index 00000000..f24a453f --- /dev/null +++ b/pkl-config-kotlin/src/test/java/org/pkl/config/kotlin/JavaPerson.java @@ -0,0 +1,56 @@ +/** + * 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.config.kotlin; + +import java.util.List; +import org.pkl.config.java.mapper.Named; + +public class JavaPerson { + private final String name; + private final int age; + private final List hobbies; + + public JavaPerson(@Named("name") String name) { + this.name = name; + age = 0; + hobbies = List.of(); + } + + public JavaPerson( + @Named("name") String name, @Named("age") int age, @Named("hobbies") List hobbies) { + this.name = name; + this.age = age; + this.hobbies = hobbies; + } + + public JavaPerson(@Named("age") int age) { + this.age = age; + name = "Default"; + hobbies = List.of(); + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public List getHobbies() { + return hobbies; + } +} diff --git a/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/ConfigExtensionsTest.kt b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/ConfigExtensionsTest.kt new file mode 100644 index 00000000..403b9e79 --- /dev/null +++ b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/ConfigExtensionsTest.kt @@ -0,0 +1,257 @@ +/** + * 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.config.kotlin + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pkl.config.java.ConfigEvaluator +import org.pkl.config.java.ConfigEvaluatorBuilder +import org.pkl.config.java.mapper.ConversionException +import org.pkl.config.kotlin.ConfigExtensionsTest.Hobby.READING +import org.pkl.config.kotlin.ConfigExtensionsTest.Hobby.SWIMMING +import org.pkl.core.ModuleSource.text + +class ConfigExtensionsTest { + private val evaluator = ConfigEvaluator.preconfigured().forKotlin() + + @Test + fun `convert to kotlin classes`() { + val config = + evaluator.evaluate( + text( + """ + pigeon { + name = "pigeon" + age = 30 + hobbies = List("swimming", "reading") + address { + street = "Fuzzy St." + } + } + """ + ) + ) + + val address = config["pigeon"]["address"].to>() + assertThat(address.street).isEqualTo("Fuzzy St.") + + val pigeon = config["pigeon"].to>() + assertThat(pigeon).isNotNull + assertThat(pigeon.name).isEqualTo("pigeon") + assertThat(pigeon.age).isEqualTo(30) + assertThat(pigeon.hobbies).isEqualTo(setOf(READING, SWIMMING)) + assertThat(pigeon.address.street).isEqualTo("Fuzzy St.") + } + + @Test + fun `convert to kotlin class with nullable property`() { + // cover ConfigEvaluatorBuilder.preconfigured() + val evaluator = ConfigEvaluatorBuilder.preconfigured().forKotlin().build() + + val config = evaluator.evaluate(text("pigeon { address = null }")) + + val pigeon = config["pigeon"].to() + assertThat(pigeon.address).isNull() + } + + @Test + fun `convert to kotlin class with covariant collection property type`() { + val config = + evaluator.evaluate( + text( + """pigeon { addresses = List(new Dynamic { street = "Fuzzy St." }, new Dynamic { street = "Other St." }) }""" + ) + ) + + config["pigeon"].to() + } + + @Test + fun `convert to nullable type`() { + val config = + evaluator.evaluate(text("""pigeon { address1 { street = "Fuzzy St." }; address2 = null }""")) + + val address1 = config["pigeon"]["address1"].to?>() + assertThat(address1).isEqualTo(Address(street = "Fuzzy St.")) + + val address2 = config["pigeon"]["address2"].to?>() + assertThat(address2).isNull() + + val e = assertThrows { config["pigeon"]["address2"].to>() } + assertThat(e) + .hasMessage( + "Expected a non-null value but got `null`. " + + "To allow null values, convert to a nullable Kotlin type, for example `String?`." + ) + } + + @Test + fun `convert to kotlin class that has defaults for constructor args`() { + val config = + evaluator.evaluate( + text( + """ + pigeon { + name = "Pigeon" + age = 42 + hobbies = List() + } + """ + ) + ) + + val pigeon = config["pigeon"].to() + assertThat(pigeon.name).isEqualTo("Pigeon") + assertThat(pigeon.age).isEqualTo(42) + assertThat(pigeon.hobbies).isEqualTo(listOf()) + } + + // check that java converter factory still kicks in + @Test + fun `convert to java class with multiple constructors`() { + val config = + evaluator.evaluate( + text( + """ + pigeon { + name = "Pigeon" + age = 42 + hobbies = List() + } + """ + ) + ) + + val pigeon = config["pigeon"].to() + assertThat(pigeon.name).isEqualTo("Pigeon") + assertThat(pigeon.age).isEqualTo(42) + assertThat(pigeon.hobbies).isEqualTo(listOf()) + } + + @Test + fun `convert list to parameterized list`() { + val config = + evaluator.evaluate( + text( + """friends = List(new Dynamic { name = "lilly"}, new Dynamic {name = "bob"}, new Dynamic {name = "susan"})""" + ) + ) + + val friends = config["friends"].to>() + assertThat(friends) + .isEqualTo(listOf(SimplePerson("lilly"), SimplePerson("bob"), SimplePerson("susan"))) + } + + @Test + fun `convert map to parameterized map`() { + val config = + evaluator.evaluate( + text( + """friends = Map("l", new Dynamic { name = "lilly"}, "b", new Dynamic { name = "bob"}, "s", new Dynamic { name = "susan"})""" + ) + ) + + val friends = config["friends"].to>() + assertThat(friends) + .isEqualTo( + mapOf( + "l" to SimplePerson("lilly"), + "b" to SimplePerson("bob"), + "s" to SimplePerson("susan") + ) + ) + } + + @Test + fun `convert container to parameterized map`() { + val config = + evaluator.evaluate( + text("""friends {l { name = "lilly"}; b { name = "bob"}; s { name = "susan"}}""") + ) + + val friends = config["friends"].to>() + assertThat(friends) + .isEqualTo( + mapOf( + "l" to SimplePerson("lilly"), + "b" to SimplePerson("bob"), + "s" to SimplePerson("susan") + ) + ) + } + + @Test + fun `convert enum with mangled names`() { + val values = MangledNameEnum.values().map { "\"$it\"" } + val config = + evaluator.evaluate( + text( + """ + typealias MangledNameEnum = ${values.joinToString(" | ")} + allEnumValues: Set = Set(${values.joinToString(", ")}) + """ + .trimIndent() + ) + ) + val allEnumValues = config["allEnumValues"].to>() + assertThat(allEnumValues).isEqualTo(MangledNameEnum.values().toSet()) + } + + data class SimplePerson(val name: String) + + class Person(val name: String, val age: Int, val hobbies: Set, val address: Address) + + enum class Hobby { + SWIMMING, + @Suppress("unused") SURFING, + READING + } + + data class Address(val street: T) + + class Person2(val address: Address?) + + class Person3(@Suppress("unused") val addresses: List) + + open class OpenAddress(val street: String) { + override fun equals(other: Any?): Boolean { + return other is OpenAddress && street == other.street + } + + override fun hashCode(): Int { + return street.hashCode() + } + } + + class PersonWithDefaults( + val name: String = "Pigeon", + val age: Int = 42, + val hobbies: List + ) + + @Suppress("NonAsciiCharacters", "EnumEntryName") + enum class MangledNameEnum(val value: String) { + FROM_CAMEL_CASE("fromCamelCase"), + HYPHENATED_NAME("hyphenated-name"), + EN_QUAD_EM_SPACE_IDEOGRAPHIC_SPACE_("EnQuad\\u2000EmSpace\\u2003IdeographicSpace\\u3000"), + ᾊ_ᾨ("ᾊ\u0ABFᾨ"), + _42_FROM_INVALID_START("42-from-invalid-start"), + __EMOJI__("❎Emoji✅✅"), + ÀŒÜ("àœü"), + 日本_つくば("日本-つくば") + } +} diff --git a/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/KotlinObjectMappingTest.kt b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/KotlinObjectMappingTest.kt new file mode 100644 index 00000000..f066d40e --- /dev/null +++ b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/KotlinObjectMappingTest.kt @@ -0,0 +1,122 @@ +/** + * 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.config.kotlin + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.pkl.config.java.ConfigEvaluator +import org.pkl.core.ModuleSource.text + +class KotlinObjectMappingTest { + data class TypedKey(val value: Int) + + data class KotlinGenericTypesTest( + // Sets + val stringSet: Set, + val intSet: Set, + val booleanSetSet: Set>, + + // Lists + val stringList: List, + val intList: List, + val booleanListList: List>, + + // Maps + val intStringMap: Map, + val booleanIntStringMapMap: Map>, + val booleanIntMapStringMap: Map, String>, + val intSetListStringMap: Map>, String>, + val typedStringMap: Map, + val dynamicStringMap: Map, String>, + + // Listings + val stringSetListing: List>, + val intListingListing: List>, + + // Mapping + val intStringMapping: Map, + val stringStringSetMapping: Map>, + + // Map & Mapping with structured keys + val intListingStringMapping: Map, String>, + val intSetListStringMapping: Map>, String>, + val thisOneGoesToEleven: Map>, Map, Map>> + ) + + @Test + fun `generic types correspond`() { + val code = + """ + module KotlinGenericTypesTest + + class Foo { + value: Int + } + + // Sets + stringSet: Set = Set("in set") + intSet: Set = Set(1,2,4,8,16,32) + booleanSetSet: Set> = Set(Set(false), Set(true), Set(true, false)) + + // Lists + stringList: List = List("in list") + intList: List = List(1,2,3,5,7,11) + booleanListList: List> = List(List(false), List(true), List(true, false)) + + // Maps + intStringMap: Map = Map(0, "in map") + booleanIntStringMapMap: Map> = Map(false, Map(0, "in map in map")) + booleanIntMapStringMap: Map, String> = Map(Map(true, 42), "in map with map keys") + + // Listings + stringSetListing: Listing> = new { Set("in set in listing") } + intListingListing: Listing> = new { new { 1337 } new { 100 } } + + // Mappings + intStringMapping: Mapping = new { [42] = "in map" } + stringStringSetMapping: Mapping> = new { ["key"] = Set("in set in map") } + + // Map & Mappings with structured keys + intSetListStringMap: Map>, String> = Map(List(Set(27)), "in map with structured key") + typedStringMap: Map = Map( + new Foo { value = 1 }, "using typed objects", + new Foo { value = 2 }, "also works") + dynamicStringMap: Map = Map( + new Dynamic { value = 42 }, "using Dynamics", + new Dynamic { hello = "world" }, "also works") + + intListingStringMapping: Mapping, String> = new { + [new Listing { 42 1337 }] = "structured key works" + } + intSetListStringMapping: Mapping>, String> = new { + [List(Set(27))] = "in mapping with structured key" + } + local intListing: Listing = new { 0 0 7 } + thisOneGoesToEleven: Mapping>, Map, Mapping>> = new { + [List(Set(0), Set(0), Set(7))] = Map(intListing, intStringMapping) + } + """ + .trimIndent() + val result = ConfigEvaluator.preconfigured().forKotlin().evaluate(text(code)) + assertDoesNotThrow { result.to() } + .apply { + assertThat(typedStringMap.keys).isEqualTo(setOf(TypedKey(1), TypedKey(2))) + assertThat(dynamicStringMap.keys) + .isEqualTo(setOf(hashMapOf("hello" to "world"), hashMapOf("value" to 42.toLong()))) + } + } +} diff --git a/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/KotlinConversionsTest.kt b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/KotlinConversionsTest.kt new file mode 100644 index 00000000..17c6b05d --- /dev/null +++ b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/KotlinConversionsTest.kt @@ -0,0 +1,84 @@ +/** + * 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.config.kotlin.mapper + +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.config.java.mapper.ConversionException +import org.pkl.config.java.mapper.ValueMapperBuilder + +class KotlinConversionsTest { + private val mapper = ValueMapperBuilder.unconfigured().build() + + @Test + fun pStringToKotlinRegex() { + val result = KotlinConversions.pStringToKotlinRegex.converter.convert("(?i)\\w*", mapper) + assertThat(result.pattern).isEqualTo("(?i)\\w*") + assertThat(result.options).isEqualTo(setOf(RegexOption.IGNORE_CASE)) + } + + @Test + fun pRegexToKotlinRegex() { + val result = + KotlinConversions.pRegexToKotlinRegex.converter.convert(Pattern.compile("(?i)\\w*"), mapper) + assertThat(result.pattern).isEqualTo("(?i)\\w*") + assertThat(result.options).isEqualTo(setOf(RegexOption.IGNORE_CASE)) + } + + @Test + fun pIntToULong() { + assertThat(KotlinConversions.pIntToULong.converter.convert(0, mapper)).isEqualTo(0UL) + + assertThat(KotlinConversions.pIntToULong.converter.convert(Long.MAX_VALUE, mapper)) + .isEqualTo(Long.MAX_VALUE.toULong()) + + assertThrows { + KotlinConversions.pIntToULong.converter.convert(-1, mapper) + } + } + + @Test + fun pIntToUInt() { + assertThat(KotlinConversions.pIntToUInt.converter.convert(0, mapper)).isEqualTo(0u) + + assertThat(KotlinConversions.pIntToUInt.converter.convert(UInt.MAX_VALUE.toLong(), mapper)) + .isEqualTo(UInt.MAX_VALUE) + + assertThrows { + KotlinConversions.pIntToUInt.converter.convert(UInt.MAX_VALUE.toLong() + 1, mapper) + } + + assertThrows { KotlinConversions.pIntToUInt.converter.convert(-1, mapper) } + } + + @Test + fun pIntToUShort() { + assertThat(KotlinConversions.pIntToUShort.converter.convert(0, mapper)).isEqualTo(0.toUShort()) + + assertThat(KotlinConversions.pIntToUShort.converter.convert(UShort.MAX_VALUE.toLong(), mapper)) + .isEqualTo(UShort.MAX_VALUE) + + assertThrows { + KotlinConversions.pIntToUShort.converter.convert(UShort.MAX_VALUE.toLong() + 1, mapper) + } + + assertThrows { + KotlinConversions.pIntToUShort.converter.convert(-1, mapper) + } + } +} diff --git a/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/OverriddenPropertyTest.kt b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/OverriddenPropertyTest.kt new file mode 100644 index 00000000..c45ec8bd --- /dev/null +++ b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/OverriddenPropertyTest.kt @@ -0,0 +1,36 @@ +/** + * 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.config.kotlin.mapper + +import com.example.OverriddenProperty +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.pkl.config.java.ConfigEvaluatorBuilder +import org.pkl.config.kotlin.forKotlin +import org.pkl.config.kotlin.to +import org.pkl.core.ModuleSource + +class OverriddenPropertyTest { + @Test + fun `overridden property`() { + ConfigEvaluatorBuilder.preconfigured().forKotlin().build().use { evaluator -> + val config = evaluator.evaluate(ModuleSource.modulePath("/codegenPkl/OverriddenProperty.pkl")) + val module = config.to() + assertThat(module.theClass.bar[0].prop1).isEqualTo("hello") + assertThat(module.theClass.bar[0].prop2).isEqualTo("hello again") + } + } +} diff --git a/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/PPairToKotlinPairTest.kt b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/PPairToKotlinPairTest.kt new file mode 100644 index 00000000..58474dba --- /dev/null +++ b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/PPairToKotlinPairTest.kt @@ -0,0 +1,70 @@ +/** + * 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.config.kotlin.mapper + +import kotlin.Pair +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.entry +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Test +import org.pkl.config.java.mapper.Types +import org.pkl.config.java.mapper.ValueMapperBuilder +import org.pkl.config.kotlin.forKotlin +import org.pkl.core.* +import org.pkl.core.ModuleSource.modulePath + +class PPairToKotlinPairTest { + companion object { + private val evaluator = Evaluator.preconfigured() + + private val module = + evaluator.evaluate(modulePath("org/pkl/config/kotlin/mapper/PPairToKotlinPairTest.pkl")) + + private val mapper = ValueMapperBuilder.preconfigured().forKotlin().build() + + @AfterAll + @Suppress("unused") + @JvmStatic + fun afterAll() { + evaluator.close() + } + } + + @Test + fun ex1() { + val ex1 = module.getProperty("ex1") + val mapped: Pair = + mapper.map( + ex1, + Types.parameterizedType(Pair::class.java, Integer::class.java, Duration::class.java) + ) + assertThat(mapped).isEqualTo(Pair(1, Duration(3.0, DurationUnit.SECONDS))) + } + + @Test + fun ex2() { + val ex2 = module.getProperty("ex2") + val mapped: Pair = + mapper.map( + ex2, + Types.parameterizedType(Pair::class.java, PObject::class.java, PObject::class.java) + ) + + assertThat(mapped.first.properties).containsOnly(entry("name", "pigeon"), entry("age", 40L)) + + assertThat(mapped.second.properties).containsOnly(entry("name", "parrot"), entry("age", 30L)) + } +} diff --git a/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/PolymorphicTest.kt b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/PolymorphicTest.kt new file mode 100644 index 00000000..b0c8cfb7 --- /dev/null +++ b/pkl-config-kotlin/src/test/kotlin/org/pkl/config/kotlin/mapper/PolymorphicTest.kt @@ -0,0 +1,38 @@ +/** + * 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.config.kotlin.mapper + +import com.example.PolymorphicLib +import com.example.PolymorphicModuleTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.pkl.config.java.ConfigEvaluatorBuilder +import org.pkl.config.kotlin.forKotlin +import org.pkl.config.kotlin.to +import org.pkl.core.ModuleSource.modulePath + +class PolymorphicTest { + @Test + fun `deserializing polymorphic objects`() { + val evaluator = ConfigEvaluatorBuilder.preconfigured().forKotlin().build() + val config = evaluator.evaluate(modulePath("/codegenPkl/PolymorphicModuleTest.pkl")) + val module = config.to() + assertThat(module.desserts[0]).isInstanceOf(PolymorphicModuleTest.Strudel::class.java) + assertThat(module.desserts[1]).isInstanceOf(PolymorphicModuleTest.TurkishDelight::class.java) + assertThat(module.planes[0]).isInstanceOf(PolymorphicLib.Jet::class.java) + assertThat(module.planes[1]).isInstanceOf(PolymorphicLib.Propeller::class.java) + } +} diff --git a/pkl-config-kotlin/src/test/resources/codegenPkl/OverriddenProperty.pkl b/pkl-config-kotlin/src/test/resources/codegenPkl/OverriddenProperty.pkl new file mode 100644 index 00000000..c78a5d0a --- /dev/null +++ b/pkl-config-kotlin/src/test/resources/codegenPkl/OverriddenProperty.pkl @@ -0,0 +1,28 @@ +module com.example.OverriddenProperty + +abstract class BaseClass { + fixed bar: Listing = new { + new { + prop1 = "hello" + } + } +} + +theClass: TheClass + +class TheClass extends BaseClass { + fixed bar: Listing = new { + new { + prop1 = "hello" + prop2 = "hello again" + } + } +} + +open class BaseBar { + prop1: String +} + +class Bar extends BaseBar { + prop2: String +} diff --git a/pkl-config-kotlin/src/test/resources/codegenPkl/PolymorphicLib.pkl b/pkl-config-kotlin/src/test/resources/codegenPkl/PolymorphicLib.pkl new file mode 100644 index 00000000..e760c453 --- /dev/null +++ b/pkl-config-kotlin/src/test/resources/codegenPkl/PolymorphicLib.pkl @@ -0,0 +1,14 @@ +module com.example.PolymorphicLib + +open class Airplane { + name: String + numSeats: Int +} + +class Jet extends Airplane { + isSuperSonic: Boolean +} + +class Propeller extends Airplane { + isTurboprop: Boolean +} diff --git a/pkl-config-kotlin/src/test/resources/codegenPkl/PolymorphicModuleTest.pkl b/pkl-config-kotlin/src/test/resources/codegenPkl/PolymorphicModuleTest.pkl new file mode 100644 index 00000000..b3433877 --- /dev/null +++ b/pkl-config-kotlin/src/test/resources/codegenPkl/PolymorphicModuleTest.pkl @@ -0,0 +1,24 @@ +/// Gets generated into a Kotlin via Gradle task `generateTestConfigClasses`. +module com.example.PolymorphicModuleTest + +import "PolymorphicLib.pkl" + +abstract class Dessert + +class Strudel extends Dessert { + numberOfRolls: Int +} + +class TurkishDelight extends Dessert { + isOfferedToEdmund: Boolean +} + +desserts: Listing = new { + new Strudel { numberOfRolls = 3 } + new TurkishDelight { isOfferedToEdmund = true } +} + +planes: Listing = new { + new PolymorphicLib.Jet { name = "Concorde"; numSeats = 128; isSuperSonic = true } + new PolymorphicLib.Propeller { name = "Cessna 172"; numSeats = 4; isTurboprop = true } +} diff --git a/pkl-config-kotlin/src/test/resources/org/pkl/config/kotlin/mapper/PPairToKotlinPairTest.pkl b/pkl-config-kotlin/src/test/resources/org/pkl/config/kotlin/mapper/PPairToKotlinPairTest.pkl new file mode 100644 index 00000000..c79dfee9 --- /dev/null +++ b/pkl-config-kotlin/src/test/resources/org/pkl/config/kotlin/mapper/PPairToKotlinPairTest.pkl @@ -0,0 +1,7 @@ +class Person { + name: String + age: Int +} + +ex1 = Pair(1, 3.s) +ex2 = Pair(new Person {name = "pigeon"; age = 40}, new Dynamic {name = "parrot"; age = 30}) diff --git a/pkl-core/README.adoc b/pkl-core/README.adoc new file mode 100644 index 00000000..a86ab0ca --- /dev/null +++ b/pkl-core/README.adoc @@ -0,0 +1,3 @@ +Core implementation of the Pkl language. Includes Java APIs for embedding the +language into JVM applications, and for building libraries and tools on top of +the language. diff --git a/pkl-core/gradle.lockfile b/pkl-core/gradle.lockfile new file mode 100644 index 00000000..a4ad3ca8 --- /dev/null +++ b/pkl-core/gradle.lockfile @@ -0,0 +1,44 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,compileOnly,compileOnlyDependenciesMetadata +com.squareup:javapoet:1.13.0=generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath +com.tunnelvisionlabs:antlr4-annotations:4.9.0=antlr +com.tunnelvisionlabs:antlr4-runtime:4.9.0=antlr,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.tunnelvisionlabs:antlr4:4.9.0=antlr +net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.abego.treelayout:org.abego.treelayout.core:1.0.1=antlr +org.antlr:ST4:4.3=antlr +org.antlr:antlr-runtime:3.5.2=antlr +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata +org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:22.3.1=compileClasspath,default,generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.truffle:truffle-api:22.3.1=compileClasspath,default,generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.truffle:truffle-dsl-processor:22.3.1=annotationProcessor +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.7.10=generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=generatorCompileClasspath,generatorImplementationDependenciesMetadata,generatorRuntimeClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathGenerator,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit.platform:junit-platform-engine:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata +org.organicdesign:Paguro:3.10.3=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.32=compileOnly +org.snakeyaml:snakeyaml-engine:2.5=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +empty=apiDependenciesMetadata,archives,compile,generatorAnnotationProcessor,generatorApiDependenciesMetadata,generatorCompileOnly,generatorCompileOnlyDependenciesMetadata,generatorIntransitiveDependenciesMetadata,generatorKotlinScriptDef,generatorKotlinScriptDefExtensions,generatorRuntimeOnlyDependenciesMetadata,intransitiveDependenciesMetadata,javaExecutable,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime diff --git a/pkl-core/pkl-core.gradle.kts b/pkl-core/pkl-core.gradle.kts new file mode 100644 index 00000000..7583e4c0 --- /dev/null +++ b/pkl-core/pkl-core.gradle.kts @@ -0,0 +1,292 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + kotlin("jvm") // for `src/generator/kotlin` + pklAllProjects + pklJavaLibrary + pklPublishLibrary + pklNativeBuild + antlr + idea +} + +val generatorSourceSet = sourceSets.register("generator") + +sourceSets { + main { + java { + srcDir(file("generated/antlr")) + } + } +} + +idea { + module { + // mark src/main/antlr as source dir, + // and generated/antlr as generated source dir + sourceDirs = sourceDirs + files("src/main/antlr", "generated/antlr") + generatedSourceDirs = generatedSourceDirs + files("generated/antlr") + } +} + +val javaExecutableConfiguration: Configuration = configurations.create("javaExecutable") + +// workaround for https://github.com/gradle/gradle/issues/820 +configurations.api.get().let { apiConfig -> + apiConfig.setExtendsFrom(apiConfig.extendsFrom.filter { it.name != "antlr" }) +} + +dependencies { + annotationProcessor(libs.truffleDslProcessor) + annotationProcessor(generatorSourceSet.get().runtimeClasspath) + + antlr(libs.antlr) + + compileOnly(libs.jsr305) + // pkl-core implements pkl-executor's ExecutorSpi, but the SPI doesn't ship with pkl-core + compileOnly(project(":pkl-executor")) + + implementation(libs.antlrRuntime) + implementation(libs.truffleApi) + implementation(libs.graalSdk) + + implementation(libs.paguro) { + exclude(group = "org.jetbrains", module = "annotations") + } + + implementation(libs.snakeYaml) + + testImplementation(project(":pkl-commons-test")) + + add("generatorImplementation", libs.javaPoet) + add("generatorImplementation", libs.truffleApi) + add("generatorImplementation", libs.kotlinStdLib) + + javaExecutableConfiguration(project(":pkl-cli", "javaExecutable")) +} + +publishing { + publications { + named("library") { + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-core") + description.set(""" + Core implementation of the Pkl configuration language. + Includes Java APIs for embedding the language into JVM applications, + and for building libraries and tools on top of the language. + """.trimIndent()) + } + } + } +} + +tasks.generateGrammarSource { + maxHeapSize = "64m" + + // generate only visitor + arguments = arguments + listOf("-visitor", "-no-listener") + + // Due to https://github.com/antlr/antlr4/issues/2260, + // we can't put .g4 files into src/main/antlr/org/pkl/core/parser/antlr. + // Instead, we put .g4 files into src/main/antlr, adapt output dir below, + // and use @header directives in .g4 files (instead of setting `-package` argument here) + // and task makeIntelliJAntlrPluginHappy to fix up the IDE story. + outputDirectory = file("generated/antlr/org/pkl/core/parser/antlr") +} + +tasks.compileJava { + dependsOn(tasks.generateGrammarSource) +} + +tasks.sourcesJar { + dependsOn(tasks.generateGrammarSource) +} + +tasks.generateTestGrammarSource { + enabled = false +} +tasks.named("generateGeneratorGrammarSource") { + enabled = false +} + +// Satisfy expectations of IntelliJ ANTLR plugin, +// which can't otherwise cope with our ANTLR setup. +val makeIntelliJAntlrPluginHappy by tasks.registering(Copy::class) { + dependsOn(tasks.generateGrammarSource) + into("src/main/antlr") + from("generated/antlr/org/pkl/core/parser/antlr") { + include("PklLexer.tokens") + } +} + +tasks.processResources { + inputs.property("version", buildInfo.pklVersion) + inputs.property("commitId", buildInfo.commitId) + + filesMatching("org/pkl/core/Release.properties") { + val stdlibModules = fileTree("$rootDir/stdlib") { + include("*.pkl") + exclude("doc-package-info.pkl") + }.map { "pkl:" + it.nameWithoutExtension } + .sortedBy { it.toLowerCase() } + + filter("tokens" to mapOf( + "version" to buildInfo.pklVersion, + "commitId" to buildInfo.commitId, + "stdlibModules" to stdlibModules.joinToString(",") + )) + } + + into("org/pkl/core/stdlib") { + from("$rootDir/stdlib") { + include("*.pkl") + } + } +} + +tasks.compileJava { + options.generatedSourceOutputDirectory.set(file("generated/truffle")) +} + +tasks.compileKotlin { + enabled = false +} + +tasks.test { + inputs.dir("src/test/files/LanguageSnippetTests/input") + inputs.dir("src/test/files/LanguageSnippetTests/input-helper") + inputs.dir("src/test/files/LanguageSnippetTests/output") + + useJUnitPlatform { + excludeEngines("MacAmd64LanguageSnippetTestsEngine") + excludeEngines("MacAarch64LanguageSnippetTestsEngine") + excludeEngines("LinuxAmd64LanguageSnippetTestsEngine") + excludeEngines("LinuxAarch64LanguageSnippetTestsEngine") + excludeEngines("AlpineLanguageSnippetTestsEngine") + } +} + +val testJavaExecutable by tasks.registering(Test::class) { + inputs.dir("src/test/files/LanguageSnippetTests/input") + inputs.dir("src/test/files/LanguageSnippetTests/input-helper") + inputs.dir("src/test/files/LanguageSnippetTests/output") + + testClassesDirs = files(tasks.test.get().testClassesDirs) + classpath = + // compiled test classes + sourceSets.test.get().output + + // java executable + javaExecutableConfiguration + + // test-only dependencies + // (test dependencies that are also main dependencies must already be contained in java executable; + // to verify that we don't want to include them here) + (configurations.testRuntimeClasspath.get() - configurations.runtimeClasspath.get()) + + useJUnitPlatform { + includeEngines("LanguageSnippetTestsEngine") + } +} + +tasks.check { + dependsOn(testJavaExecutable) +} + +val testMacExecutableAmd64 by tasks.registering(Test::class) { + enabled = buildInfo.os.isMacOsX && buildInfo.graalVm.isGraal22 + dependsOn(":pkl-cli:macExecutableAmd64") + + inputs.dir("src/test/files/LanguageSnippetTests/input") + inputs.dir("src/test/files/LanguageSnippetTests/input-helper") + inputs.dir("src/test/files/LanguageSnippetTests/output") + + testClassesDirs = files(tasks.test.get().testClassesDirs) + classpath = tasks.test.get().classpath + + useJUnitPlatform { + includeEngines("MacAmd64LanguageSnippetTestsEngine") + } +} + +val testMacExecutableAarch64 by tasks.registering(Test::class) { + enabled = buildInfo.os.isMacOsX && !buildInfo.graalVm.isGraal22 + dependsOn(":pkl-cli:macExecutableAarch64") + + inputs.dir("src/test/files/LanguageSnippetTests/input") + inputs.dir("src/test/files/LanguageSnippetTests/input-helper") + inputs.dir("src/test/files/LanguageSnippetTests/output") + + testClassesDirs = files(tasks.test.get().testClassesDirs) + classpath = tasks.test.get().classpath + + useJUnitPlatform { + includeEngines("MacAarch64LanguageSnippetTestsEngine") + } +} + +val testLinuxExecutableAmd64 by tasks.registering(Test::class) { + enabled = buildInfo.os.isLinux && buildInfo.arch == "amd64" + dependsOn(":pkl-cli:linuxExecutableAmd64") + + inputs.dir("src/test/files/LanguageSnippetTests/input") + inputs.dir("src/test/files/LanguageSnippetTests/input-helper") + inputs.dir("src/test/files/LanguageSnippetTests/output") + + testClassesDirs = files(tasks.test.get().testClassesDirs) + classpath = tasks.test.get().classpath + + useJUnitPlatform { + includeEngines("LinuxAmd64LanguageSnippetTestsEngine") + } +} + +val testLinuxExecutableAarch64 by tasks.registering(Test::class) { + enabled = buildInfo.os.isLinux && buildInfo.arch == "aarch64" + dependsOn(":pkl-cli:linuxExecutableAarch64") + + inputs.dir("src/test/files/LanguageSnippetTests/input") + inputs.dir("src/test/files/LanguageSnippetTests/input-helper") + inputs.dir("src/test/files/LanguageSnippetTests/output") + + testClassesDirs = files(tasks.test.get().testClassesDirs) + classpath = tasks.test.get().classpath + + useJUnitPlatform { + includeEngines("LinuxAarch64LanguageSnippetTestsEngine") + } +} + +val testAlpineExecutableAmd64 by tasks.registering(Test::class) { + enabled = buildInfo.os.isLinux && buildInfo.arch == "amd64" + dependsOn(":pkl-cli:alpineExecutableAmd64") + + inputs.dir("src/test/files/LanguageSnippetTests/input") + inputs.dir("src/test/files/LanguageSnippetTests/input-helper") + inputs.dir("src/test/files/LanguageSnippetTests/output") + + testClassesDirs = files(tasks.test.get().testClassesDirs) + classpath = tasks.test.get().classpath + + useJUnitPlatform { + includeEngines("AlpineLanguageSnippetTestsEngine") + } +} + +tasks.checkNative { + dependsOn(testLinuxExecutableAmd64) + dependsOn(testLinuxExecutableAarch64) + dependsOn(testMacExecutableAmd64) + dependsOn(testMacExecutableAarch64) + dependsOn(testAlpineExecutableAmd64) +} + +tasks.clean { + delete("generated/") + delete("$buildDir/test-packages") +} + +spotless { + antlr4 { + licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt")) + target(files("src/main/antlr/PklParser.g4", "src/main/antlr/PklLexer.g4")) + } +} diff --git a/pkl-core/src/generator/kotlin/org/pkl/core/generator/MemberRegistryGenerator.kt b/pkl-core/src/generator/kotlin/org/pkl/core/generator/MemberRegistryGenerator.kt new file mode 100644 index 00000000..d97cf8c0 --- /dev/null +++ b/pkl-core/src/generator/kotlin/org/pkl/core/generator/MemberRegistryGenerator.kt @@ -0,0 +1,163 @@ +package org.pkl.core.generator + +import com.oracle.truffle.api.dsl.GeneratedBy +import com.squareup.javapoet.ClassName +import javax.lang.model.SourceVersion +import javax.annotation.processing.RoundEnvironment +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeSpec +import javax.annotation.processing.AbstractProcessor +import javax.lang.model.element.* +import javax.lang.model.type.TypeMirror + +/** + * Generates a subclass of `org.pkl.core.stdlib.registry.ExternalMemberRegistry` + * for each stdlib module and a factory to instantiate them. + * Generated classes are written to `generated/truffle/org/pkl/core/stdlib/registry`. + * + * Inputs: + * - Generated Truffle node classes for stdlib members. + * These classes are located in subpackages of `org.pkl.core.stdlib` + * and identified via their `@GeneratedBy` annotations. + * - `@PklName` annotations on hand-written node classes from which Truffle node classes are generated. + */ +class MemberRegistryGenerator : AbstractProcessor() { + private val truffleNodeClassSuffix = "NodeGen" + private val truffleNodeFactorySuffix = "NodesFactory" + private val stdLibPackageName: String = "org.pkl.core.stdlib" + private val registryPackageName: String = "$stdLibPackageName.registry" + private val modulePackageName: String = "org.pkl.core.module" + + private val externalMemberRegistryClassName: ClassName = + ClassName.get(registryPackageName, "ExternalMemberRegistry") + private val emptyMemberRegistryClassName: ClassName = + ClassName.get(registryPackageName, "EmptyMemberRegistry") + private val memberRegistryFactoryClassName: ClassName = + ClassName.get(registryPackageName, "MemberRegistryFactory") + private val moduleKeyClassName: ClassName = + ClassName.get(modulePackageName, "ModuleKey") + private val moduleKeysClassName: ClassName = + ClassName.get(modulePackageName, "ModuleKeys") + + override fun getSupportedAnnotationTypes(): Set = setOf(GeneratedBy::class.java.name) + + override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.RELEASE_11 + + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + if (annotations.isEmpty()) return true + + val nodeClassesByPackage = collectNodeClasses(roundEnv) + generateRegistryClasses(nodeClassesByPackage) + generateRegistryFactoryClass(nodeClassesByPackage.keys) + + return true + } + + private fun collectNodeClasses(roundEnv: RoundEnvironment) = roundEnv + .getElementsAnnotatedWith(GeneratedBy::class.java) + .asSequence() + .filterIsInstance() + .filter { it.qualifiedName.toString().startsWith(stdLibPackageName) } + .filter { it.simpleName.toString().endsWith(truffleNodeClassSuffix) } + .sortedWith(compareBy( + { if (it.enclosingElement.kind == ElementKind.PACKAGE) "" else it.enclosingElement.simpleName.toString() }, + { it.simpleName.toString() } + )) + .groupBy { processingEnv.elementUtils.getPackageOf(it) } + + private fun generateRegistryClasses(nodeClassesByPackage: Map>) { + for ((pkg, nodeClasses) in nodeClassesByPackage) { + generateRegistryClass(pkg, nodeClasses) + } + } + + private fun generateRegistryClass(pkg: PackageElement, nodeClasses: List) { + val pklModuleName = getAnnotatedPklName(pkg) ?: pkg.simpleName.toString() + val pklModuleNameCapitalized = pklModuleName.capitalize() + val registryClassName = ClassName.get(registryPackageName, "${pklModuleNameCapitalized}MemberRegistry") + + val registryClass = TypeSpec.classBuilder(registryClassName) + .addJavadoc("Generated by {@link ${this::class.qualifiedName}}.") + .addModifiers(Modifier.FINAL) + .superclass(externalMemberRegistryClassName) + val registryClassConstructor = MethodSpec.constructorBuilder() + + for (nodeClass in nodeClasses) { + val enclosingClass = nodeClass.enclosingElement + val pklClassName = getAnnotatedPklName(enclosingClass) + ?: enclosingClass.simpleName.toString().removeSuffix(truffleNodeFactorySuffix) + val pklMemberName = getAnnotatedPklName(nodeClass) + ?: nodeClass.simpleName.toString().removeSuffix(truffleNodeClassSuffix) + val pklMemberNameQualified = when (pklClassName) { + // By convention, the top-level class containing node classes + // for *module* members is named `Nodes`. + // Example: `BaseNodes` for pkl.base + pklModuleNameCapitalized -> + "pkl.$pklModuleName#$pklMemberName" + else -> + "pkl.$pklModuleName#$pklClassName.$pklMemberName" + } + registryClass.addOriginatingElement(nodeClass) + registryClassConstructor + .addStatement("register(\$S, \$T::create)", pklMemberNameQualified, nodeClass) + } + + registryClass.addMethod(registryClassConstructor.build()) + val javaFile = JavaFile.builder(registryPackageName, registryClass.build()).build() + javaFile.writeTo(processingEnv.filer) + } + + private fun generateRegistryFactoryClass(packages: Collection) { + val registryFactoryClass = TypeSpec.classBuilder(memberRegistryFactoryClassName) + .addJavadoc("Generated by {@link ${this::class.qualifiedName}}.") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + val registryFactoryConstructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + registryFactoryClass.addMethod(registryFactoryConstructor.build()) + val registryFactoryGetMethod = MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(moduleKeyClassName, "moduleKey") + .returns(externalMemberRegistryClassName) + .beginControlFlow("if (!\$T.isStdLibModule(moduleKey))", moduleKeysClassName) + .addStatement("return \$T.INSTANCE", emptyMemberRegistryClassName) + .endControlFlow() + .beginControlFlow("switch (moduleKey.getUri().getSchemeSpecificPart())") + + for (pkg in packages) { + val pklModuleName = getAnnotatedPklName(pkg) ?: pkg.simpleName.toString() + val pklModuleNameCapitalized = pklModuleName.capitalize() + val registryClassName = ClassName.get(registryPackageName, "${pklModuleNameCapitalized}MemberRegistry") + + // declare dependency on package-info.java (for `@PklName`) + registryFactoryClass.addOriginatingElement(pkg) + registryFactoryGetMethod + .addCode("case \$S:\n", pklModuleName) + .addStatement(" return new \$T()", registryClassName) + } + + registryFactoryGetMethod + .addCode("default:\n") + .addStatement(" return \$T.INSTANCE", emptyMemberRegistryClassName) + .endControlFlow() + registryFactoryClass.addMethod(registryFactoryGetMethod.build()) + val javaFile = JavaFile.builder(registryPackageName, registryFactoryClass.build()).build() + javaFile.writeTo(processingEnv.filer) + } + + private fun getAnnotatedPklName(element: Element): String? { + for (annotation in element.annotationMirrors) { + when (annotation.annotationType.asElement().simpleName.toString()) { + "PklName" -> + return annotation.elementValues.values.iterator().next().value.toString() + "GeneratedBy" -> { + val annotationValue = annotation.elementValues.values.first().value as TypeMirror + return getAnnotatedPklName(processingEnv.typeUtils.asElement(annotationValue)) + } + } + } + return null + } + + private fun String.capitalize(): String = replaceFirstChar { it.titlecaseChar() } +} diff --git a/pkl-core/src/generator/resources/META-INF/gradle/incremental.annotation.processors b/pkl-core/src/generator/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 00000000..a8807956 --- /dev/null +++ b/pkl-core/src/generator/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1,2 @@ +# https://docs.gradle.org/current/userguide/java_plugin.html#aggregating_annotation_processors +org.pkl.core.generator.MemberRegistryGenerator,aggregating diff --git a/pkl-core/src/generator/resources/META-INF/services/javax.annotation.processing.Processor b/pkl-core/src/generator/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000..ce1e9611 --- /dev/null +++ b/pkl-core/src/generator/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.pkl.core.generator.MemberRegistryGenerator diff --git a/pkl-core/src/main/antlr/PclLexer.tokens b/pkl-core/src/main/antlr/PclLexer.tokens new file mode 100644 index 00000000..19b20fb6 --- /dev/null +++ b/pkl-core/src/main/antlr/PclLexer.tokens @@ -0,0 +1,170 @@ +ABSTRACT=1 +AMENDS=2 +AS=3 +CLASS=4 +ELSE=5 +EXTENDS=6 +EXTERNAL=7 +FALSE=8 +FINAL=9 +FOR=10 +FUNCTION=11 +HIDDEN_=12 +IF=13 +IMPORT=14 +IMPORT_GLOB=15 +IN=16 +IS=17 +LET=18 +LOCAL=19 +MODULE=20 +NEW=21 +NOTHING=22 +NULL=23 +OPEN=24 +OUT=25 +OUTER=26 +READ=27 +READ_GLOB=28 +READ_OR_NULL=29 +SUPER=30 +THIS=31 +THROW=32 +TRACE=33 +TRUE=34 +TYPE_ALIAS=35 +UNKNOWN=36 +WHEN=37 +LPAREN=38 +RPAREN=39 +LBRACE=40 +RBRACE=41 +LBRACK=42 +RBRACK=43 +LPRED=44 +COMMA=45 +DOT=46 +QDOT=47 +COALESCE=48 +NON_NULL=49 +AT=50 +ASSIGN=51 +GT=52 +LT=53 +NOT=54 +QUESTION=55 +COLON=56 +ARROW=57 +EQUAL=58 +NOT_EQUAL=59 +LTE=60 +GTE=61 +AND=62 +OR=63 +PLUS=64 +MINUS=65 +POW=66 +MUL=67 +DIV=68 +INT_DIV=69 +MOD=70 +UNION=71 +PIPE=72 +SPREAD=73 +QSPREAD=74 +SLQuote=75 +MLQuote=76 +IntLiteral=77 +FloatLiteral=78 +Identifier=79 +NewlineSemicolon=80 +Whitespace=81 +DocComment=82 +BlockComment=83 +LineComment=84 +ShebangComment=85 +SLEndQuote=86 +SLInterpolation=87 +SLUnicodeEscape=88 +SLCharacterEscape=89 +SLCharacters=90 +MLEndQuote=91 +MLInterpolation=92 +MLUnicodeEscape=93 +MLCharacterEscape=94 +MLNewline=95 +MLCharacters=96 +'abstract'=1 +'amends'=2 +'as'=3 +'class'=4 +'else'=5 +'extends'=6 +'external'=7 +'false'=8 +'final'=9 +'for'=10 +'function'=11 +'hidden'=12 +'if'=13 +'import'=14 +'import*'=15 +'in'=16 +'is'=17 +'let'=18 +'local'=19 +'module'=20 +'new'=21 +'nothing'=22 +'null'=23 +'open'=24 +'out'=25 +'outer'=26 +'read'=27 +'read*'=28 +'read?'=29 +'super'=30 +'this'=31 +'throw'=32 +'trace'=33 +'true'=34 +'typealias'=35 +'unknown'=36 +'when'=37 +'('=38 +')'=39 +'{'=40 +'}'=41 +'['=42 +']'=43 +'[['=44 +','=45 +'.'=46 +'?.'=47 +'??'=48 +'!!'=49 +'@'=50 +'='=51 +'>'=52 +'<'=53 +'!'=54 +'?'=55 +':'=56 +'->'=57 +'=='=58 +'!='=59 +'<='=60 +'>='=61 +'&&'=62 +'||'=63 +'+'=64 +'-'=65 +'**'=66 +'*'=67 +'/'=68 +'~/'=69 +'%'=70 +'|'=71 +'|>'=72 +'...'=73 +'...?'=74 diff --git a/pkl-core/src/main/antlr/PklLexer.g4 b/pkl-core/src/main/antlr/PklLexer.g4 new file mode 100644 index 00000000..389a2bcf --- /dev/null +++ b/pkl-core/src/main/antlr/PklLexer.g4 @@ -0,0 +1,387 @@ +/** + * 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. + */ +lexer grammar PklLexer; + +@header { +package org.pkl.core.parser.antlr; +} + +@members { +class StringInterpolationScope { + int parenLevel = 0; + int poundLength = 0; +} + +java.util.Deque interpolationScopes = new java.util.ArrayDeque<>(); +StringInterpolationScope interpolationScope; + +{ pushInterpolationScope(); } + +void pushInterpolationScope() { + interpolationScope = new StringInterpolationScope(); + interpolationScopes.push(interpolationScope); +} + +void incParenLevel() { + interpolationScope.parenLevel += 1; +} + +void decParenLevel() { + if (interpolationScope.parenLevel == 0) { + // guard against syntax errors + if (interpolationScopes.size() > 1) { + interpolationScopes.pop(); + interpolationScope = interpolationScopes.peek(); + popMode(); + } + } else { + interpolationScope.parenLevel -= 1; + } +} + +boolean isPounds() { + // optimize for common cases (0, 1) + switch (interpolationScope.poundLength) { + case 0: return true; + case 1: return _input.LA(1) == '#'; + default: + int poundLength = interpolationScope.poundLength; + for (int i = 1; i <= poundLength; i++) { + if (_input.LA(i) != '#') return false; + } + return true; + } +} + +boolean isQuote() { + return _input.LA(1) == '"'; +} + +boolean endsWithPounds(String text) { + assert text.length() >= 2; + + // optimize for common cases (0, 1) + switch (interpolationScope.poundLength) { + case 0: return true; + case 1: return text.charAt(text.length() - 1) == '#'; + default: + int poundLength = interpolationScope.poundLength; + int textLength = text.length(); + if (textLength < poundLength) return false; + + int stop = textLength - poundLength; + for (int i = textLength - 1; i >= stop; i--) { + if (text.charAt(i) != '#') return false; + } + + return true; + } +} + +void removeBackTicks() { + String text = getText(); + setText(text.substring(1, text.length() - 1)); +} + +// look ahead in predicate rather than consume in grammar so that newlines +// go to NewlineSemicolonChannel, which is important for consumers of that channel +boolean isNewlineOrEof() { + int input = _input.LA(1); + return input == '\n' || input == '\r' || input == IntStream.EOF; +} + +} + +channels { + NewlineSemicolonChannel, + WhitespaceChannel, + CommentsChannel, + ShebangChannel +} + +ABSTRACT : 'abstract'; +AMENDS : 'amends'; +AS : 'as'; +CLASS : 'class'; +CONST : 'const'; +ELSE : 'else'; +EXTENDS : 'extends'; +EXTERNAL : 'external'; +FALSE : 'false'; +FIXED : 'fixed'; +FOR : 'for'; +FUNCTION : 'function'; +HIDDEN_ : 'hidden'; +IF : 'if'; +IMPORT : 'import'; +IMPORT_GLOB : 'import*'; +IN : 'in'; +IS : 'is'; +LET : 'let'; +LOCAL : 'local'; +MODULE : 'module'; +NEW : 'new'; +NOTHING : 'nothing'; +NULL : 'null'; +OPEN : 'open'; +OUT : 'out'; +OUTER : 'outer'; +READ : 'read'; +READ_GLOB : 'read*'; +READ_OR_NULL : 'read?'; +SUPER : 'super'; +THIS : 'this'; +THROW : 'throw'; +TRACE : 'trace'; +TRUE : 'true'; +TYPE_ALIAS : 'typealias'; +UNKNOWN : 'unknown'; +WHEN : 'when'; + +// reserved for future use, but not used today +PROTECTED : 'protected'; +OVERRIDE : 'override'; +RECORD : 'record'; +DELETE : 'delete'; +CASE : 'case'; +SWITCH : 'switch'; +VARARG : 'vararg'; + +LPAREN : '(' { incParenLevel(); }; +RPAREN : ')' { decParenLevel(); }; +LBRACE : '{'; +RBRACE : '}'; +LBRACK : '['; +RBRACK : ']'; +LPRED : '[['; // No RPRED, because that lexes too eager to allow nested index expressions, e.g. foo[bar[baz]] +COMMA : ','; +DOT : '.'; +QDOT : '?.'; +COALESCE : '??'; +NON_NULL : '!!'; + +AT : '@'; +ASSIGN : '='; +GT : '>'; +LT : '<'; +NOT : '!'; +QUESTION : '?'; +COLON : ':'; +ARROW : '->'; +EQUAL : '=='; +NOT_EQUAL : '!='; +LTE : '<='; +GTE : '>='; +AND : '&&'; +OR : '||'; +PLUS : '+'; +MINUS : '-'; +POW : '**'; +STAR : '*'; +DIV : '/'; +INT_DIV : '~/'; +MOD : '%'; +UNION : '|'; +PIPE : '|>'; +SPREAD : '...'; +QSPREAD : '...?'; +UNDERSCORE : '_'; + +SLQuote : '#'* '"' { interpolationScope.poundLength = getText().length() - 1; } -> pushMode(SLString); +MLQuote : '#'* '"""' { interpolationScope.poundLength = getText().length() - 3; } -> pushMode(MLString); + +IntLiteral + : DecimalLiteral + | HexadecimalLiteral + | BinaryLiteral + | OctalLiteral +; + +// leading zeros are allowed (cf. Swift) +fragment DecimalLiteral + : DecimalDigit DecimalDigitCharacters? + ; + +fragment DecimalDigitCharacters + : DecimalDigitCharacter+ + ; + +fragment DecimalDigitCharacter + : DecimalDigit + | '_' + ; + +fragment DecimalDigit + : [0-9] + ; + +fragment HexadecimalLiteral + : '0x' HexadecimalCharacter+ // intentionally allow underscore after '0x'; e.g. `0x_ab`. We will throw an error in AstBuilder. + ; + +fragment HexadecimalCharacter + : [0-9a-fA-F_] + ; + +fragment BinaryLiteral + : '0b' BinaryCharacter+ // intentionally allow underscore after '0b'; e.g. `0b_11`. We will throw an error in AstBuilder. + ; + +fragment BinaryCharacter + : [01_] + ; + +fragment OctalLiteral + : '0o' OctalCharacter+ // intentionally allow underscore after '0o'; e.g. `0o_34`. We will throw an error in AstBuilder. + ; + +fragment OctalCharacter + : [0-7_] + ; + +FloatLiteral + : DecimalLiteral? '.' '_'? DecimalLiteral Exponent? // intentionally allow underscore. We will throw an error in AstBuilder. + | DecimalLiteral Exponent + ; + +fragment Exponent + : [eE] [+-]? '_'? DecimalLiteral // intentionally allow underscore. We will throw an error in AstBuilder. + ; + +Identifier + : RegularIdentifier + | QuotedIdentifier { removeBackTicks(); } + ; + +// Note: Keep in sync with Lexer.isRegularIdentifier() +fragment RegularIdentifier + : IdentifierStart IdentifierPart* + ; + +fragment QuotedIdentifier + : '`' (~'`')+ '`' + ; + +fragment +IdentifierStart + : [a-zA-Z$_] // handle common cases without a predicate + | . {Character.isUnicodeIdentifierStart(_input.LA(-1))}? + ; + +fragment +IdentifierPart + : [a-zA-Z0-9$_] // handle common cases without a predicate + | . {Character.isUnicodeIdentifierPart(_input.LA(-1))}? + ; + +NewlineSemicolon + : [\r\n;]+ -> channel(NewlineSemicolonChannel) + ; + +// Note: Java, Scala, and Swift treat \f as whitespace; Dart doesn't. +// Python and C also include vertical tab. +// C# also includes Unicode class Zs (separator, space). +Whitespace + : [ \t\f]+ -> channel(WhitespaceChannel) + ; + +DocComment + : ([ \t\f]* '///' .*? (Newline|EOF))+ + ; + +BlockComment + : '/*' (BlockComment | .)*? '*/' -> channel(CommentsChannel) + ; + +LineComment + : '//' .*? {isNewlineOrEof()}? -> channel(CommentsChannel) + ; + +ShebangComment + : '#!' .*? {isNewlineOrEof()}? -> channel(ShebangChannel) + ; + +// strict: '\\' Pounds 'u{' HexDigit (HexDigit (HexDigit (HexDigit (HexDigit (HexDigit (HexDigit HexDigit? )?)?)?)?)?)? '}' +fragment UnicodeEscape + : '\\' Pounds 'u{' ~[}\r\n "]* '}'? + ; + +// strict: '\\' Pounds [tnr"\\] +fragment CharacterEscape + : '\\' Pounds . + ; + +fragment Pounds + : { interpolationScope.poundLength == 0 }? + | '#' { interpolationScope.poundLength == 1 }? + | '#'+ { endsWithPounds(getText()) }? + ; + +fragment Newline + : '\n' | '\r' '\n'? + ; + +mode SLString; + +// strict: '"' Pounds +SLEndQuote + : ('"' Pounds | Newline ) -> popMode + ; + +SLInterpolation + : '\\' Pounds '(' { pushInterpolationScope(); } -> pushMode(DEFAULT_MODE) + ; + +SLUnicodeEscape + : UnicodeEscape + ; + +SLCharacterEscape + : CharacterEscape + ; + +SLCharacters + : ~["\\\r\n]+ SLCharacters? + | ["\\] {!isPounds()}? SLCharacters? + ; + +mode MLString; + +MLEndQuote + : '"""' Pounds -> popMode + ; + +MLInterpolation + : '\\' Pounds '(' { pushInterpolationScope(); } -> pushMode(DEFAULT_MODE) + ; + +MLUnicodeEscape + : UnicodeEscape + ; + +MLCharacterEscape + : CharacterEscape + ; + +MLNewline + : Newline + ; + +MLCharacters + : ~["\\\r\n]+ MLCharacters? + | ('\\' | '"""') {!isPounds()}? MLCharacters? + | '"' '"'? {!isQuote()}? MLCharacters? + ; diff --git a/pkl-core/src/main/antlr/PklParser.g4 b/pkl-core/src/main/antlr/PklParser.g4 new file mode 100644 index 00000000..de9ed4c2 --- /dev/null +++ b/pkl-core/src/main/antlr/PklParser.g4 @@ -0,0 +1,255 @@ +/** + * 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. + */ +parser grammar PklParser; + +@header { +package org.pkl.core.parser.antlr; +} + +@members { +/** + * Returns true if and only if the next token to be consumed is not preceded by a newline or semicolon. + */ +boolean noNewlineOrSemicolon() { + for (int i = _input.index() - 1; i >= 0; i--) { + Token token = _input.get(i); + int channel = token.getChannel(); + if (channel == PklLexer.DEFAULT_TOKEN_CHANNEL) return true; + if (channel == PklLexer.NewlineSemicolonChannel) return false; + } + return true; +} +} + +options { + tokenVocab = PklLexer; +} + +replInput + : ((moduleDecl + | importClause + | clazz + | typeAlias + | classProperty + | classMethod + | expr))* EOF + ; + +exprInput + : expr EOF + ; + +module + : moduleDecl? (is+=importClause)* ((cs+=clazz | ts+=typeAlias | ps+=classProperty | ms+=classMethod))* EOF + ; + +moduleDecl + : t=DocComment? annotation* moduleHeader + ; + +moduleHeader + : modifier* 'module' qualifiedIdentifier moduleExtendsOrAmendsClause? + | moduleExtendsOrAmendsClause + ; + +moduleExtendsOrAmendsClause + : t=('extends' | 'amends') stringConstant + ; + +importClause + : t=('import' | 'import*') stringConstant ('as' Identifier)? + ; + +clazz + : t=DocComment? annotation* classHeader classBody? + ; + +classHeader + : modifier* 'class' Identifier typeParameterList? ('extends' type)? + ; + +modifier + : t=('external' | 'abstract' | 'open' | 'local' | 'hidden' | 'fixed' | 'const') + ; + +classBody + : '{' ((ps+=classProperty | ms+=classMethod))* err='}'? + ; + +typeAlias + : t=DocComment? annotation* typeAliasHeader '=' type + ; + +typeAliasHeader + : modifier* 'typealias' Identifier typeParameterList? + ; + +// allows `foo: Bar { ... }` s.t. AstBuilder can provide better error message +classProperty + : t=DocComment? annotation* modifier* Identifier (typeAnnotation | typeAnnotation? ('=' expr | objectBody+)) + ; + +classMethod + : t=DocComment? annotation* methodHeader ('=' expr)? + ; + +methodHeader + : modifier* 'function' Identifier typeParameterList? parameterList typeAnnotation? + ; + +parameterList + : '(' (ts+=parameter (errs+=','? ts+=parameter)*)? err=')'? + ; + +argumentList + : {noNewlineOrSemicolon()}? '(' (es+=expr (errs+=','? es+=expr)*)? err=')'? + ; + +annotation + : '@' type objectBody? + ; + +qualifiedIdentifier + : ts+=Identifier ('.' ts+=Identifier)* + ; + +typeAnnotation + : ':' type + ; + +typeParameterList + : '<' ts+=typeParameter (errs+=','? ts+=typeParameter)* err='>'? + ; + +typeParameter + : t=('in' | 'out')? Identifier + ; + +typeArgumentList + : '<' ts+=type (errs+=','? ts+=type)* err='>'? + ; + +type + : 'unknown' # unknownType + | 'nothing' # nothingType + | 'module' # moduleType + | stringConstant # stringLiteralType + | qualifiedIdentifier typeArgumentList? # declaredType + | '(' type err=')'? # parenthesizedType + | type '?' # nullableType + | type {noNewlineOrSemicolon()}? t='(' es+=expr (errs+=','? es+=expr)* err=')'? # constrainedType + | '*' u=type # defaultUnionType + | l=type '|' r=type # unionType + | t='(' (ps+=type (errs+=','? ps+=type)*)? err=')'? '->' r=type # functionType + ; + +typedIdentifier + : Identifier typeAnnotation? + ; + +parameter + : '_' + | typedIdentifier + ; + +// Many languages (e.g., Python) give `**` higher precedence than unary minus. +// The reason is that in Math, `-a^2` means `-(a^2)`. +// To avoid confusion, JS rejects `-a**2` and requires explicit parens. +// `-3.abs()` is a similar problem, handled differently by different languages. +expr + : 'this' # thisExpr + | 'outer' # outerExpr + | 'module' # moduleExpr + | 'null' # nullLiteral + | 'true' # trueLiteral + | 'false' # falseLiteral + | IntLiteral # intLiteral + | FloatLiteral # floatLiteral + | 'throw' '(' expr err=')'? # throwExpr + | 'trace' '(' expr err=')'? # traceExpr + | t=('import' | 'import*') '(' stringConstant err=')'? # importExpr + | t=('read' | 'read?' | 'read*') '(' expr err=')'? # readExpr + | Identifier argumentList? # unqualifiedAccessExpr + | t=SLQuote singleLineStringPart* t2=SLEndQuote # singleLineStringLiteral + | t=MLQuote multiLineStringPart* t2=MLEndQuote # multiLineStringLiteral + | t='new' type? objectBody # newExpr + | expr objectBody # amendExpr + | 'super' '.' Identifier argumentList? # superAccessExpr + | 'super' t='[' e=expr err=']'? # superSubscriptExpr + | expr t=('.' | '?.') Identifier argumentList? # qualifiedAccessExpr + | l=expr {noNewlineOrSemicolon()}? t='[' r=expr err=']'? # subscriptExpr + | expr '!!' # nonNullExpr + | '-' expr # unaryMinusExpr + | '!' expr # logicalNotExpr + | l=expr t='**' r=expr # exponentiationExpr + // for some reason, moving rhs of rules starting with `l=expr` into a + // separate rule (to avoid repeated parsing of `expr`) messes up precedence + | l=expr t=('*' | '/' | '~/' | '%') r=expr # multiplicativeExpr + | l=expr (t='+' | {noNewlineOrSemicolon()}? t='-') r=expr # additiveExpr + | l=expr t=('<' | '>' | '<=' | '>=') r=expr # comparisonExpr + | l=expr t=('is' | 'as') r=type # typeTestExpr + | l=expr t=('==' | '!=') r=expr # equalityExpr + | l=expr t='&&' r=expr # logicalAndExpr + | l=expr t='||' r=expr # logicalOrExpr + | l=expr t='|>' r=expr # pipeExpr + | l=expr t='??' r=expr # nullCoalesceExpr + | 'if' '(' c=expr err=')'? l=expr 'else' r=expr # ifExpr + | 'let' '(' parameter '=' l=expr err=')'? r=expr # letExpr + | parameterList '->' expr # functionLiteral + | '(' expr err=')'? # parenthesizedExpr + ; + +objectBody + : '{' (ps+=parameter (errs+=','? ps+=parameter)* '->')? objectMember* err='}'? + ; + +objectMember + : modifier* Identifier (typeAnnotation? '=' expr | objectBody+) # objectProperty + | methodHeader '=' expr # objectMethod + | t='[[' k=expr err1=']'? err2=']'? ('=' v=expr | objectBody+) # memberPredicate + | t='[' k=expr err1=']'? err2=']'? ('=' v=expr | objectBody+) # objectEntry + | expr # objectElement + | ('...' | '...?') expr # objectSpread + | 'when' '(' e=expr err=')'? (b1=objectBody ('else' b2=objectBody)?) # whenGenerator + | 'for' '(' t1=parameter (',' t2=parameter)? 'in' e=expr err=')'? objectBody # forGenerator + ; + +stringConstant + : t=SLQuote (ts+=SLCharacters | ts+=SLCharacterEscape | ts+=SLUnicodeEscape)* t2=SLEndQuote + ; + +singleLineStringPart + : SLInterpolation e=expr ')' + | (ts+=SLCharacters | ts+=SLCharacterEscape | ts+=SLUnicodeEscape)+ + ; + +multiLineStringPart + : MLInterpolation e=expr ')' + | (ts+=MLCharacters | ts+=MLNewline | ts+=MLCharacterEscape | ts+=MLUnicodeEscape)+ + ; + +// intentionally unused +//TODO: we get a "Mismatched Input" error unless we introduce this parser rule. Why? +reservedKeyword + : 'protected' + | 'override' + | 'record' + | 'delete' + | 'case' + | 'switch' + | 'vararg' + | 'const' + ; diff --git a/pkl-core/src/main/java/org/pkl/core/BufferedLogger.java b/pkl-core/src/main/java/org/pkl/core/BufferedLogger.java new file mode 100644 index 00000000..1f1ce499 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/BufferedLogger.java @@ -0,0 +1,47 @@ +/** + * 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; + +/** A logger that keeps messages locally and can return them. */ +public class BufferedLogger implements Logger { + + private final StringBuilder builder = new StringBuilder(); + private final Logger logger; + + public BufferedLogger(Logger logger) { + this.logger = logger; + } + + @Override + public void trace(String message, StackFrame frame) { + builder.append(message).append("\n"); + logger.trace(message, frame); + } + + @Override + public void warn(String message, StackFrame frame) { + builder.append(message).append("\n"); + logger.warn(message, frame); + } + + public void clear() { + builder.setLength(0); + } + + public String getLogs() { + return builder.toString(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Composite.java b/pkl-core/src/main/java/org/pkl/core/Composite.java new file mode 100644 index 00000000..dcb5fb07 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Composite.java @@ -0,0 +1,50 @@ +/** + * 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; + +import java.util.Map; +import org.pkl.core.util.Nullable; + +/** A container of properties. */ +public interface Composite extends Value { + /** Shorthand for {@code getProperties.containsKey(name)}. */ + default boolean hasProperty(String name) { + return getProperties().containsKey(name); + } + + /** + * Returns the value of the property with the given name, or throws {@link + * NoSuchPropertyException} if no such property exists. + */ + Object getProperty(String name); + + /** Shorthand for {@code getProperties().get(name)}; */ + default @Nullable Object getPropertyOrNull(String name) { + return getProperties().get(name); + } + + /** + * Same as {@link #getProperty} except that this method returns {@code null} instead of {@link + * PNull} for Pkl value {@code null}. + */ + default @Nullable Object get(String name) { + var result = getProperty(name); + return result instanceof PNull ? null : result; + } + + /** Returns the properties of this composite. */ + Map getProperties(); +} diff --git a/pkl-core/src/main/java/org/pkl/core/DataSize.java b/pkl-core/src/main/java/org/pkl/core/DataSize.java new file mode 100644 index 00000000..843f9fe8 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/DataSize.java @@ -0,0 +1,258 @@ +/** + * 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; + +import static org.pkl.core.DataSizeUnit.*; + +import java.util.Objects; +import org.pkl.core.util.MathUtils; +import org.pkl.core.util.Nullable; + +/** Java representation of a {@code pkl.base#DataSize} value. */ +public final strictfp class DataSize implements Value { + private static final long serialVersionUID = 0L; + + private final double value; + private final DataSizeUnit unit; + + /** Constructs a new data size with the given value and unit. */ + public DataSize(double value, DataSizeUnit unit) { + this.value = value; + this.unit = Objects.requireNonNull(unit, "unit"); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#BYTES}. */ + public static DataSize ofBytes(double value) { + return new DataSize(value, BYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#KILOBYTES}. */ + public static DataSize ofKilobytes(double value) { + return new DataSize(value, KILOBYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#KIBIBYTES}. */ + public static DataSize ofKibibytes(double value) { + return new DataSize(value, KIBIBYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#MEGABYTES}. */ + public static DataSize ofMegabytes(double value) { + return new DataSize(value, MEGABYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#MEBIBYTES}. */ + public static DataSize ofMebibytes(double value) { + return new DataSize(value, MEBIBYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#GIGABYTES}. */ + public static DataSize ofGigabytes(double value) { + return new DataSize(value, GIGABYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#GIBIBYTES}. */ + public static DataSize ofGibibytes(double value) { + return new DataSize(value, GIBIBYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#TERABYTES}. */ + public static DataSize ofTerabytes(double value) { + return new DataSize(value, TERABYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#TEBIBYTES}. */ + public static DataSize ofTebibytes(double value) { + return new DataSize(value, TEBIBYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#PETABYTES}. */ + public static DataSize ofPetabytes(double value) { + return new DataSize(value, PETABYTES); + } + + /** Constructs a new data size with the given value and unit {@link DataSizeUnit#PEBIBYTES}. */ + public static DataSize ofPebibytes(double value) { + return new DataSize(value, PEBIBYTES); + } + + /** Returns the value of this data size. The value is relative to the unit and may be negative. */ + public double getValue() { + return value; + } + + /** Returns the unit of this data size. */ + public DataSizeUnit getUnit() { + return unit; + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#BYTES}. */ + public double inBytes() { + return convertValueTo(BYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#KILOBYTES}. */ + public double inKilobytes() { + return convertValueTo(KILOBYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#KIBIBYTES}. */ + public double inKibibytes() { + return convertValueTo(KIBIBYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#MEGABYTES}. */ + public double inMegabytes() { + return convertValueTo(MEGABYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#MEBIBYTES}. */ + public double inMebibytes() { + return convertValueTo(MEBIBYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#GIGABYTES}. */ + public double inGigabytes() { + return convertValueTo(GIGABYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#GIBIBYTES}. */ + public double inGibibytes() { + return convertValueTo(GIBIBYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#TERABYTES}. */ + public double inTerabytes() { + return convertValueTo(TERABYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#TEBIBYTES}. */ + public double inTebibytes() { + return convertValueTo(TEBIBYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#PETABYTES}. */ + public double inPetabytes() { + return convertValueTo(PETABYTES); + } + + /** Returns the value of this data size measured in {@link DataSizeUnit#PEBIBYTES}. */ + public double inPebibytes() { + return convertValueTo(PEBIBYTES); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#BYTES}. */ + public long inWholeBytes() { + return Math.round(inBytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#KILOBYTES}. */ + public long inWholeKilobytes() { + return Math.round(inKilobytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#KIBIBYTES}. */ + public long inWholeKibibytes() { + return Math.round(inKibibytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#MEGABYTES}. */ + public long inWholeMegabytes() { + return Math.round(inMegabytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#MEBIBYTES}. */ + public long inWholeMebibytes() { + return Math.round(inMebibytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#GIGABYTES}. */ + public long inWholeGigabytes() { + return Math.round(inGigabytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#GIBIBYTES}. */ + public long inWholeGibibytes() { + return Math.round(inGibibytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#TERABYTES}. */ + public long inWholeTerabytes() { + return Math.round(inTerabytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#TEBIBYTES}. */ + public long inWholeTebibytes() { + return Math.round(inTebibytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#PETABYTES}. */ + public long inWholePetabytes() { + return Math.round(inPetabytes()); + } + + /** Returns the value of this data size measured in whole {@link DataSizeUnit#PEBIBYTES}. */ + public long inWholePebibytes() { + return Math.round(inPebibytes()); + } + + /** Returns a new data size with the given unit and this value converted to the given unit. */ + public DataSize convertTo(DataSizeUnit other) { + return new DataSize(convertValueTo(other), other); + } + + /** Returns the value of this data size converted to the given unit. */ + public double convertValueTo(DataSizeUnit other) { + return value * unit.getBytes() / other.getBytes(); + } + + /** {@inheritDoc} */ + @Override + public void accept(ValueVisitor visitor) { + visitor.visitDataSize(this); + } + + /** {@inheritDoc} */ + @Override + public T accept(ValueConverter converter) { + return converter.convertDataSize(this); + } + + /** {@inheritDoc} */ + @Override + public PClassInfo getClassInfo() { + return PClassInfo.DataSize; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof DataSize)) return false; + + var other = (DataSize) obj; + return convertValueTo(DataSizeUnit.BYTES) == other.convertValueTo(DataSizeUnit.BYTES); + } + + @Override + public int hashCode() { + return Double.hashCode(convertValueTo(DataSizeUnit.BYTES)); + } + + @Override + public String toString() { + return MathUtils.isMathematicalInteger(value) ? (long) value + "." + unit : value + "." + unit; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/DataSizeUnit.java b/pkl-core/src/main/java/org/pkl/core/DataSizeUnit.java new file mode 100644 index 00000000..0353e09b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/DataSizeUnit.java @@ -0,0 +1,92 @@ +/** + * 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; + +import org.pkl.core.util.Nullable; + +/** + * The unit of a {@link DataSize}. In Pkl, data size units are represented as String {@link + * #getSymbol() symbols}. + */ +public strictfp enum DataSizeUnit { + BYTES(1, "b"), + KILOBYTES(1000, "kb"), + KIBIBYTES(1024, "kib"), + MEGABYTES(1000 * 1000, "mb"), + MEBIBYTES(1024 * 1024, "mib"), + GIGABYTES(1000 * 1000 * 1000, "gb"), + GIBIBYTES(1024 * 1024 * 1024, "gib"), + TERABYTES(1000L * 1000 * 1000 * 1000, "tb"), + TEBIBYTES(1024L * 1024 * 1024 * 1024, "tib"), + PETABYTES(1000L * 1000 * 1000 * 1000 * 1000, "pb"), + PEBIBYTES(1024L * 1024 * 1024 * 1024 * 1024, "pib"); + + private final long bytes; + + private final String symbol; + + DataSizeUnit(long bytes, String symbol) { + this.bytes = bytes; + this.symbol = symbol; + } + + /** + * Returns the unit with the given symbol, or {@code null} if no unit with the given symbol + * exists. + */ + public static @Nullable DataSizeUnit parse(String symbol) { + switch (symbol) { + case "b": + return BYTES; + case "kb": + return KILOBYTES; + case "kib": + return KIBIBYTES; + case "mb": + return MEGABYTES; + case "mib": + return MEBIBYTES; + case "gb": + return GIGABYTES; + case "gib": + return GIBIBYTES; + case "tb": + return TERABYTES; + case "tib": + return TEBIBYTES; + case "pb": + return PETABYTES; + case "pib": + return PEBIBYTES; + default: + return null; + } + } + + /** Returns the String symbol of this unit. */ + public String getSymbol() { + return symbol; + } + + /** Returns the conversion factor from this unit to bytes. */ + public long getBytes() { + return bytes; + } + + public String toString() { + return symbol; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Duration.java b/pkl-core/src/main/java/org/pkl/core/Duration.java new file mode 100644 index 00000000..eb419b47 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Duration.java @@ -0,0 +1,233 @@ +/** + * 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; + +import static org.pkl.core.DurationUnit.*; + +import java.util.Objects; +import org.pkl.core.util.DurationUtils; +import org.pkl.core.util.Nullable; + +/** Java representation of a {@code pkl.base#Duration} value. */ +public final strictfp class Duration implements Value { + private static final long serialVersionUID = 0L; + + private final double value; + private final DurationUnit unit; + + /** Constructs a new duration with the given value and unit. */ + public Duration(double value, DurationUnit unit) { + this.value = value; + this.unit = Objects.requireNonNull(unit, "unit"); + } + + /** Constructs a new duration with the given value and unit {@link DurationUnit#NANOS}. */ + public static Duration ofNanos(double value) { + return new Duration(value, NANOS); + } + + /** Constructs a new duration with the given value and unit {@link DurationUnit#MICROS}. */ + public static Duration ofMicros(double value) { + return new Duration(value, MICROS); + } + + /** Constructs a new duration with the given value and unit {@link DurationUnit#MILLIS}. */ + public static Duration ofMillis(double value) { + return new Duration(value, MILLIS); + } + + /** Constructs a new duration with the given value and unit {@link DurationUnit#SECONDS}. */ + public static Duration ofSeconds(double value) { + return new Duration(value, SECONDS); + } + + /** Constructs a new duration with the given value and unit {@link DurationUnit#MINUTES}. */ + public static Duration ofMinutes(double value) { + return new Duration(value, MINUTES); + } + + /** Constructs a new duration with the given value and unit {@link DurationUnit#HOURS}. */ + public static Duration ofHours(double value) { + return new Duration(value, HOURS); + } + + /** Constructs a new duration with the given value and unit {@link DurationUnit#DAYS}. */ + public static Duration ofDays(double value) { + return new Duration(value, DAYS); + } + + /** Returns the value of this duration. The value is relative to the unit and may be negative. */ + public double getValue() { + return value; + } + + /** Returns an ISO 8601 representation of this duration. */ + public String toIsoString() { + return DurationUtils.toIsoString(value, unit); + } + + /** Returns the unit of this duration. */ + public DurationUnit getUnit() { + return unit; + } + + /** Returns the value of this duration measured in {@link DurationUnit#NANOS}. */ + public double inNanos() { + return convertValueTo(NANOS); + } + + /** Returns the value of this duration measured in {@link DurationUnit#MICROS}. */ + public double inMicros() { + return convertValueTo(MICROS); + } + + /** Returns the value of this duration measured in {@link DurationUnit#MILLIS}. */ + public double inMillis() { + return convertValueTo(MILLIS); + } + + /** Returns the value of this duration measured in {@link DurationUnit#SECONDS}. */ + public double inSeconds() { + return convertValueTo(SECONDS); + } + + /** Returns the value of this duration measured in {@link DurationUnit#MINUTES}. */ + public double inMinutes() { + return convertValueTo(MINUTES); + } + + /** Returns the value of this duration measured in {@link DurationUnit#HOURS}. */ + public double inHours() { + return convertValueTo(HOURS); + } + + /** Returns the value of this duration measured in {@link DurationUnit#DAYS}. */ + public double inDays() { + return convertValueTo(DAYS); + } + + /** Returns the value of this duration measured in whole {@link DurationUnit#NANOS}. */ + public long inWholeNanos() { + return Math.round(inNanos()); + } + + /** Returns the value of this duration measured in whole {@link DurationUnit#MICROS}. */ + public long inWholeMicros() { + return Math.round(inMicros()); + } + + /** Returns the value of this duration measured in whole {@link DurationUnit#MILLIS}. */ + public long inWholeMillis() { + return Math.round(inMillis()); + } + + /** Returns the value of this duration measured in whole {@link DurationUnit#SECONDS}. */ + public long inWholeSeconds() { + return Math.round(inSeconds()); + } + + /** Returns the value of this duration measured in whole {@link DurationUnit#MINUTES}. */ + public long inWholeMinutes() { + return Math.round(inMinutes()); + } + + /** Returns the value of this duration measured in whole {@link DurationUnit#HOURS}. */ + public long inWholeHours() { + return Math.round(inHours()); + } + + /** Returns the value of this duration measured in whole {@link DurationUnit#DAYS}. */ + public long inWholeDays() { + return Math.round(inDays()); + } + + /** + * Converts this duration to a {@link java.time.Duration}. If {@link #getValue()} is NaN, + * +/-Infinity or too large to fit into {@link java.time.Duration}, {@link ArithmeticException} is + * thrown. + */ + public java.time.Duration toJavaDuration() { + if (!Double.isFinite(value)) { + throw new ArithmeticException( + "Cannot convert Pkl duration `" + this + "` to `java.time.Duration`."); + } + + var l = (long) value; + if (l == value) { + // `value` is a mathematical integer that fits into a long. + // Hence this duration is easy to convert without risk of rounding errors. + // Throws ArithmeticException if this duration doesn't fit into java.time.Duration (e.g., + // Long.MAX_VALUE days). + return java.time.Duration.of(l, unit.toChronoUnit()); + } + + var seconds = convertValueTo(DurationUnit.SECONDS); + var secondsPart = (long) seconds; + var nanosPart = (long) ((seconds - secondsPart) * 1_000_000_000); + // If `seconds` is infinite or too large to fit into java.time.Duration, + // this throws ArithmeticException because one the following holds: + // secondsPart == Long.MAX_VALUE && nanosPart >= 1_000_000_000 + // secondsPart == Long.MIN_VALUE && nanosPart <= -1_000_000_000. + return java.time.Duration.ofSeconds(secondsPart, nanosPart); + } + + /** Returns a new duration with the given unit and this value converted to the given unit. */ + public Duration convertTo(DurationUnit other) { + return new Duration(convertValueTo(other), other); + } + + /** Returns the value of this duration converted to the given unit. */ + public double convertValueTo(DurationUnit other) { + return value * unit.getNanos() / other.getNanos(); + } + + /** {@inheritDoc} */ + @Override + public void accept(ValueVisitor visitor) { + visitor.visitDuration(this); + } + + /** {@inheritDoc} */ + @Override + public T accept(ValueConverter converter) { + return converter.convertDuration(this); + } + + /** {@inheritDoc} */ + @Override + public PClassInfo getClassInfo() { + return PClassInfo.Duration; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Duration)) return false; + + var other = (Duration) obj; + return convertValueTo(DurationUnit.NANOS) == other.convertValueTo(DurationUnit.NANOS); + } + + @Override + public int hashCode() { + return Double.hashCode(convertValueTo(DurationUnit.NANOS)); + } + + @Override + public String toString() { + return DurationUtils.toPklString(value, unit); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/DurationUnit.java b/pkl-core/src/main/java/org/pkl/core/DurationUnit.java new file mode 100644 index 00000000..01046e0d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/DurationUnit.java @@ -0,0 +1,127 @@ +/** + * 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; + +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; +import org.pkl.core.util.Nullable; + +/** + * The unit of a {@link Duration}. In Pkl, duration units are represented as String {@link + * #getSymbol() symbols}. + */ +public enum DurationUnit { + NANOS(1, "ns"), + MICROS(1000, "us"), + MILLIS(1000 * 1000, "ms"), + SECONDS(1000 * 1000 * 1000, "s"), + MINUTES(1000L * 1000 * 1000 * 60, "min"), + HOURS(1000L * 1000 * 1000 * 60 * 60, "h"), + DAYS(1000L * 1000 * 1000 * 60 * 60 * 24, "d"); + + private final long nanos; + + private final String symbol; + + DurationUnit(long nanos, String symbol) { + this.nanos = nanos; + this.symbol = symbol; + } + + /** + * Returns the unit with the given symbol, or {@code null} if no unit with the given symbol + * exists. + */ + public static @Nullable DurationUnit parse(String symbol) { + switch (symbol) { + case "ns": + return NANOS; + case "us": + return MICROS; + case "ms": + return MILLIS; + case "s": + return SECONDS; + case "min": + return MINUTES; + case "h": + return HOURS; + case "d": + return DAYS; + default: + return null; + } + } + + /** Returns the string symbol of this unit. */ + public String getSymbol() { + return symbol; + } + + /** Returns the conversion factor from this unit to nanoseconds. */ + public long getNanos() { + return nanos; + } + + /** Converts this unit to a {@link java.time.temporal.ChronoUnit}. */ + public ChronoUnit toChronoUnit() { + switch (this) { + case NANOS: + return ChronoUnit.NANOS; + case MICROS: + return ChronoUnit.MICROS; + case MILLIS: + return ChronoUnit.MILLIS; + case SECONDS: + return ChronoUnit.SECONDS; + case MINUTES: + return ChronoUnit.MINUTES; + case HOURS: + return ChronoUnit.HOURS; + case DAYS: + return ChronoUnit.DAYS; + default: + throw new AssertionError("Unknown duration unit: " + this); + } + } + + /** Converts this unit to a {@link java.util.concurrent.TimeUnit}. */ + public TimeUnit toTimeUnit() { + switch (this) { + case NANOS: + return TimeUnit.NANOSECONDS; + case MICROS: + return TimeUnit.MICROSECONDS; + case MILLIS: + return TimeUnit.MILLISECONDS; + case SECONDS: + return TimeUnit.SECONDS; + case MINUTES: + return TimeUnit.MINUTES; + case HOURS: + return TimeUnit.HOURS; + case DAYS: + return TimeUnit.DAYS; + default: + throw new AssertionError("Unknown duration unit: " + this); + } + } + + @Override + public String toString() { + return symbol; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Evaluator.java b/pkl-core/src/main/java/org/pkl/core/Evaluator.java new file mode 100644 index 00000000..93290df4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Evaluator.java @@ -0,0 +1,216 @@ +/** + * 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; + +import java.util.Map; +import org.pkl.core.runtime.TestResults; +import org.pkl.core.runtime.VmEvalException; + +/** + * Evaluates a Pkl module through different modes of evaluation. Throws {@link VmEvalException} if + * an error occurs during evaluation. + * + *

Evaluated modules, and modules imported by them, are cached based on their origin. This is + * important to guarantee consistent evaluation results, for example when the same module is used by + * multiple other modules. To reset the cache, {@link #close()} the current instance and create a + * new one. + * + *

Construct an evaluator through {@link EvaluatorBuilder}. + */ +@SuppressWarnings("unused") +public interface Evaluator extends AutoCloseable { + + /** Shorthand for {@code EvaluatorBuilder.preconfigured().build()}. */ + static Evaluator preconfigured() { + return EvaluatorBuilder.preconfigured().build(); + } + + /** + * Evaluates the module, returning the Java representation of the module object. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + PModule evaluate(ModuleSource moduleSource); + + /** + * Evaluates a module's {@code output.text} property. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + String evaluateOutputText(ModuleSource moduleSource); + + /** + * Evaluates a module's {@code output.value} property. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + Object evaluateOutputValue(ModuleSource moduleSource); + + /** + * Evaluates a module's {@code output.files} property. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + Map evaluateOutputFiles(ModuleSource moduleSource); + + /** + * Evaluates the Pkl expression represented as {@code expression}, returning the Java + * representation of the result. + * + *

The following table describes how Pkl types are represented in Java: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Pkl typeJava type
Null{@link PNull}
String{@link String}
Boolean{@link Boolean}
Int{@link Long}
Float{@link Double}
Typed, Dynamic{@link PObject} ({@link PModule} if the object is a module)
Mapping, Map{@link Map}
Listing, List{@link java.util.List}
Set{@link java.util.Set}
Pair{@link Pair}
Regex{@link java.util.regex.Pattern}
DataSize{@link DataSize}
Duration{@link Duration}
Class{@link PClass}
TypeAlias{@link TypeAlias}
+ * + *

The following Pkl types have no Java representation, and an error is thrown if an expression + * computes to a value of these types: + * + *

    + *
  • IntSeq + *
  • Function + *
+ * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + Object evaluateExpression(ModuleSource moduleSource, String expression); + + /** + * Evaluates the Pkl expression, returning the stringified result. + * + *

This is equivalent to wrapping the expression with {@code .toString()} + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + String evaluateExpressionString(ModuleSource moduleSource, String expression); + + /** + * Evalautes the module's schema, which describes the properties, methods, and classes of a + * module. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + ModuleSchema evaluateSchema(ModuleSource moduleSource); + + /** + * Evaluates the module's {@code output.value} property, and validates that its type matches the + * provided class info. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo classInfo); + + /** + * Runs tests within the module, and returns the test results. + * + *

This requires that the target module be a test module; it must either amend or extend module + * {@code "pkl:test"}. Otherwise, a type mismatch error is thrown. + * + *

This method will write possibly {@code pcf-expected.pkl} and {@code pcf-actual.pcf} files as + * a sibling of the test module. The {@code overwrite} parameter causes the evaluator to overwrite + * {@code pcf-expected.pkl} files if they currently exist. + * + * @throws PklException if an error occurs during evaluation + * @throws IllegalStateException if this evaluator has already been closed + */ + TestResults evaluateTest(ModuleSource moduleSource, boolean overwrite); + + /** + * Releases all resources held by this evaluator. If an {@code evaluate} method is currently + * executing, this method blocks until cancellation of that execution has completed. + * + *

Once an evaluator has been closed, it can no longer be used, and calling {@code evaluate} + * methods will throw {@link IllegalStateException}. However, objects previously returned by + * {@code evaluate} methods remain valid. + */ + @Override + void close(); +} diff --git a/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java b/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java new file mode 100644 index 00000000..f4b59a88 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java @@ -0,0 +1,482 @@ +/** + * 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; + +import com.oracle.truffle.api.TruffleOptions; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Pattern; +import org.pkl.core.SecurityManagers.StandardBuilder; +import org.pkl.core.module.ModuleKeyFactories; +import org.pkl.core.module.ModuleKeyFactory; +import org.pkl.core.module.ModulePathResolver; +import org.pkl.core.project.DeclaredDependencies; +import org.pkl.core.project.Project; +import org.pkl.core.resource.ResourceReader; +import org.pkl.core.resource.ResourceReaders; +import org.pkl.core.runtime.LoggerImpl; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; + +/** A builder for an {@link Evaluator}. Can be reused to build multiple evaluators. */ +@SuppressWarnings({"UnusedReturnValue", "unused"}) +public final class EvaluatorBuilder { + private final StandardBuilder securityManagerBuilder = SecurityManagers.standardBuilder(); + + private @Nullable SecurityManager securityManager; + + private Logger logger = Loggers.noop(); + + private final List moduleKeyFactories = new ArrayList<>(); + + private final List resourceReaders = new ArrayList<>(); + + private final Map environmentVariables = new HashMap<>(); + + private final Map externalProperties = new HashMap<>(); + + private java.time.@Nullable Duration timeout; + + private @Nullable Path moduleCacheDir = IoUtils.getDefaultModuleCacheDir(); + + private @Nullable String outputFormat; + + private @Nullable StackFrameTransformer stackFrameTransformer; + + private @Nullable DeclaredDependencies dependencies; + + private EvaluatorBuilder() {} + + /** + * Creates a builder preconfigured with: + * + *

    + *
  • {@link SecurityManagers#defaultAllowedModules} + *
  • {@link SecurityManagers#defaultAllowedResources} + *
  • {@link Loggers#noop()} + *
  • {@link ModuleKeyFactories#standardLibrary} + *
  • {@link ModuleKeyFactories#classPath} + *
  • {@link ModuleKeyFactories#fromServiceProviders} + *
  • {@link ModuleKeyFactories#file} + *
  • {@link ModuleKeyFactories#pkg} + *
  • {@link ModuleKeyFactories#projectpackage} + *
  • {@link ModuleKeyFactories#genericUrl} + *
  • {@link ResourceReaders#environmentVariable} + *
  • {@link ResourceReaders#externalProperty} + *
  • {@link ResourceReaders#classPath} + *
  • {@link ResourceReaders#file} + *
  • {@link ResourceReaders#http} + *
  • {@link ResourceReaders#https} + *
  • {@link ResourceReaders#pkg} + *
  • {@link ResourceReaders#projectpackage} + *
  • {@link System#getProperties} + *
+ */ + public static EvaluatorBuilder preconfigured() { + EvaluatorBuilder builder = new EvaluatorBuilder(); + + builder + .setStackFrameTransformer(StackFrameTransformers.defaultTransformer) + .setAllowedModules(SecurityManagers.defaultAllowedModules) + .setAllowedResources(SecurityManagers.defaultAllowedResources) + .addResourceReader(ResourceReaders.environmentVariable()) + .addResourceReader(ResourceReaders.externalProperty()) + .addResourceReader(ResourceReaders.file()) + .addResourceReader(ResourceReaders.http()) + .addResourceReader(ResourceReaders.https()) + .addResourceReader(ResourceReaders.pkg()) + .addResourceReader(ResourceReaders.projectpackage()) + .addModuleKeyFactory(ModuleKeyFactories.standardLibrary); + + if (!TruffleOptions.AOT) { + // AOT does not support class loader API + var classLoader = EvaluatorBuilder.class.getClassLoader(); + builder + .addModuleKeyFactory(ModuleKeyFactories.classPath(classLoader)) + .addResourceReader(ResourceReaders.classPath(classLoader)); + + // only add system properties when running on JVM + addSystemProperties(builder); + } + + builder + .addModuleKeyFactories(ModuleKeyFactories.fromServiceProviders()) + .addModuleKeyFactory(ModuleKeyFactories.file) + .addModuleKeyFactory(ModuleKeyFactories.pkg) + .addModuleKeyFactory(ModuleKeyFactories.projectpackage) + .addModuleKeyFactory(ModuleKeyFactories.genericUrl) + .addEnvironmentVariables(System.getenv()); + + return builder; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void addSystemProperties(EvaluatorBuilder builder) { + builder.addExternalProperties((Map) System.getProperties()); + } + + /** + * Creates a builder that is unconfigured. At a minimum, a security manager will need to be set + * before building an instance. + */ + public static EvaluatorBuilder unconfigured() { + return new EvaluatorBuilder(); + } + + /** Sets the given stack frame transformer, replacing any previously set transformer. */ + public EvaluatorBuilder setStackFrameTransformer(StackFrameTransformer stackFrameTransformer) { + this.stackFrameTransformer = stackFrameTransformer; + return this; + } + + /** Returns the currently set stack frame transformer. */ + public @Nullable StackFrameTransformer getStackFrameTransformer() { + return stackFrameTransformer; + } + + /** Sets the given security manager, replacing any previously set security manager. */ + public EvaluatorBuilder setSecurityManager(@Nullable SecurityManager manager) { + this.securityManager = manager; + return this; + } + + public EvaluatorBuilder unsetSecurityManager() { + this.securityManager = null; + return this; + } + + /** Returns the currently set security manager. */ + public @Nullable SecurityManager getSecurityManager() { + return securityManager; + } + + /** + * Sets the set of URI patterns to be allowed when importing modules. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public EvaluatorBuilder setAllowedModules(Collection patterns) { + if (securityManager != null) { + throw new IllegalStateException( + "Cannot call both `setSecurityManager` and `setAllowedModules`, because both define security manager settings."); + } + securityManagerBuilder.setAllowedModules(patterns); + return this; + } + + /** Returns the set of patterns to be allowed when importing modules. */ + public List getAllowedModules() { + return securityManagerBuilder.getAllowedModules(); + } + + /** + * Sets the set of URI patterns to be allowed when reading resources. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public EvaluatorBuilder setAllowedResources(Collection patterns) { + if (securityManager != null) { + throw new IllegalStateException( + "Cannot call both `setSecurityManager` and `setAllowedResources`, because both define security manager settings."); + } + securityManagerBuilder.setAllowedResources(patterns); + return this; + } + + /** Returns the set of patterns to be allowed when reading resources. */ + public List getAllowedResources() { + return securityManagerBuilder.getAllowedResources(); + } + + /** + * Sets the root directory, which restricts access to file-based modules and resources located + * under this directory. + */ + public EvaluatorBuilder setRootDir(@Nullable Path rootDir) { + securityManagerBuilder.setRootDir(rootDir); + return this; + } + + /** Returns the currently set root directory, if set. */ + public @Nullable Path getRootDir() { + return securityManagerBuilder.getRootDir(); + } + + /** Sets the given logger, replacing any previously set logger. */ + public EvaluatorBuilder setLogger(Logger logger) { + this.logger = logger; + return this; + } + + /** Returns the currently set logger. */ + public Logger getLogger() { + return logger; + } + + /** + * Adds the given module key factory. Factories will be asked to resolve module keys in the order + * they have been added to this builder. + */ + public EvaluatorBuilder addModuleKeyFactory(ModuleKeyFactory factory) { + moduleKeyFactories.add(factory); + return this; + } + + /** + * Adds the given module key factories. Factories will be asked to resolve module keys in the + * order they have been added to this builder. + */ + public EvaluatorBuilder addModuleKeyFactories(Collection factories) { + moduleKeyFactories.addAll(factories); + return this; + } + + /** Removes any existing module key factories, then adds the given factories. */ + public EvaluatorBuilder setModuleKeyFactories(Collection factories) { + moduleKeyFactories.clear(); + return addModuleKeyFactories(factories); + } + + /** Returns the currently set module key factories. */ + public List getModuleKeyFactories() { + return moduleKeyFactories; + } + + public EvaluatorBuilder addResourceReader(ResourceReader reader) { + resourceReaders.add(reader); + return this; + } + + public EvaluatorBuilder addResourceReaders(Collection readers) { + resourceReaders.addAll(readers); + return this; + } + + public EvaluatorBuilder setResourceReaders(Collection readers) { + resourceReaders.clear(); + return addResourceReaders(readers); + } + + /** Returns the currently set resource readers. */ + public List getResourceReaders() { + return resourceReaders; + } + + /** + * Adds the given environment variable, overriding any environment variable previously added under + * the same name. + * + *

Pkl code can read environment variables with {@code read("env:")}. + */ + public EvaluatorBuilder addEnvironmentVariable(String name, String value) { + environmentVariables.put(name, value); + return this; + } + + /** + * Adds the given environment variables, overriding any environment variables previously added + * under the same name. + * + *

Pkl code can read environment variables with {@code read("env:")}. + */ + public EvaluatorBuilder addEnvironmentVariables(Map envVars) { + environmentVariables.putAll(envVars); + return this; + } + + /** Removes any existing environment variables, then adds the given environment variables. */ + public EvaluatorBuilder setEnvironmentVariables(Map envVars) { + environmentVariables.clear(); + return addEnvironmentVariables(envVars); + } + + /** Returns the currently set environment variables. */ + public Map getEnvironmentVariables() { + return environmentVariables; + } + + /** + * Adds the given external property, overriding any property previously set under the same name. + * + *

Pkl code can read external properties with {@code read("prop:")}. + */ + public EvaluatorBuilder addExternalProperty(String name, String value) { + externalProperties.put(name, value); + return this; + } + + /** + * Adds the given external properties, overriding any properties previously set under the same + * name. + * + *

Pkl code can read external properties with {@code read("prop:")}. + */ + public EvaluatorBuilder addExternalProperties(Map properties) { + externalProperties.putAll(properties); + return this; + } + + /** Removes any existing external properties, then adds the given properties. */ + public EvaluatorBuilder setExternalProperties(Map properties) { + externalProperties.clear(); + return addExternalProperties(properties); + } + + /** Returns the currently set external properties. */ + public Map getExternalProperties() { + return externalProperties; + } + + /** + * Sets an evaluation timeout to be enforced by the {@link Evaluator}'s {@code evaluate} methods. + */ + public EvaluatorBuilder setTimeout(java.time.@Nullable Duration timeout) { + this.timeout = timeout; + return this; + } + + /** Returns the currently set evaluation timeout. */ + public java.time.@Nullable Duration getTimeout() { + return timeout; + } + + /** + * Sets the directory where `package:` modules are cached. + * + *

If {@code null}, the module cache is disabled. + */ + public EvaluatorBuilder setModuleCacheDir(@Nullable Path moduleCacheDir) { + this.moduleCacheDir = moduleCacheDir; + return this; + } + + /** + * Returns the directory where `package:` modules are cached. If {@code null}, the module cache is + * disabled. + */ + public @Nullable Path getModuleCacheDir() { + return moduleCacheDir; + } + + /** + * Sets the desired output format, if any. + * + *

By default, modules support the formats described by {@link OutputFormat}. and fall back to + * {@link OutputFormat#PCF} if no format is specified. + * + *

Modules that override {@code output.renderer} in their source code may ignore this option or + * may support formats other than those described by {@link OutputFormat}. In particular, most + * templates ignore this option and always render the same format. + */ + public EvaluatorBuilder setOutputFormat(@Nullable String outputFormat) { + this.outputFormat = outputFormat; + return this; + } + + /** + * Sets the desired output format, if any. + * + *

By default, modules support the formats described by {@link OutputFormat}. and fall back to + * {@link OutputFormat#PCF} if no format is specified. + * + *

Modules that override {@code output.renderer} in their source code may ignore this option or + * may support formats other than those described by {@link OutputFormat}. In particular, most + * templates ignore this option and always render the same format. + */ + public EvaluatorBuilder setOutputFormat(@Nullable OutputFormat outputFormat) { + this.outputFormat = outputFormat == null ? null : outputFormat.toString(); + return this; + } + + /** Returns the currently set output format, if any. */ + public @Nullable String getOutputFormat() { + return outputFormat; + } + + /** Sets the project dependencies for the evaluator. */ + public EvaluatorBuilder setProjectDependencies(DeclaredDependencies dependencies) { + this.dependencies = dependencies; + return this; + } + + /** + * Given a project, sets its dependencies, and also applies any evaluator settings if set. + * + * @throws IllegalStateException if {@link #setSecurityManager(SecurityManager)} was also called. + */ + public EvaluatorBuilder applyFromProject(Project project) { + this.dependencies = project.getDependencies(); + var settings = project.getSettings(); + if (securityManager != null) { + throw new IllegalStateException( + "Cannot call both `setSecurityManager` and `setProject`, because both define security manager settings. Call `setProjectOnly` if the security manager is desired."); + } + if (settings.getAllowedModules() != null) { + setAllowedModules(settings.getAllowedModules()); + } + if (settings.getAllowedResources() != null) { + setAllowedResources(settings.getAllowedResources()); + } + if (settings.getExternalProperties() != null) { + setExternalProperties(settings.getExternalProperties()); + } + if (settings.getEnv() != null) { + setEnvironmentVariables(settings.getEnv()); + } + if (settings.getTimeout() != null) { + setTimeout(settings.getTimeout().toJavaDuration()); + } + if (settings.getModulePath() != null) { + // indirectly closed by `ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)` + var modulePathResolver = new ModulePathResolver(settings.getModulePath()); + addResourceReader(ResourceReaders.modulePath(modulePathResolver)); + addModuleKeyFactory(ModuleKeyFactories.modulePath(modulePathResolver)); + } + if (settings.getRootDir() != null) { + setRootDir(settings.getRootDir()); + } + if (Boolean.TRUE.equals(settings.isNoCache())) { + setModuleCacheDir(null); + } else if (settings.getModuleCacheDir() != null) { + setModuleCacheDir(settings.getModuleCacheDir()); + } + return this; + } + + public Evaluator build() { + if (securityManager == null) { + securityManager = securityManagerBuilder.build(); + } + + if (stackFrameTransformer == null) { + throw new IllegalStateException("No stack frame transformer set."); + } + + return new EvaluatorImpl( + stackFrameTransformer, + securityManager, + new LoggerImpl(logger, stackFrameTransformer), + // copy to shield against subsequent modification through builder + new ArrayList<>(moduleKeyFactories), + new ArrayList<>(resourceReaders), + new HashMap<>(environmentVariables), + new HashMap<>(externalProperties), + timeout, + moduleCacheDir, + dependencies, + outputFormat); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java new file mode 100644 index 00000000..b2247e38 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java @@ -0,0 +1,439 @@ +/** + * 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; + +import com.oracle.truffle.api.TruffleStackTrace; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; +import org.graalvm.polyglot.Context; +import org.pkl.core.ast.ConstantValueNode; +import org.pkl.core.ast.internal.ToStringNodeGen; +import org.pkl.core.module.ModuleKeyFactory; +import org.pkl.core.module.ProjectDependenciesManager; +import org.pkl.core.packages.PackageResolver; +import org.pkl.core.project.DeclaredDependencies; +import org.pkl.core.resource.ResourceReader; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.ModuleResolver; +import org.pkl.core.runtime.ResourceManager; +import org.pkl.core.runtime.TestResults; +import org.pkl.core.runtime.TestRunner; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.runtime.VmException; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmMapping; +import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmStackOverflowException; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.runtime.VmValue; +import org.pkl.core.runtime.VmValueRenderer; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.Nullable; + +public class EvaluatorImpl implements Evaluator { + protected final StackFrameTransformer frameTransformer; + protected final ModuleResolver moduleResolver; + protected final Context polyglotContext; + protected final @Nullable Duration timeout; + protected final @Nullable ScheduledExecutorService timeoutExecutor; + protected final SecurityManager securityManager; + protected final BufferedLogger logger; + protected final PackageResolver packageResolver; + private final VmValueRenderer vmValueRenderer = VmValueRenderer.singleLine(1000); + + public EvaluatorImpl( + StackFrameTransformer transformer, + SecurityManager manager, + Logger logger, + Collection factories, + Collection readers, + Map environmentVariables, + Map externalProperties, + @Nullable Duration timeout, + @Nullable Path moduleCacheDir, + @Nullable DeclaredDependencies projectDependencies, + @Nullable String outputFormat) { + + securityManager = manager; + frameTransformer = transformer; + moduleResolver = new ModuleResolver(factories); + this.logger = new BufferedLogger(logger); + packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir); + polyglotContext = + VmUtils.createContext( + () -> { + VmContext vmContext = VmContext.get(null); + vmContext.initialize( + new VmContext.Holder( + transformer, + manager, + moduleResolver, + new ResourceManager(manager, readers), + this.logger, + environmentVariables, + externalProperties, + moduleCacheDir, + outputFormat, + packageResolver, + projectDependencies == null + ? null + : new ProjectDependenciesManager(projectDependencies))); + }); + this.timeout = timeout; + // NOTE: would probably make sense to share executor between evaluators + // (blocked on https://github.com/oracle/graal/issues/1230) + timeoutExecutor = + timeout == null + ? null + : Executors.newSingleThreadScheduledExecutor( + runnable -> { + Thread t = new Thread(runnable, "Pkl Timeout Scheduler"); + t.setDaemon(true); + return t; + }); + } + + @Override + public PModule evaluate(ModuleSource moduleSource) { + return doEvaluate( + moduleSource, + (module) -> { + module.force(false); + return (PModule) module.export(); + }); + } + + @Override + public String evaluateOutputText(ModuleSource moduleSource) { + return doEvaluate( + moduleSource, + (module) -> { + var output = (VmTyped) VmUtils.readMember(module, Identifier.OUTPUT); + return VmUtils.readTextProperty(output); + }); + } + + @Override + public Object evaluateOutputValue(ModuleSource moduleSource) { + return doEvaluate( + moduleSource, + (module) -> { + var output = (VmTyped) VmUtils.readMember(module, Identifier.OUTPUT); + var value = VmUtils.readMember(output, Identifier.VALUE); + if (value instanceof VmValue) { + var vmValue = (VmValue) value; + vmValue.force(false); + return vmValue.export(); + } + return value; + }); + } + + @Override + public Map evaluateOutputFiles(ModuleSource moduleSource) { + return doEvaluate( + moduleSource, + (module) -> { + var output = (VmTyped) VmUtils.readMember(module, Identifier.OUTPUT); + var filesOrNull = VmUtils.readMember(output, Identifier.FILES); + if (filesOrNull instanceof VmNull) { + return Map.of(); + } + var files = (VmMapping) filesOrNull; + var result = new LinkedHashMap(); + files.forceAndIterateMemberValues( + (key, member, value) -> { + assert member.isEntry(); + result.put((String) key, new FileOutputImpl(this, (VmTyped) value)); + return true; + }); + return result; + }); + } + + @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) { + var value = (VmValue) expressionResult; + 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. + if (expression.equals("output.text")) { + return evaluateOutputText(moduleSource); + } + return doEvaluate( + moduleSource, + (module) -> { + var expressionResult = + VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver); + var toStringNode = + ToStringNodeGen.create( + VmUtils.unavailableSourceSection(), new ConstantValueNode(expressionResult)); + var stringified = toStringNode.executeGeneric(VmUtils.createEmptyMaterializedFrame()); + return (String) stringified; + }); + } + + @Override + public ModuleSchema evaluateSchema(ModuleSource moduleSource) { + return doEvaluate(moduleSource, (module) -> module.getModuleInfo().getModuleSchema(module)); + } + + @Override + public TestResults evaluateTest(ModuleSource moduleSource, boolean overwrite) { + return doEvaluate( + moduleSource, + (module) -> { + var testRunner = new TestRunner(logger, frameTransformer, overwrite); + return testRunner.run(module); + }); + } + + @Override + public T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo classInfo) { + return doEvaluate( + moduleSource, + (module) -> { + var output = (VmTyped) VmUtils.readMember(module, Identifier.OUTPUT); + var value = VmUtils.readMember(output, Identifier.VALUE); + var valueClassInfo = VmUtils.getClass(value).getPClassInfo(); + if (valueClassInfo.equals(classInfo)) { + if (value instanceof VmValue) { + var vmValue = (VmValue) value; + vmValue.force(false); + //noinspection unchecked + return (T) vmValue.export(); + } + //noinspection unchecked + return (T) value; + } + throw moduleOutputValueTypeMismatch(module, classInfo, value, output); + }); + } + + @Override + public void close() { + // if currently executing, blocks until cancellation has completed (see + // https://github.com/oracle/graal/issues/1230) + polyglotContext.close(true); + try { + packageResolver.close(); + } catch (IOException ignored) { + } + + if (timeoutExecutor != null) { + timeoutExecutor.shutdown(); + } + } + + String evaluateOutputText(VmTyped fileOutput) { + return doEvaluate(() -> VmUtils.readTextProperty(fileOutput)); + } + + private T doEvaluate(Supplier supplier) { + @Nullable TimeoutTask timeoutTask = null; + logger.clear(); + if (timeout != null) { + assert timeoutExecutor != null; + timeoutTask = new TimeoutTask(); + timeoutExecutor.schedule(timeoutTask, timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + polyglotContext.enter(); + T evalResult; + // There is a chance that a timeout is triggered just when evaluation completes on its own. + // In this case, if evaluation completed normally or with an expected exception (VmException), + // nevertheless report the timeout. (For technical reasons, timeout implies that evaluator is + // being closed, + // which needs to be communicated to the client.) + // If evaluation completed with an unexpected exception (translated to PklBugException) or + // error, + // report that instead of the timeout so as not to swallow a fundamental problem. + try { + evalResult = supplier.get(); + } catch (VmStackOverflowException e) { + if (isPklBug(e)) { + throw new VmExceptionBuilder() + .bug("Stack overflow") + .withCause(e.getCause()) + .build() + .toPklException(frameTransformer); + } + handleTimeout(timeoutTask); + throw e.toPklException(frameTransformer); + } catch (VmException e) { + handleTimeout(timeoutTask); + throw e.toPklException(frameTransformer); + } catch (Exception e) { + throw new PklBugException(e); + } catch (ExceptionInInitializerError e) { + if (!(e.getCause() instanceof VmException)) { + throw new PklBugException(e); + } + var pklException = ((VmException) e.getCause()).toPklException(frameTransformer); + var error = new ExceptionInInitializerError(pklException); + error.setStackTrace(e.getStackTrace()); + throw new PklBugException(error); + } catch (ThreadDeath e) { + if (e.getClass() + .getName() + .equals("com.oracle.truffle.polyglot.PolyglotEngineImpl$CancelExecution")) { + // Truffle cancelled evaluation in response to polyglotContext.close(true) triggered by + // TimeoutTask + handleTimeout(timeoutTask); + throw PklBugException.unreachableCode(); + } else { + throw e; + } + } finally { + try { + polyglotContext.leave(); + } catch (IllegalStateException ignored) { + // happens if evaluation has already been cancelled with polyglotContext.close(true) + } + } + + handleTimeout(timeoutTask); + return evalResult; + } + + protected T doEvaluate(ModuleSource moduleSource, Function doEvaluate) { + return doEvaluate( + () -> { + var moduleKey = moduleResolver.resolve(moduleSource); + var module = VmLanguage.get(null).loadModule(moduleKey); + return doEvaluate.apply(module); + }); + } + + private void handleTimeout(@Nullable TimeoutTask timeoutTask) { + if (timeoutTask == null || timeoutTask.cancel()) return; + + assert timeout != null; + // TODO: use a different exception type so that clients can tell apart timeouts from other + // errors + throw new PklException( + ErrorMessages.create( + "evaluationTimedOut", (timeout.getSeconds() + timeout.getNano() / 1_000_000_000d))); + } + + private VmException moduleOutputValueTypeMismatch( + VmTyped module, PClassInfo expectedClassInfo, Object value, VmTyped output) { + var moduleUri = module.getModuleInfo().getModuleKey().getUri(); + var builder = + new VmExceptionBuilder() + .evalError( + "invalidModuleOutputValue", + expectedClassInfo.getDisplayName(), + VmUtils.getClass(value).getPClassInfo().getDisplayName(), + moduleUri); + var outputValueMember = output.getMember(Identifier.VALUE); + assert outputValueMember != null; + var uriOfValueMember = outputValueMember.getSourceSection().getSource().getURI(); + // If `value` was explicitly re-assigned, show that in the stack trace. + // Otherwise, show the module header. + if (!uriOfValueMember.equals(PClassInfo.pklBaseUri)) { + return builder + .withSourceSection(outputValueMember.getBodySection()) + .withMemberName("value") + .build(); + } else { + // if the module does not extend or amend anything, suggest amending the module URI + if (module.getParent() != null + && module.getParent().getVmClass().equals(BaseModule.getModuleClass()) + && expectedClassInfo.isModuleClass()) { + builder.withHint( + String.format( + "Try adding `amends %s` to the module header.", + vmValueRenderer.render(expectedClassInfo.getModuleUri().toString()))); + } + return builder + .withSourceSection(module.getModuleInfo().getHeaderSection()) + .withMemberName(module.getModuleInfo().getModuleName()) + .build(); + } + } + + private boolean isPklBug(VmStackOverflowException e) { + // There's no good way to tell if a StackOverflowError came from Pkl, or from our + // implementation. + // This is a simple heuristic; it's pretty likely that any stack overflow error that occurs + // if there's less than 100 truffle frames is due to our own doing. + var truffleStackTraceElements = TruffleStackTrace.getStackTrace(e); + return truffleStackTraceElements != null && truffleStackTraceElements.size() < 100; + } + + // ScheduledFuture.cancel() is problematic, so let's handle cancellation on our own + private final class TimeoutTask implements Runnable { + // both fields guarded by synchronizing on `this` + private boolean started = false; + private boolean cancelled = false; + + @Override + public void run() { + synchronized (this) { + if (cancelled) return; + + started = true; + } + + // may take a while + close(); + } + + /** Returns `true` if this task was successfully cancelled before it had started. */ + public synchronized boolean cancel() { + if (started) return false; + + cancelled = true; + return true; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/FileOutput.java b/pkl-core/src/main/java/org/pkl/core/FileOutput.java new file mode 100644 index 00000000..3daca6cf --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/FileOutput.java @@ -0,0 +1,26 @@ +/** + * 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; + +/** Java representation of {@code pkl.base#FileOutput}. */ +public interface FileOutput { + /** + * Returns the text content of this file. + * + * @throws PklException if an error occurs during evaluation + */ + String getText(); +} diff --git a/pkl-core/src/main/java/org/pkl/core/FileOutputImpl.java b/pkl-core/src/main/java/org/pkl/core/FileOutputImpl.java new file mode 100644 index 00000000..32db1520 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/FileOutputImpl.java @@ -0,0 +1,47 @@ +/** + * 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; + +import org.graalvm.polyglot.PolyglotException; +import org.pkl.core.runtime.VmTyped; + +final class FileOutputImpl implements FileOutput { + private final VmTyped fileOutput; + private final EvaluatorImpl evaluator; + + FileOutputImpl(EvaluatorImpl evaluator, VmTyped fileOutput) { + this.evaluator = evaluator; + this.fileOutput = fileOutput; + } + + /** + * Evaluates the text output of this file. + * + *

Will throw {@link PklException} if a normal evaluator error occurs. + * + *

If the evaluator that produced this {@link FileOutput} is closed, an error will be thrown. + */ + public String getText() { + try { + return evaluator.evaluateOutputText(fileOutput); + } catch (PolyglotException e) { + if (e.isCancelled()) { + throw new PklException("The evaluator is no longer available", e); + } + throw new PklBugException(e); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/JsonRenderer.java b/pkl-core/src/main/java/org/pkl/core/JsonRenderer.java new file mode 100644 index 00000000..0b29f4c1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/JsonRenderer.java @@ -0,0 +1,212 @@ +/** + * 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; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.pkl.core.util.json.JsonWriter; + +final class JsonRenderer implements ValueRenderer { + private final JsonWriter writer; + private final boolean omitNullProperties; + + public JsonRenderer(Writer writer, String indent, boolean omitNullProperties) { + this.writer = new JsonWriter(writer); + this.writer.setIndent(indent); + this.omitNullProperties = omitNullProperties; + } + + @Override + public void renderDocument(Object value) { + // JSON document can have any top-level value + // https://stackoverflow.com/a/3833312 + // http://www.ietf.org/rfc/rfc7159.txt + renderValue(value); + try { + writer.newline(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void renderValue(Object value) { + new Visitor().visit(value); + } + + private class Visitor implements ValueVisitor { + @Override + public void visitString(String value) { + try { + writer.value(value); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void visitInt(Long value) { + try { + writer.value(value); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void visitFloat(Double value) { + try { + writer.value(value); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void visitBoolean(Boolean value) { + try { + writer.value(value); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void visitDuration(Duration value) { + throw new RendererException( + String.format("Values of type `Duration` cannot be rendered as JSON. Value: %s", value)); + } + + @Override + public void visitDataSize(DataSize value) { + throw new RendererException( + String.format("Values of type `DataSize` cannot be rendered as JSON. Value: %s", value)); + } + + @Override + public void visitPair(Pair value) { + try { + writer.beginArray(); + visit(value.getFirst()); + visit(value.getSecond()); + writer.endArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void visitList(List value) { + doVisitCollection(value); + } + + @Override + public void visitSet(Set value) { + doVisitCollection(value); + } + + @Override + public void visitMap(Map value) { + for (var key : value.keySet()) { + if (!(key instanceof String)) { + throw new RendererException( + String.format( + "Maps containing non-String keys cannot be rendered as JSON. Key: %s", key)); + } + } + + @SuppressWarnings("unchecked") + var mapValue = (Map) value; + doVisitProperties(mapValue); + } + + @Override + public void visitObject(PObject value) { + doVisitProperties(value.getProperties()); + } + + @Override + public void visitModule(PModule value) { + doVisitProperties(value.getProperties()); + } + + @Override + public void visitNull() { + try { + writer.nullValue(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void visitClass(PClass value) { + throw new RendererException( + String.format( + "Values of type `Class` cannot be rendered as JSON. Value: %s", + value.getSimpleName())); + } + + @Override + public void visitTypeAlias(TypeAlias value) { + throw new RendererException( + String.format( + "Values of type `TypeAlias` cannot be rendered as JSON. Value: %s", + value.getSimpleName())); + } + + @Override + public void visitRegex(Pattern value) { + throw new RendererException( + String.format("Values of type `Regex` cannot be rendered as JSON. Value: %s", value)); + } + + private void doVisitCollection(Collection collection) { + try { + writer.beginArray(); + for (var elem : collection) visit(elem); + writer.endArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void doVisitProperties(Map properties) { + try { + writer.beginObject(); + + for (var entry : properties.entrySet()) { + var value = entry.getValue(); + + if (omitNullProperties && value instanceof PNull) continue; + + writer.name(entry.getKey()); + visit(value); + } + + writer.endObject(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Logger.java b/pkl-core/src/main/java/org/pkl/core/Logger.java new file mode 100644 index 00000000..46367b1c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Logger.java @@ -0,0 +1,29 @@ +/** + * 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; + +/** + * SPI for log messages emitted by the Pkl evaluator. Use {@link EvaluatorBuilder#setLogger} to set + * a logger. See {@link Loggers} for predefined loggers. + */ +@SuppressWarnings("unused") +public interface Logger { + /** Logs the given message on level TRACE. */ + default void trace(String message, StackFrame frame) {} + + /** Logs the given message on level WARN. */ + default void warn(String message, StackFrame frame) {} +} diff --git a/pkl-core/src/main/java/org/pkl/core/Loggers.java b/pkl-core/src/main/java/org/pkl/core/Loggers.java new file mode 100644 index 00000000..a9c6b3c7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Loggers.java @@ -0,0 +1,83 @@ +/** + * 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; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** Predefined {@link Logger}s. */ +@SuppressWarnings("unused") +public final class Loggers { + /** Returns a logger that discards log messages. */ + public static Logger noop() { + return new Logger() { + @Override + public void trace(String message, StackFrame frame) { + // do nothing + } + + @Override + public void warn(String message, StackFrame frame) { + // do nothing + } + }; + } + + /** Returns a logger that sends log messages to standard error. */ + public static Logger stdErr() { + return stream(System.err); + } + + /** Returns a logger that sends log messages to the given stream. */ + @SuppressWarnings("DuplicatedCode") + public static Logger stream(PrintStream stream) { + return new Logger() { + @Override + public void trace(String message, StackFrame frame) { + stream.println(formatMessage("TRACE", message, frame)); + stream.flush(); + } + + @Override + public void warn(String message, StackFrame frame) { + stream.println(formatMessage("WARN", message, frame)); + stream.flush(); + } + }; + } + + /** Returns a logger that sends log messages to the given writer. */ + @SuppressWarnings("DuplicatedCode") + public static Logger writer(PrintWriter writer) { + return new Logger() { + @Override + public void trace(String message, StackFrame frame) { + writer.println(formatMessage("TRACE", message, frame)); + writer.flush(); + } + + @Override + public void warn(String message, StackFrame frame) { + writer.println(formatMessage("WARN", message, frame)); + writer.flush(); + } + }; + } + + private static String formatMessage(String level, String message, StackFrame frame) { + return "pkl: " + level + ": " + message + " (" + frame.getModuleUri() + ')'; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Member.java b/pkl-core/src/main/java/org/pkl/core/Member.java new file mode 100644 index 00000000..2e67cc0d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Member.java @@ -0,0 +1,121 @@ +/** + * 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; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.pkl.core.util.Nullable; + +/** Common base class for TypeAlias, PClass, PClass.Property, and PClass.Method. */ +public abstract class Member implements Serializable { + + private static final long serialVersionUID = 0L; + + private final @Nullable String docComment; + private final SourceLocation sourceLocation; + private final Set modifiers; + private final List annotations; + private final String simpleName; + + public Member( + @Nullable String docComment, + SourceLocation sourceLocation, + Set modifiers, + List annotations, + String simpleName) { + this.docComment = docComment; + this.sourceLocation = Objects.requireNonNull(sourceLocation, "sourceLocation"); + this.modifiers = Objects.requireNonNull(modifiers, "modifiers"); + this.annotations = Objects.requireNonNull(annotations, "annotations"); + this.simpleName = Objects.requireNonNull(simpleName, "simpleName"); + } + + /** Returns the name of the module that this member is declared in. */ + public abstract String getModuleName(); + + /** Returns the documentation comment of this member. */ + public @Nullable String getDocComment() { + return docComment; + } + + /** Returns the source location, such as start and end line, of this member. */ + public SourceLocation getSourceLocation() { + return sourceLocation; + } + + /** Returns the modifiers of this member. */ + public Set getModifiers() { + return modifiers; + } + + /** Returns the annotations of this member. */ + public List getAnnotations() { + return annotations; + } + + /** Tells if this member has an {@code external} modifier. */ + public boolean isExternal() { + return modifiers.contains(Modifier.EXTERNAL); + } + + /** Tells if this member has an {@code abstract} modifier. */ + public boolean isAbstract() { + return modifiers.contains(Modifier.ABSTRACT); + } + + /** Tells if this member has a {@code hidden} modifier. */ + public boolean isHidden() { + return modifiers.contains(Modifier.HIDDEN); + } + + /** Tells if this member has an {@code open} modifier. */ + public boolean isOpen() { + return modifiers.contains(Modifier.OPEN); + } + + /** Tells if this member is defined in Pkl's standard library. */ + public final boolean isStandardLibraryMember() { + return getModuleName().startsWith("pkl."); + } + + /** Returns the unqualified name of this member. */ + public String getSimpleName() { + return simpleName; + } + + public static class SourceLocation implements Serializable { + + private static final long serialVersionUID = 0L; + + private final int startLine; + private final int endLine; + + public SourceLocation(int startLine, int endLine) { + this.startLine = startLine; + this.endLine = endLine; + } + + public int getStartLine() { + return startLine; + } + + public int getEndLine() { + return endLine; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Modifier.java b/pkl-core/src/main/java/org/pkl/core/Modifier.java new file mode 100644 index 00000000..70a06f72 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Modifier.java @@ -0,0 +1,34 @@ +/** + * 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; + +public enum Modifier { + ABSTRACT("abstract"), + OPEN("open"), + HIDDEN("hidden"), + EXTERNAL("external"); + + private final String displayName; + + Modifier(String displayName) { + this.displayName = displayName; + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ModuleSchema.java b/pkl-core/src/main/java/org/pkl/core/ModuleSchema.java new file mode 100644 index 00000000..45471e66 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ModuleSchema.java @@ -0,0 +1,182 @@ +/** + * 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; + +import java.net.URI; +import java.util.*; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +/** Describes the property, method and class members of a module. */ +public final class ModuleSchema { + private final URI moduleUri; + private final String moduleName; + private final boolean isAmend; + private final @Nullable ModuleSchema supermodule; + private final PClass moduleClass; + private final @Nullable String docComment; + private final List annotations; + private final Map classes; + private final Map typeAliases; + private final Map imports; + + @LateInit private Map __allClasses; + @LateInit private Map __allTypeAliases; + + /** Constructs a {@code ModuleSchema} instance. */ + public ModuleSchema( + URI moduleUri, + String moduleName, + boolean isAmend, + @Nullable ModuleSchema supermodule, + PClass moduleClass, + @Nullable String docComment, + List annotations, + Map classes, + Map typeAliases, + Map imports) { + this.moduleUri = moduleUri; + this.moduleName = moduleName; + this.isAmend = isAmend; + this.supermodule = supermodule; + this.moduleClass = moduleClass; + this.docComment = docComment; + this.annotations = annotations; + this.classes = classes; + this.typeAliases = typeAliases; + this.imports = imports; + } + + /** Returns the absolute URI from which this module was first loaded. */ + public URI getModuleUri() { + return moduleUri; + } + + /** + * Returns the name of this module. + * + *

Note that module names are not guaranteed to be unique, especially if they are not declared + * but inferred from the module URI. + */ + public String getModuleName() { + return moduleName; + } + + /** + * Returns the last name part of a dot-separated {@link #getModuleName}, or the entire {@link + * #getModuleName} if it is not dot-separated. + */ + public String getShortModuleName() { + var index = moduleName.lastIndexOf('.'); + return moduleName.substring(index + 1); + } + + /** + * Returns this module's supermodule, or {@code null} if this module does not amend or extend + * another module. + */ + public @Nullable ModuleSchema getSupermodule() { + return supermodule; + } + + /** Tells if this module amends a module (namely {@link #getSupermodule()}). */ + public boolean isAmend() { + return isAmend; + } + + /** Tells if this module extends a module (namely {@link #getSupermodule()}). */ + public boolean isExtend() { + return supermodule != null && !isAmend; + } + + /** Returns the doc comment of this module (if any). */ + public @Nullable String getDocComment() { + return docComment; + } + + /** Returns the annotations of this module. */ + public List getAnnotations() { + return annotations; + } + + /** + * Returns the class of this module, which describes the properties and methods defined in this + * module. + */ + public PClass getModuleClass() { + return moduleClass; + } + + /** + * Returns the imports declared in this module. + * + *

Map keys are the identifiers by which imports are accessed within this module. Map values + * are the URIs of the imported modules. + * + *

Does not cover import expressions. + */ + public Map getImports() { + return imports; + } + + /** Returns the classes defined in this module in declaration order. */ + public Map getClasses() { + return classes; + } + + /** + * Returns all classes defined in this module and its supermodules in declaration order. + * Supermodule classes are ordered before submodule classes. + */ + public Map getAllClasses() { + if (__allClasses == null) { + if (supermodule == null) { + __allClasses = classes; + } else if (classes.isEmpty()) { + __allClasses = supermodule.getAllClasses(); + } else { + __allClasses = new LinkedHashMap<>(); + __allClasses.putAll(supermodule.getAllClasses()); + __allClasses.putAll(classes); + } + } + return __allClasses; + } + + /** Returns the type aliases defined in this module in declaration order. */ + public Map getTypeAliases() { + return typeAliases; + } + + /** + * Returns all type aliases defined in this module and its supermodules in declaration order. + * Supermodule type aliases are ordered before submodule type aliases. + */ + public Map getAllTypeAliases() { + if (__allTypeAliases == null) { + if (supermodule == null) { + __allTypeAliases = typeAliases; + } else if (typeAliases.isEmpty()) { + __allTypeAliases = supermodule.getAllTypeAliases(); + } else { + __allTypeAliases = new LinkedHashMap<>(); + __allTypeAliases.putAll(supermodule.getAllTypeAliases()); + __allTypeAliases.putAll(typeAliases); + } + } + return __allTypeAliases; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ModuleSource.java b/pkl-core/src/main/java/org/pkl/core/ModuleSource.java new file mode 100644 index 00000000..574899a2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ModuleSource.java @@ -0,0 +1,91 @@ +/** + * 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; + +import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; + +/** + * A representation for a Pkl module's source URI, and optionally its source text. + * + *

Create a new module source via {@link #create(URI, String)}, or one of the various helper + * factory methods. + */ +public class ModuleSource { + public static ModuleSource create(URI uri, @Nullable String text) { + return new ModuleSource(uri, text); + } + + public static ModuleSource path(Path path) { + return new ModuleSource(path.toUri(), null); + } + + public static ModuleSource path(String path) { + return path(Path.of(path)); + } + + public static ModuleSource text(String text) { + return new ModuleSource(VmUtils.REPL_TEXT_URI, text); + } + + public static ModuleSource file(String file) { + return file(new File(file)); + } + + public static ModuleSource file(File file) { + // File.toPath.toUri() gives more complaint file URIs + // than File.toUri() (file:/// vs. file:/ for local files) + return new ModuleSource(file.toPath().toUri(), null); + } + + public static ModuleSource uri(String uri) { + return uri(URI.create(uri)); + } + + public static ModuleSource uri(URI uri) { + return new ModuleSource(uri, null); + } + + public static ModuleSource modulePath(String path) { + URI uri; + if (path.charAt(0) == '/') { + uri = URI.create("modulepath:" + path); + } else { + uri = URI.create("modulepath:/" + path); + } + return uri(uri); + } + + private final URI uri; + + @Nullable private final String contents; + + private ModuleSource(URI uri, @Nullable String contents) { + this.uri = uri; + this.contents = contents; + } + + public URI getUri() { + return uri; + } + + public @Nullable String getContents() { + return contents; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/NoSuchPropertyException.java b/pkl-core/src/main/java/org/pkl/core/NoSuchPropertyException.java new file mode 100644 index 00000000..bbff99d1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/NoSuchPropertyException.java @@ -0,0 +1,33 @@ +/** + * 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; + +/** + * Indicates that a non-existent property was requested for a {@link Composite}. To check if a + * property exists, use {@link Composite#hasProperty(String)}. + */ +public final class NoSuchPropertyException extends RuntimeException { + private final String propertyName; + + public NoSuchPropertyException(String message, String propertyName) { + super(message); + this.propertyName = propertyName; + } + + public String getPropertyName() { + return propertyName; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/OutputFormat.java b/pkl-core/src/main/java/org/pkl/core/OutputFormat.java new file mode 100644 index 00000000..8ed1e110 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/OutputFormat.java @@ -0,0 +1,40 @@ +/** + * 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; + +/** The output formats that Pkl supports out-of-the-box. */ +public enum OutputFormat { + JSON("json"), + JSONNET("jsonnet"), + PCF("pcf"), + PROPERTIES("properties"), + PLIST("plist"), + TEXTPROTO("textproto"), + XML("xml"), + YAML("yaml"); + + private final String cliArgument; + + OutputFormat(String cliArgument) { + this.cliArgument = cliArgument; + } + + /** The argument for CLI option --format that selects this output format. */ + @Override + public String toString() { + return cliArgument; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PClass.java b/pkl-core/src/main/java/org/pkl/core/PClass.java new file mode 100644 index 00000000..e1c7352a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PClass.java @@ -0,0 +1,282 @@ +/** + * 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; + +import java.util.*; +import org.pkl.core.util.Nullable; + +/** Java representation of a {@code pkl.base#Class} value. */ +public final class PClass extends Member implements Value { + private static final long serialVersionUID = 0L; + + private final PClassInfo classInfo; + private final List typeParameters; + private final Map properties; + private final Map methods; + + private @Nullable PType supertype; + private @Nullable PClass superclass; + + private @Nullable Map allProperties; + private @Nullable Map allMethods; + + public PClass( + @Nullable String docComment, + SourceLocation sourceLocation, + Set modifiers, + List annotations, + PClassInfo classInfo, + List typeParameters, + Map properties, + Map methods) { + super(docComment, sourceLocation, modifiers, annotations, classInfo.getSimpleName()); + this.classInfo = classInfo; + this.typeParameters = typeParameters; + this.properties = properties; + this.methods = methods; + } + + public void initSupertype(PType supertype, PClass superclass) { + this.supertype = supertype; + this.superclass = superclass; + } + + /** + * Returns the name of the module that this class is declared in. Note that a module name is not + * guaranteed to be unique, especially if it not declared but inferred from the module URI. + */ + public String getModuleName() { + return classInfo.getModuleName(); + } + + /** + * Returns the qualified name of this class, `moduleName#className`. Note that a qualified class + * name is not guaranteed to be unique, especially if the module name is not declared but inferred + * from the module URI. + */ + public String getQualifiedName() { + return classInfo.getQualifiedName(); + } + + public String getDisplayName() { + return classInfo.getDisplayName(); + } + + public PClassInfo getInfo() { + return classInfo; + } + + /** Tells if this class is the class of a module. */ + public boolean isModuleClass() { + return getInfo().isModuleClass(); + } + + public List getTypeParameters() { + return typeParameters; + } + + public @Nullable PType getSupertype() { + return supertype; + } + + public @Nullable PClass getSuperclass() { + return superclass; + } + + public Map getProperties() { + return properties; + } + + public Map getMethods() { + return methods; + } + + public Map getAllProperties() { + if (allProperties == null) { + allProperties = collectAllProperties(this, new LinkedHashMap<>()); + } + return allProperties; + } + + public Map getAllMethods() { + if (allMethods == null) { + allMethods = collectAllMethods(this, new LinkedHashMap<>()); + } + return allMethods; + } + + @Override + public void accept(ValueVisitor visitor) { + visitor.visitClass(this); + } + + @Override + public T accept(ValueConverter converter) { + return converter.convertClass(this); + } + + @Override + public PClassInfo getClassInfo() { + return PClassInfo.Class; + } + + public String toString() { + return getDisplayName(); + } + + public abstract static class ClassMember extends Member { + + private static final long serialVersionUID = 0L; + + private final PClass owner; + + public ClassMember( + @Nullable String docComment, + SourceLocation sourceLocation, + Set modifiers, + List annotations, + String simpleName, + PClass owner) { + super(docComment, sourceLocation, modifiers, annotations, simpleName); + this.owner = owner; + } + + /** + * @inheritDoc + */ + @Override + public String getModuleName() { + return owner.getInfo().getModuleName(); + } + + /** Returns the class declaring this member. */ + public PClass getOwner() { + return owner; + } + + /** + * Returns the documentation comment of this member. If this member does not have a + * documentation comment, returns the documentation comment of the nearest documented ancestor, + * if any. + */ + public abstract @Nullable String getInheritedDocComment(); + } + + public static final class Property extends ClassMember { + + private static final long serialVersionUID = 0L; + + private final PType type; + + public Property( + PClass owner, + @Nullable String docComment, + SourceLocation sourceLocation, + Set modifiers, + List annotations, + String simpleName, + PType type) { + super(docComment, sourceLocation, modifiers, annotations, simpleName, owner); + this.type = type; + } + + public PType getType() { + return type; + } + + @Override + public @Nullable String getInheritedDocComment() { + if (getDocComment() != null) return getDocComment(); + + for (var clazz = getOwner().getSuperclass(); clazz != null; clazz = clazz.getSuperclass()) { + var property = clazz.getProperties().get(getSimpleName()); + if (property != null && property.getDocComment() != null) { + return property.getDocComment(); + } + } + + return null; + } + } + + public static final class Method extends ClassMember { + + private static final long serialVersionUID = 0L; + + private final List typeParameters; + private final Map parameters; + private final PType returnType; + + public Method( + PClass owner, + @Nullable String docComment, + SourceLocation sourceLocation, + Set modifiers, + List annotations, + String simpleName, + List typeParameters, + Map parameters, + PType returnType) { + super(docComment, sourceLocation, modifiers, annotations, simpleName, owner); + this.typeParameters = typeParameters; + this.parameters = parameters; + this.returnType = returnType; + } + + public List getTypeParameters() { + return typeParameters; + } + + public Map getParameters() { + return parameters; + } + + public PType getReturnType() { + return returnType; + } + + @Override + public @Nullable String getInheritedDocComment() { + if (getDocComment() != null) return getDocComment(); + + for (var clazz = getOwner().getSuperclass(); clazz != null; clazz = clazz.getSuperclass()) { + var method = clazz.getMethods().get(getSimpleName()); + if (method != null && method.getDocComment() != null) { + return method.getDocComment(); + } + } + + return null; + } + } + + private Map collectAllProperties( + PClass clazz, Map collector) { + if (clazz.superclass != null) { + collectAllProperties(clazz.superclass, collector); + } + collector.putAll(clazz.properties); + return collector; + } + + private Map collectAllMethods(PClass clazz, Map collector) { + if (clazz.superclass != null) { + collectAllMethods(clazz.superclass, collector); + } + collector.putAll(clazz.methods); + return collector; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PClassInfo.java b/pkl-core/src/main/java/org/pkl/core/PClassInfo.java new file mode 100644 index 00000000..2d540881 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PClassInfo.java @@ -0,0 +1,248 @@ +/** + * 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; + +import static java.util.Map.*; + +import java.io.Serializable; +import java.net.URI; +import java.util.*; +import java.util.regex.Pattern; +import org.pkl.core.util.Nullable; + +/** Information about a Pkl class and its Java representation. */ +@SuppressWarnings("rawtypes") +public final class PClassInfo implements Serializable { + + private static final long serialVersionUID = 0L; + + // Simple name of a module's class. + // User-facing via `module.getClass()` and error messages. + // "Module" would result in a name clash between + // the module class of `pkl.base` and class `Module` defined in `pkl.base`. + public static final String MODULE_CLASS_NAME = "ModuleClass"; + + public static final URI pklBaseUri = URI.create("pkl:base"); + public static final URI pklSemverUri = URI.create("pkl:semver"); + public static final URI pklProjectUri = URI.create("pkl:Project"); + + public static final PClassInfo Any = pklBaseClassInfo("Any", Void.class); + public static final PClassInfo Null = pklBaseClassInfo("Null", PNull.class); + public static final PClassInfo String = pklBaseClassInfo("String", String.class); + public static final PClassInfo Boolean = pklBaseClassInfo("Boolean", boolean.class); + public static final PClassInfo Number = pklBaseClassInfo("Number", Void.class); + public static final PClassInfo Int = pklBaseClassInfo("Int", long.class); + public static final PClassInfo Float = pklBaseClassInfo("Float", double.class); + public static final PClassInfo Duration = pklBaseClassInfo("Duration", Duration.class); + public static final PClassInfo DataSize = pklBaseClassInfo("DataSize", DataSize.class); + public static final PClassInfo Pair = pklBaseClassInfo("Pair", Pair.class); + public static final PClassInfo Collection = pklBaseClassInfo("Collection", Void.class); + public static final PClassInfo List = pklBaseClassInfo("List", ArrayList.class); + public static final PClassInfo Set = pklBaseClassInfo("Set", LinkedHashSet.class); + public static final PClassInfo Map = pklBaseClassInfo("Map", LinkedHashMap.class); + public static final PClassInfo Object = pklBaseClassInfo("Object", PObject.class); + public static final PClassInfo Dynamic = pklBaseClassInfo("Dynamic", PObject.class); + public static final PClassInfo Typed = pklBaseClassInfo("Typed", PObject.class); + public static final PClassInfo Listing = pklBaseClassInfo("Listing", ArrayList.class); + public static final PClassInfo Mapping = + pklBaseClassInfo("Mapping", LinkedHashMap.class); + public static final PClassInfo Module = pklBaseClassInfo("Module", PModule.class); + public static final PClassInfo Class = pklBaseClassInfo("Class", PClass.class); + public static final PClassInfo TypeAlias = + pklBaseClassInfo("TypeAlias", TypeAlias.class); + public static final PClassInfo Regex = pklBaseClassInfo("Regex", Pattern.class); + public static final PClassInfo Deprecated = + pklBaseClassInfo("Deprecated", PObject.class); + public static final PClassInfo AlsoKnownAs = + pklBaseClassInfo("AlsoKnownAs", PObject.class); + public static final PClassInfo Unlisted = pklBaseClassInfo("Unlisted", PObject.class); + public static final PClassInfo DocExample = + pklBaseClassInfo("DocExample", PObject.class); + public static final PClassInfo PcfRenderDirective = + pklBaseClassInfo("PcfRenderDirective", PObject.class); + public static final PClassInfo ModuleInfo = + pklBaseClassInfo("ModuleInfo", PObject.class); + public static final PClassInfo Version = + new PClassInfo<>("pkl.semver", "Version", PObject.class, pklSemverUri); + public static final PClassInfo Project = + new PClassInfo<>("pkl.Project", "ModuleClass", PObject.class, pklProjectUri); + + public static final PClassInfo Unavailable = + new PClassInfo<>("unavailable", "unavailable", Object.class, URI.create("pkl:unavailable")); + + /** Returns the class info for the class with the given module and class name. */ + public static PClassInfo get(String moduleName, String className, URI moduleUri) { + if (moduleName.equals("pkl.base")) { + var classInfo = pooledPklBaseClassInfos.get(className); + if (classInfo != null) return classInfo; + } + return new PClassInfo<>(moduleName, className, PObject.class, moduleUri); + } + + /** Returns the class info for the module class with the given module name. */ + public static PClassInfo forModuleClass(String moduleName, URI moduleUri) { + return get(moduleName, MODULE_CLASS_NAME, moduleUri); + } + + /** Returns the class info for the given value's class. */ + @SuppressWarnings("unchecked") + public static PClassInfo forValue(T value) { + if (value instanceof Value) return (PClassInfo) ((Value) value).getClassInfo(); + + if (value instanceof String) return (PClassInfo) String; + if (value instanceof Boolean) return (PClassInfo) Boolean; + if (value instanceof Long) return (PClassInfo) Int; + if (value instanceof Double) return (PClassInfo) Float; + if (value instanceof List) return (PClassInfo) List; + if (value instanceof Set) return (PClassInfo) Set; + if (value instanceof Map) return (PClassInfo) Map; + if (value instanceof Pattern) return (PClassInfo) Regex; + + throw new IllegalArgumentException("Not a Pkl value: " + value); + } + + /** + * Returns the name of the module that this Pkl class is declared in. Note that a module name is + * not guaranteed to be unique, especially if it not declared but inferred. + */ + public String getModuleName() { + return moduleName; + } + + /** Returns the simple name of this Pkl class. */ + public String getSimpleName() { + return className; + } + + /** + * Returns the qualified name of this Pkl class, `moduleName/className`. Note that a qualified + * class name is not guaranteed to be unique, especially if the module name is not declared but + * inferred. + */ + public String getQualifiedName() { + return qualifiedName; + } + + public String getDisplayName() { + // display `String` rather than `pkl.base#String`, etc. + return moduleName.equals("pkl.base") ? className : isModuleClass() ? moduleName : qualifiedName; + } + + public boolean isModuleClass() { + // should have a better way but this is what we got + return className.equals(MODULE_CLASS_NAME); + } + + /** + * Returns the concrete Java class used to represent values of this Pkl class in Java. Returns + * {@code Void.class} for abstract Pkl classes. + */ + public Class getJavaClass() { + return javaClass; + } + + /** Tells if this Pkl class is external (built-in). */ + public boolean isExternalClass() { + return javaClass != PObject.class; + } + + /** Tells if this class is defined in Pkl's standard library. */ + public boolean isStandardLibraryClass() { + return moduleName.startsWith("pkl."); + } + + public boolean isConcreteCollectionClass() { + return this == PClassInfo.List || this == PClassInfo.Set; + } + + public boolean isExactClassOf(Object value) { + var clazz = value.getClass(); + if (clazz != javaClass) return false; + if (clazz != PObject.class) return true; + + var pObject = (PObject) value; + return pObject.getClassInfo().equals(this); + } + + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof PClassInfo)) return false; + + var other = (PClassInfo) obj; + return qualifiedName.equals(other.qualifiedName); + } + + public int hashCode() { + return qualifiedName.hashCode(); + } + + public String toString() { + return getDisplayName(); + } + + private static final Map> pooledPklBaseClassInfos = + java.util.Map.ofEntries( + entry(Any.className, Any), + entry(Null.className, Null), + entry(Boolean.className, Boolean), + entry(String.className, String), + entry(Number.className, Number), + entry(Int.className, Int), + entry(Float.className, Float), + entry(Duration.className, Duration), + entry(DataSize.className, DataSize), + entry(Pair.className, Pair), + entry(Collection.className, Collection), + entry(List.className, List), + entry(Set.className, Set), + entry(Map.className, Map), + entry(Object.className, Object), + entry(Dynamic.className, Dynamic), + entry(Typed.className, Typed), + entry(Listing.className, Listing), + entry(Mapping.className, Mapping), + entry(Module.className, Module), + entry(Class.className, Class), + entry(TypeAlias.className, TypeAlias), + entry(Regex.className, Regex), + entry(Deprecated.className, Deprecated), + entry(AlsoKnownAs.className, AlsoKnownAs), + entry(Unlisted.className, Unlisted), + entry(DocExample.className, DocExample), + entry(PcfRenderDirective.className, PcfRenderDirective)); + + private final String moduleName; + private final String className; + private final URI moduleUri; + private final String qualifiedName; + private final Class javaClass; + + private PClassInfo(String moduleName, String className, Class javaClass, URI moduleUri) { + this.moduleName = moduleName; + this.className = className; + this.moduleUri = moduleUri; + this.qualifiedName = moduleName + "#" + className; + this.javaClass = javaClass; + } + + private static PClassInfo pklBaseClassInfo(String className, Class javaType) { + return new PClassInfo<>("pkl.base", className, javaType, pklBaseUri); + } + + public URI getModuleUri() { + return moduleUri; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PListRenderer.java b/pkl-core/src/main/java/org/pkl/core/PListRenderer.java new file mode 100644 index 00000000..a7ff8625 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PListRenderer.java @@ -0,0 +1,297 @@ +/** + * 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; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.*; +import java.util.regex.Pattern; +import org.pkl.core.util.ArrayCharEscaper; + +// To instantiate this class, use ValueRenderers.plist(). +final class PListRenderer implements ValueRenderer { + private static final String LINE_BREAK = "\n"; + + // it's safe (though not required) to escape all of the following characters in XML text nodes + private static final ArrayCharEscaper charEscaper = + ArrayCharEscaper.builder() + .withEscape('"', """) + .withEscape('\'', "'") + .withEscape('<', "<") + .withEscape('>', ">") + .withEscape('&', "&") + .build(); + + private final Writer writer; + private final String indent; + + public PListRenderer(Writer writer, String indent) { + this.writer = writer; + this.indent = indent; + } + + @Override + public void renderDocument(Object value) { + if (!(value instanceof Collection || value instanceof Map || value instanceof Composite)) { + // as far as I can tell, only arrays and dicts are allowed as top-level values + // see: + // https://github.com/apple/swift/blob/2bf6b88585ba0bc756c8a50c95081fc08f47fbf0/stdlib/public/SDK/Foundation/PlistEncoder.swift#L79 + throw new RendererException( + String.format( + "The top-level value of an XML property list must have type `Collection`, `Map`, or `Composite`, but got type `%s`.", + value.getClass().getTypeName())); + } + + write(""); + write(LINE_BREAK); + write( + ""); + write(LINE_BREAK); + write(""); + write(LINE_BREAK); + + new Visitor().visit(value); + + write(LINE_BREAK); + write(""); + write(LINE_BREAK); + } + + @Override + public void renderValue(Object value) { + new Visitor().visit(value); + } + + // keep in sync with org.pkl.core.stdlib.PListRendererNodes.PListRenderer + private class Visitor implements ValueVisitor { + private String currIndent = ""; + + @Override + public void visitString(String value) { + write(""); + write(charEscaper.escape(value)); + write(""); + } + + @Override + public void visitInt(Long value) { + write(""); + write(value.toString()); + write(""); + } + + // according to: + // https://www.apple.com/DTDs/PropertyList-1.0.dtd + // http://www.atomicbird.com/blog/json-vs-plists (nan, infinity) + @Override + @SuppressWarnings("Duplicates") + public void visitFloat(Double value) { + write(""); + + if (value.isNaN()) { + write("nan"); + } else if (value == Double.POSITIVE_INFINITY) { + write("+infinity"); + } else if (value == Double.NEGATIVE_INFINITY) { + write("-infinity"); + } else { + write(value.toString()); + } + + write(""); + } + + @Override + public void visitBoolean(Boolean value) { + write(value ? "" : ""); + } + + @Override + public void visitDuration(Duration value) { + throw new RendererException( + String.format( + "Values of type `Duration` cannot be rendered as XML property list. Value: %s", + value)); + } + + @Override + public void visitDataSize(DataSize value) { + throw new RendererException( + String.format( + "Values of type `DataSize` cannot be rendered as XML property list. Value: %s", + value)); + } + + @Override + public void visitPair(Pair value) { + doVisitIterable(value, false); + } + + @Override + public void visitList(List value) { + doVisitIterable(value, value.isEmpty()); + } + + @Override + public void visitSet(Set value) { + doVisitIterable(value, value.isEmpty()); + } + + @Override + public void visitMap(Map map) { + var renderedAtLeastOneEntry = false; + + for (var entry : map.entrySet()) { + var key = entry.getKey(); + if (!(key instanceof String)) { + throw new RendererException( + String.format( + "Maps with non-String keys cannot be rendered as XML property list. Key: %s", + key)); + } + + var value = entry.getValue(); + if (value instanceof PNull) continue; + + if (!renderedAtLeastOneEntry) { + write(""); + write(LINE_BREAK); + currIndent += indent; + renderedAtLeastOneEntry = true; + } + + write(currIndent); + write(""); + write(charEscaper.escape((String) key)); + write(""); + write(LINE_BREAK); + + write(currIndent); + visit(value); + write(LINE_BREAK); + } + + if (renderedAtLeastOneEntry) { + currIndent = currIndent.substring(0, currIndent.length() - indent.length()); + write(currIndent); + write(""); + } else { + write(""); + } + } + + @Override + public void visitObject(PObject value) { + doVisitComposite(value); + } + + @Override + public void visitModule(PModule value) { + doVisitComposite(value); + } + + @Override + public void visitNull() { + throw new RendererException("`null` values cannot be rendered as XML property list."); + } + + @Override + public void visitClass(PClass value) { + throw new RendererException( + String.format( + "Values of type `Class` cannot be rendered as XML property list. Value: %s", value)); + } + + @Override + public void visitTypeAlias(TypeAlias value) { + throw new RendererException( + String.format( + "Values of type `TypeAlias` cannot be rendered as XML property list. Value: %s", + value.getSimpleName())); + } + + @Override + public void visitRegex(Pattern value) { + throw new RendererException( + String.format( + "Values of type `Regex` cannot be rendered as XML property list. Value: %s", value)); + } + + private void doVisitIterable(Iterable iterable, boolean isEmpty) { + if (isEmpty) { + write(""); + return; + } + + write(""); + write(LINE_BREAK); + currIndent += indent; + + for (var elem : iterable) { + write(currIndent); + visit(elem); + write(LINE_BREAK); + } + + currIndent = currIndent.substring(0, currIndent.length() - indent.length()); + write(currIndent); + write(""); + } + + private void doVisitComposite(Composite composite) { + var renderedAtLeastOneProperty = false; + + for (var entry : composite.getProperties().entrySet()) { + var value = entry.getValue(); + if (value instanceof PNull) continue; + + if (!renderedAtLeastOneProperty) { + write(""); + write(LINE_BREAK); + currIndent += indent; + renderedAtLeastOneProperty = true; + } + + write(currIndent); + write(""); + write(charEscaper.escape(entry.getKey())); + write(""); + write(LINE_BREAK); + + write(currIndent); + visit(value); + write(LINE_BREAK); + } + + if (renderedAtLeastOneProperty) { + currIndent = currIndent.substring(0, currIndent.length() - indent.length()); + write(currIndent); + write(""); + } else { + write(""); + } + } + } + + private void write(String str) { + try { + writer.write(str); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PModule.java b/pkl-core/src/main/java/org/pkl/core/PModule.java new file mode 100644 index 00000000..48ebfc3d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PModule.java @@ -0,0 +1,84 @@ +/** + * 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; + +import java.net.URI; +import java.util.Map; +import java.util.Objects; +import org.pkl.core.util.Nullable; + +/** Java representation of a Pkl module. */ +public final class PModule extends PObject { + private static final long serialVersionUID = 0L; + + private final URI moduleUri; + private final String moduleName; + + public PModule( + URI moduleUri, String moduleName, PClassInfo classInfo, Map properties) { + super(classInfo, properties); + this.moduleUri = Objects.requireNonNull(moduleUri, "moduleUri"); + this.moduleName = Objects.requireNonNull(moduleName, "moduleName"); + } + + public URI getModuleUri() { + return moduleUri; + } + + public String getModuleName() { + return moduleName; + } + + @Override + public Object getProperty(String name) { + var result = properties.get(name); + if (result != null) return result; + + throw new NoSuchPropertyException( + String.format( + "Module `%s` does not have a property named `%s`. Available properties: %s", + moduleName, name, properties.keySet()), + name); + } + + @Override + public void accept(ValueVisitor visitor) { + visitor.visitModule(this); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof PModule)) return false; + + var other = (PModule) obj; + + return moduleUri.equals(other.moduleUri) + && moduleName.equals(other.moduleName) + && classInfo.equals(other.classInfo) + && properties.equals(other.properties); + } + + @Override + public int hashCode() { + return Objects.hash(moduleUri, moduleName, classInfo, properties); + } + + @Override + public String toString() { + return render(moduleName); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PNull.java b/pkl-core/src/main/java/org/pkl/core/PNull.java new file mode 100644 index 00000000..aadac269 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PNull.java @@ -0,0 +1,47 @@ +/** + * 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; + +/** Java representation of a {@code pkl.base#Null} value. */ +public final class PNull implements Value { + private static final long serialVersionUID = 0L; + private static final PNull INSTANCE = new PNull(); + + /** Returns the sole instance of this class. */ + public static PNull getInstance() { + return INSTANCE; + } + + @Override + public void accept(ValueVisitor visitor) { + visitor.visitNull(); + } + + @Override + public T accept(ValueConverter converter) { + return converter.convertNull(); + } + + @Override + public PClassInfo getClassInfo() { + return PClassInfo.Null; + } + + @Override + public String toString() { + return "null"; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PObject.java b/pkl-core/src/main/java/org/pkl/core/PObject.java new file mode 100644 index 00000000..fdd3621f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PObject.java @@ -0,0 +1,109 @@ +/** + * 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; + +import java.util.Map; +import java.util.Objects; +import org.pkl.core.util.Nullable; + +/** Java representation of a Pkl object. */ +public class PObject implements Composite { + private static final long serialVersionUID = 0L; + + protected final PClassInfo classInfo; + protected final Map properties; + + public PObject(PClassInfo classInfo, Map properties) { + this.classInfo = Objects.requireNonNull(classInfo, "classInfo"); + this.properties = Objects.requireNonNull(properties, "properties"); + } + + @Override + public final PClassInfo getClassInfo() { + return classInfo; + } + + @Override + public final Map getProperties() { + return properties; + } + + @Override + public Object getProperty(String name) { + var result = properties.get(name); + if (result != null) return result; + + throw new NoSuchPropertyException( + String.format( + "Object of type `%s` does not have a property named `%s`. Available properties: %s", + classInfo.getQualifiedName(), name, properties.keySet()), + name); + } + + @Override + public void accept(ValueVisitor visitor) { + visitor.visitObject(this); + } + + @Override + public T accept(ValueConverter converter) { + return converter.convertObject(this); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + var other = (PObject) obj; + return classInfo.equals(other.classInfo) && properties.equals(other.properties); + } + + @Override + public int hashCode() { + return Objects.hash(classInfo, properties); + } + + @Override + public String toString() { + return render(getClassInfo().getDisplayName()); + } + + protected String render(@Nullable String prefix) { + var builder = new StringBuilder(); + + if (prefix != null) { + builder.append(prefix); + builder.append(" { "); + } else { + builder.append("{ "); + } + + var first = true; + for (var property : properties.entrySet()) { + if (first) { + first = false; + } else { + builder.append("; "); + } + builder.append(property.getKey()).append(" = ").append(property.getValue()); + } + + builder.append(" }"); + return builder.toString(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PType.java b/pkl-core/src/main/java/org/pkl/core/PType.java new file mode 100644 index 00000000..171a87b2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PType.java @@ -0,0 +1,225 @@ +/** + * 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; + +import java.io.Serializable; +import java.util.*; + +/** A Pkl type as used in type annotations. */ +public abstract class PType implements Serializable { + + private static final long serialVersionUID = 0L; + + /** The `unknown` type. Omitting a type annotation is equivalent to stating this type. */ + public static final PType UNKNOWN = + new PType() { + private static final long serialVersionUID = 0L; + }; + + /** The bottom type. */ + public static final PType NOTHING = + new PType() { + private static final long serialVersionUID = 0L; + }; + + /** The type of the enclosing module. */ + public static final PType MODULE = + new PType() { + private static final long serialVersionUID = 0L; + }; + + private PType() {} + + public List getTypeArguments() { + return List.of(); + } + + public static final class StringLiteral extends PType { + + private static final long serialVersionUID = 0L; + + private final String literal; + + public StringLiteral(String literal) { + this.literal = literal; + } + + public String getLiteral() { + return literal; + } + } + + public static final class Class extends PType { + + private static final long serialVersionUID = 0L; + + private final PClass pClass; + private final List typeArguments; + + public Class(PClass pClass, List typeArguments) { + this.pClass = pClass; + this.typeArguments = typeArguments; + } + + public Class(PClass pClass) { + this(pClass, List.of()); + } + + public Class(PClass pClass, PType typeArgument1) { + this(pClass, List.of(typeArgument1)); + } + + public Class(PClass pClass, PType typeArgument1, PType typeArgument2) { + this(pClass, List.of(typeArgument1, typeArgument2)); + } + + public PClass getPClass() { + return pClass; + } + + @Override + public List getTypeArguments() { + return typeArguments; + } + } + + public static final class Nullable extends PType { + + private static final long serialVersionUID = 0L; + + private final PType baseType; + + public Nullable(PType baseType) { + this.baseType = baseType; + } + + public PType getBaseType() { + return baseType; + } + } + + public static final class Constrained extends PType { + + private static final long serialVersionUID = 0L; + + private final PType baseType; + private final List constraints; + + public Constrained(PType baseType, List constraints) { + this.baseType = baseType; + this.constraints = constraints; + } + + public PType getBaseType() { + return baseType; + } + + public List getConstraints() { + return constraints; + } + } + + public static final class Alias extends PType { + + private static final long serialVersionUID = 0L; + + private final TypeAlias typeAlias; + private final List typeArguments; + private final PType aliasedType; + + public Alias(TypeAlias typeAlias) { + this(typeAlias, List.of(), typeAlias.getAliasedType()); + } + + public Alias(TypeAlias typeAlias, List typeArguments, PType aliasedType) { + this.typeAlias = typeAlias; + this.typeArguments = typeArguments; + this.aliasedType = aliasedType; + } + + public TypeAlias getTypeAlias() { + return typeAlias; + } + + @Override + public List getTypeArguments() { + return typeArguments; + } + + /** + * Returns the aliased type, namely {@code getTypeAlias().getAliasedType()} with type arguments + * substituted for type variables. + */ + public PType getAliasedType() { + return aliasedType; + } + } + + public static final class Function extends PType { + + private static final long serialVersionUID = 0L; + + private final List parameterTypes; + private final PType returnType; + + public Function(List parameterTypes, PType returnType) { + this.parameterTypes = parameterTypes; + this.returnType = returnType; + } + + public List getParameterTypes() { + return parameterTypes; + } + + public PType getReturnType() { + return returnType; + } + } + + public static final class Union extends PType { + + private static final long serialVersionUID = 0L; + + private final List elementTypes; + + public Union(List elementTypes) { + this.elementTypes = elementTypes; + } + + public List getElementTypes() { + return elementTypes; + } + } + + public static final class TypeVariable extends PType { + + private static final long serialVersionUID = 0L; + + private final TypeParameter typeParameter; + + public TypeVariable(TypeParameter typeParameter) { + this.typeParameter = typeParameter; + } + + public String getName() { + return typeParameter.getName(); + } + + public TypeParameter getTypeParameter() { + return typeParameter; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Pair.java b/pkl-core/src/main/java/org/pkl/core/Pair.java new file mode 100644 index 00000000..ae3cabda --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Pair.java @@ -0,0 +1,102 @@ +/** + * 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; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import org.pkl.core.util.Nullable; + +/** Java representation of a {@code pkl.base#Pair} value. */ +public final class Pair implements Value, Iterable { + private static final long serialVersionUID = 0L; + + private final F first; + private final S second; + + /** Constructs a pair with the given elements. */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** Returns the first element of this pair. */ + public F getFirst() { + return first; + } + + /** Returns the second element of this pair. */ + public S getSecond() { + return second; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + int pos = 0; + + @Override + public boolean hasNext() { + return pos < 2; + } + + @Override + public Object next() { + switch (pos++) { + case 0: + return first; + case 1: + return second; + default: + throw new NoSuchElementException("Pair only has two elements."); + } + } + }; + } + + @Override + public void accept(ValueVisitor visitor) { + visitor.visitPair(this); + } + + @Override + public T accept(ValueConverter converter) { + return converter.convertPair(this); + } + + @Override + public PClassInfo getClassInfo() { + return PClassInfo.Pair; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Pair)) return false; + + var other = (Pair) obj; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return first.hashCode() * 31 + second.hashCode(); + } + + @Override + public String toString() { + return "Pair(" + first + ", " + second + ")"; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PcfRenderer.java b/pkl-core/src/main/java/org/pkl/core/PcfRenderer.java new file mode 100644 index 00000000..cb2e0bf5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PcfRenderer.java @@ -0,0 +1,255 @@ +/** + * 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; + +import java.io.IOException; +import java.io.Writer; +import java.util.*; +import java.util.regex.Pattern; +import org.pkl.core.parser.Lexer; + +// To instantiate this class, use ValueRenderers.pcf(). +final class PcfRenderer implements ValueRenderer { + private static final String LINE_SEPARATOR = "\n"; + + private final Writer writer; + private final String indent; + private final boolean omitNullProperties; + private final ValueFormatter valueFormatter; + + private String currIndent = ""; + + public PcfRenderer( + Writer writer, String indent, boolean omitNullProperties, boolean useCustomStringDelimiters) { + this.writer = writer; + this.indent = indent; + this.omitNullProperties = omitNullProperties; + this.valueFormatter = new ValueFormatter(true, useCustomStringDelimiters); + } + + @Override + public void renderDocument(Object value) { + if (!(value instanceof Composite)) { + throw new RendererException( + String.format( + "The top-level value of a Pcf document must have type `Composite`, but got type `%s`.", + value.getClass().getTypeName())); + } + + new Visitor().doVisitProperties(((Composite) value).getProperties()); + } + + @Override + public void renderValue(Object value) { + new Visitor().visit(value); + } + + private class Visitor implements ValueVisitor { + @Override + public void visitString(String value) { + try { + valueFormatter.formatStringValue(value, currIndent + indent, writer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void visitInt(Long value) { + write(value.toString()); + } + + @Override + public void visitFloat(Double value) { + write(value.toString()); + } + + @Override + public void visitBoolean(Boolean value) { + write(value.toString()); + } + + @Override + public void visitDuration(Duration value) { + write(value.toString()); + } + + @Override + public void visitDataSize(DataSize value) { + write(value.toString()); + } + + @Override + public void visitPair(Pair value) { + doVisitIterable(value, "Pair("); + } + + @Override + public void visitList(List value) { + doVisitIterable(value, "List("); + } + + @Override + public void visitSet(Set value) { + doVisitIterable(value, "Set("); + } + + @Override + public void visitMap(Map value) { + var first = true; + write("Map("); + for (var entry : value.entrySet()) { + if (first) { + first = false; + } else { + write(", "); + } + if (entry.getKey() instanceof Composite) { + write("new "); + } + visit(entry.getKey()); + write(", "); + if (entry.getValue() instanceof Composite) { + write("new "); + } + visit(entry.getValue()); + } + write(')'); + } + + @Override + public void visitObject(PObject value) { + doVisitComposite(value); + } + + @Override + public void visitModule(PModule value) { + doVisitComposite(value); + } + + @Override + public void visitClass(PClass value) { + throw new RendererException( + String.format( + "Values of type `Class` cannot be rendered as Pcf. Value: %s", + value.getSimpleName())); + } + + @Override + public void visitTypeAlias(TypeAlias value) { + throw new RendererException( + String.format( + "Values of type `TypeAlias` cannot be rendered as Pcf. Value: %s", + value.getSimpleName())); + } + + @Override + public void visitNull() { + write("null"); + } + + @Override + public void visitRegex(Pattern value) { + write("Regex("); + visitString(value.pattern()); + write(')'); + } + + private void doVisitIterable(Iterable iterable, String prefix) { + var first = true; + write(prefix); + for (var elem : iterable) { + if (first) { + first = false; + } else { + write(", "); + } + if (elem == null) { // unevaluated property + write("?"); + continue; + } + if (elem instanceof Composite) { + write("new "); + } + visit(elem); + } + write(')'); + } + + private void doVisitComposite(Composite composite) { + if (composite.getProperties().isEmpty()) { + write("{}"); + return; + } + + write('{'); + write(LINE_SEPARATOR); + currIndent += indent; + doVisitProperties(composite.getProperties()); + currIndent = currIndent.substring(0, currIndent.length() - indent.length()); + write(currIndent); + write('}'); + } + + private void doVisitProperties(Map properties) { + properties.forEach( + (name, value) -> { + if (omitNullProperties && value instanceof PNull) return; + + write(currIndent); + writeIdentifier(name); + + if (value == null) { // unevaluated property + write(" = ?"); + } else if (value instanceof Composite) { + write(' '); + visit(value); + } else { + write(" = "); + visit(value); + } + + write(LINE_SEPARATOR); + }); + } + + private void write(char ch) { + try { + writer.write(ch); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void write(String str) { + try { + writer.write(str); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void writeIdentifier(String identifier) { + if (Lexer.isRegularIdentifier(identifier)) { + write(identifier); + } else { + write('`'); + write(identifier); + write('`'); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PklBugException.java b/pkl-core/src/main/java/org/pkl/core/PklBugException.java new file mode 100644 index 00000000..028aea7b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PklBugException.java @@ -0,0 +1,34 @@ +/** + * 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; + +public final class PklBugException extends PklException { + public static PklBugException unreachableCode() { + return new PklBugException("Unreachable code."); + } + + public PklBugException(String message, Throwable cause) { + super(message, cause); + } + + public PklBugException(Throwable cause) { + super("An unexpected error has occurred. Would you mind filing a bug report?", cause); + } + + public PklBugException(String message) { + super(message); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PklException.java b/pkl-core/src/main/java/org/pkl/core/PklException.java new file mode 100644 index 00000000..f3355472 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PklException.java @@ -0,0 +1,26 @@ +/** + * 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; + +public class PklException extends RuntimeException { + public PklException(String message, Throwable cause) { + super(message, cause); + } + + public PklException(String message) { + super(message); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PklInfo.java b/pkl-core/src/main/java/org/pkl/core/PklInfo.java new file mode 100644 index 00000000..80338290 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PklInfo.java @@ -0,0 +1,63 @@ +/** + * 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; + +/** Information about the Pkl package index. */ +public final class PklInfo { + private static final String PACKAGE_INDEX_HOMEPAGE = "https://pkl-lang.org/package-docs"; + + private static final PklInfo CURRENT; + private final PackageIndex packageIndex; + + static { + CURRENT = new PklInfo(new PackageIndex(PACKAGE_INDEX_HOMEPAGE)); + } + + /** The current {@link PklInfo}. */ + public static PklInfo current() { + return CURRENT; + } + + /** Constructs a {@link PklInfo}. */ + PklInfo(PackageIndex packageIndex) { + this.packageIndex = packageIndex; + } + + /** The Pkl package index. */ + public PackageIndex getPackageIndex() { + return packageIndex; + } + + /** A Pkl package index. */ + public static final class PackageIndex { + private final String homepage; + + /** Constructs a {@link PackageIndex}. */ + public PackageIndex(String homepage) { + this.homepage = homepage; + } + + /** The homepage of this package index. */ + public String homepage() { + return homepage; + } + + /** Returns the homepage of the given package. */ + public String getPackagePage(String packageName, String packageVersion) { + return homepage + packageName + "/" + packageVersion + "/"; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Platform.java b/pkl-core/src/main/java/org/pkl/core/Platform.java new file mode 100644 index 00000000..0f6c59ce --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Platform.java @@ -0,0 +1,284 @@ +/** + * 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; + +import com.oracle.truffle.api.Truffle; +import com.oracle.truffle.api.TruffleOptions; +import java.util.Objects; +import org.graalvm.home.Version; + +/** + * Information about the Pkl release that the current program runs on. This class is the Java + * equivalent of standard library module {@code pkl.platform}. + */ +public final class Platform { + private static final Platform CURRENT; + + static { + var pklVersion = Release.current().version().toString(); + var osName = System.getProperty("os.name"); + if (osName.equals("Mac OS X")) osName = "macOS"; + var osVersion = System.getProperty("os.version"); + var architecture = System.getProperty("os.arch"); + + var runtimeName = Truffle.getRuntime().getName(); + var runtimeVersion = Version.getCurrent().toString(); + var vmName = TruffleOptions.AOT ? runtimeName : System.getProperty("java.vm.name"); + var vmVersion = TruffleOptions.AOT ? runtimeVersion : System.getProperty("java.vm.version"); + + CURRENT = + new Platform( + new Language(pklVersion), + new Runtime(runtimeName, runtimeVersion), + new VirtualMachine(vmName, vmVersion), + new OperatingSystem(osName, osVersion), + new Processor(architecture)); + } + + private final Language language; + private final Runtime runtime; + private final VirtualMachine virtualMachine; + private final OperatingSystem operatingSystem; + private final Processor processor; + + /** Constructs a platform. */ + public Platform( + Language language, + Runtime runtime, + VirtualMachine virtualMachine, + OperatingSystem operatingSystem, + Processor processor) { + this.language = language; + this.runtime = runtime; + this.virtualMachine = virtualMachine; + this.operatingSystem = operatingSystem; + this.processor = processor; + } + + /** The Pkl release that the current program runs on. */ + public static Platform current() { + return CURRENT; + } + + /** The language implementation of this platform. */ + public Language language() { + return language; + } + + /** The language runtime of this platform. */ + public Runtime runtime() { + return runtime; + } + + /** The virtual machine of this platform. */ + public VirtualMachine virtualMachine() { + return virtualMachine; + } + + /** The operating system of this platform. */ + public OperatingSystem operatingSystem() { + return operatingSystem; + } + + /** The processor of this platform. */ + public Processor processor() { + return processor; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Platform)) return false; + + var other = (Platform) obj; + return language.equals(other.language) + && runtime.equals(other.runtime) + && virtualMachine.equals(other.virtualMachine) + && operatingSystem.equals(other.operatingSystem) + && processor.equals(other.processor); + } + + @Override + public int hashCode() { + return Objects.hash(language, runtime, virtualMachine, operatingSystem, processor); + } + + /** The language implementation of a platform. */ + public static final class Language { + private final String version; + + /** Constructs a {@link Language}. */ + public Language(String version) { + this.version = version; + } + + /** The version of this language implementation. */ + public String version() { + return version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Language)) return false; + + var other = (Language) obj; + return version.equals(other.version); + } + + @Override + public int hashCode() { + return version.hashCode(); + } + } + + /** The language runtime of a platform. */ + public static final class Runtime { + private final String name; + private final String version; + + /** Constructs a {@link Runtime}. */ + public Runtime(String name, String version) { + this.name = name; + this.version = version; + } + + /** The name of this language runtime. */ + public String name() { + return name; + } + + /** The version of this language runtime. */ + public String version() { + return version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Runtime)) return false; + + var other = (Runtime) obj; + return name.equals(other.name) && version.equals(other.version); + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + } + + /** The virtual machine of a platform. */ + public static final class VirtualMachine { + private final String name; + private final String version; + + /** Constructs a {@link VirtualMachine}. */ + public VirtualMachine(String name, String version) { + this.name = name; + this.version = version; + } + + /** The name of this virtual machine. */ + public String name() { + return name; + } + + /** The version of this virtual machine. */ + public String version() { + return version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof VirtualMachine)) return false; + + var other = (VirtualMachine) obj; + return name.equals(other.name) && version.equals(other.version); + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + } + + /** The operating system of a platform. */ + public static final class OperatingSystem { + private final String name; + private final String version; + + /** Constructs an {@link OperatingSystem}. */ + public OperatingSystem(String name, String version) { + this.name = name; + this.version = version; + } + + /** The name of this operating system. */ + public String name() { + return name; + } + + /** The version of this operating system. */ + public String version() { + return version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof OperatingSystem)) return false; + + var other = (OperatingSystem) obj; + return name.equals(other.name) && version.equals(other.version); + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + } + + /** The processor of a platform. */ + public static final class Processor { + private final String architecture; + + /** Constructs a {@link Processor}. */ + public Processor(String architecture) { + this.architecture = architecture; + } + + /** The instruction set architecture of this processor. */ + public String architecture() { + return architecture; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Processor)) return false; + + var other = (Processor) obj; + return architecture.equals(other.architecture); + } + + @Override + public int hashCode() { + return architecture.hashCode(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/PropertiesRenderer.java b/pkl-core/src/main/java/org/pkl/core/PropertiesRenderer.java new file mode 100644 index 00000000..93fe9760 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/PropertiesRenderer.java @@ -0,0 +1,223 @@ +/** + * 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; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.properties.PropertiesUtils; + +// To instantiate this class, use ValueRenderers.properties(). +final class PropertiesRenderer implements ValueRenderer { + private final Writer writer; + private final boolean omitNullProperties; + private final boolean restrictCharset; + + public PropertiesRenderer(Writer writer, boolean omitNullProperties, boolean restrictCharset) { + this.writer = writer; + this.omitNullProperties = omitNullProperties; + this.restrictCharset = restrictCharset; + } + + @Override + public void renderDocument(Object value) { + new Visitor().renderDocument(value); + } + + @Override + public void renderValue(Object value) { + new Visitor().renderValue(value); + } + + private class Visitor implements ValueConverter { + + public void renderDocument(Object value) { + if (value instanceof Composite) { + doVisitMap(null, ((Composite) value).getProperties()); + } else if (value instanceof Map) { + doVisitMap(null, (Map) value); + } else if (value instanceof Pair) { + Pair pair = (Pair) value; + doVisitKeyAndValue(null, pair.getFirst(), pair.getSecond()); + } else { + throw new RendererException( + String.format( + "The top-level value of a Java properties file must have type `Composite`, `Map`, or `Pair`, but got type `%s`.", + value.getClass().getTypeName())); + } + } + + public void renderValue(Object value) { + write(convert(value), false, restrictCharset); + } + + @Override + public String convertNull() { + return ""; + } + + @Override + public String convertString(String value) { + return value; + } + + @Override + public String convertInt(Long value) { + return value.toString(); + } + + @Override + public String convertFloat(Double value) { + return value.toString(); + } + + @Override + public String convertBoolean(Boolean value) { + return value.toString(); + } + + @Override + public String convertDuration(Duration value) { + throw new RendererException( + String.format( + "Values of type `Duration` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertDataSize(DataSize value) { + throw new RendererException( + String.format( + "Values of type `DataSize` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertPair(Pair value) { + throw new RendererException( + String.format( + "Values of type `Pair` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertList(List value) { + throw new RendererException( + String.format( + "Values of type `List` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertSet(Set value) { + throw new RendererException( + String.format("Values of type `Set` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertMap(Map value) { + throw new RendererException( + String.format("Values of type `Map` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertObject(PObject value) { + throw new RendererException( + String.format( + "Values of type `Object` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertModule(PModule value) { + throw new RendererException( + String.format( + "Values of type `Module` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertClass(PClass value) { + throw new RendererException( + String.format( + "Values of type `Class` cannot be rendered as Properties. Value: %s", + value.getSimpleName())); + } + + @Override + public String convertTypeAlias(TypeAlias value) { + throw new RendererException( + String.format( + "Values of type `TypeAlias` cannot be rendered as Properties. Value: %s", + value.getSimpleName())); + } + + @Override + public String convertRegex(Pattern value) { + throw new RendererException( + String.format( + "Values of type `Regex` cannot be rendered as Properties. Value: %s", value)); + } + + private void doVisitMap(@Nullable String keyPrefix, Map map) { + for (Map.Entry entry : map.entrySet()) { + doVisitKeyAndValue(keyPrefix, entry.getKey(), entry.getValue()); + } + } + + private void doVisitKeyAndValue(@Nullable String keyPrefix, Object key, Object value) { + if (omitNullProperties && value instanceof PNull) return; + + var keyString = keyPrefix == null ? convert(key) : keyPrefix + "." + convert(key); + + if (value instanceof Composite) { + doVisitMap(keyString, ((Composite) value).getProperties()); + } else if (value instanceof Map) { + doVisitMap(keyString, (Map) value); + } else { + write(keyString, true, restrictCharset); + writeSeparator(); + write(convert(value), false, restrictCharset); + writeLineBreak(); + } + } + + private void write(String value, boolean escapeSpace, boolean restrictCharset) { + try { + writer.write( + PropertiesUtils.renderPropertiesKeyOrValue(value, escapeSpace, restrictCharset)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void writeSeparator() { + try { + writer.write(' '); + writer.write('='); + writer.write(' '); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void writeLineBreak() { + try { + writer.write('\n'); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Release.java b/pkl-core/src/main/java/org/pkl/core/Release.java new file mode 100644 index 00000000..44abc777 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Release.java @@ -0,0 +1,276 @@ +/** + * 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; + +import com.oracle.truffle.api.TruffleOptions; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; + +/** + * Information about the Pkl release that the current program runs on. This class is the Java + * equivalent of standard library module {@code pkl.release}. + */ +public class Release { + private static final String SOURCE_CODE_HOMEPAGE = "https://github.com/apple/pkl/"; + private static final String DOCUMENTATION_HOMEPAGE = "https://pkl-lang.org/main/"; + + private static final Release CURRENT; + + static { + var properties = new Properties(); + + try (var stream = Release.class.getResourceAsStream("Release.properties")) { + if (stream == null) { + throw new AssertionError("Failed to locate `Release.properties`."); + } + properties.load(stream); + } catch (IOException e) { + throw new AssertionError("Failed to load `Release.properties`.", e); + } + + var version = Version.parse(properties.getProperty("version")); + var commitId = properties.getProperty("commitId"); + var osName = System.getProperty("os.name"); + if (osName.equals("Mac OS X")) osName = "macOS"; + var osVersion = System.getProperty("os.version"); + var os = osName + " " + osVersion; + var flavor = TruffleOptions.AOT ? "native" : "Java " + System.getProperty("java.version"); + var versionInfo = "Pkl " + version + " (" + os + ", " + flavor + ")"; + var commitish = version.isNormal() ? version.toString() : commitId; + var docsVersion = version.isNormal() ? version.toString() : "latest"; + var docsHomepage = DOCUMENTATION_HOMEPAGE + docsVersion + "/"; + var stdlibModules = + new LinkedHashSet<>(List.of(properties.getProperty("stdlibModules").split(","))); + + CURRENT = + new Release( + version, + os, + flavor, + versionInfo, + commitId, + new SourceCode(SOURCE_CODE_HOMEPAGE, commitish), + new Documentation(docsHomepage), + new StandardLibrary(stdlibModules)); + } + + private final Version version; + private final String os; + private final String flavor; + private final String versionInfo; + private final String commitId; + private final SourceCode sourceCode; + private final Documentation documentation; + private final StandardLibrary standardLibrary; + + /** Constructs a release. */ + public Release( + Version version, + String os, + String flavor, + String versionInfo, + String commitId, + SourceCode sourceCode, + Documentation documentation, + StandardLibrary standardLibrary) { + this.version = version; + this.os = os; + this.flavor = flavor; + this.versionInfo = versionInfo; + this.commitId = commitId; + this.sourceCode = sourceCode; + this.documentation = documentation; + this.standardLibrary = standardLibrary; + } + + /** The Pkl release that the current program runs on. */ + public static Release current() { + return CURRENT; + } + + /** The version of this release. */ + public Version version() { + return version; + } + + /** The operating system (name and version) this release is running on. */ + public String os() { + return os; + } + + /** The flavor of this release (native, or Java and JVM version). */ + public String flavor() { + return flavor; + } + + /** The output of {@code pkl --version} for this release. */ + public String versionInfo() { + return versionInfo; + } + + /** The Git commit ID of this release. */ + public String commitId() { + return commitId; + } + + /** The source code of this release. */ + public SourceCode sourceCode() { + return sourceCode; + } + + /** The documentation of this release. */ + public Documentation documentation() { + return documentation; + } + + /** The standard library of this release. */ + public StandardLibrary standardLibrary() { + return standardLibrary; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Release)) return false; + + var other = (Release) obj; + return version.equals(other.version) + && versionInfo.equals(other.versionInfo) + && commitId.equals(other.commitId) + && sourceCode.equals(other.sourceCode) + && documentation.equals(other.documentation) + && standardLibrary.equals(other.standardLibrary); + } + + @Override + public int hashCode() { + return Objects.hash(version, versionInfo, commitId, sourceCode, documentation, standardLibrary); + } + + /** The source code of a Pkl release. */ + public static final class SourceCode { + private final String homepage; + private final String version; + + /** Constructs a {@link SourceCode}. */ + public SourceCode(String homepage, String version) { + this.homepage = homepage; + this.version = version; + } + + public String getVersion() { + return version; + } + + /** The homepage of this source code. */ + public String homepage() { + return homepage; + } + + /** + * Returns the source code page of the file with the given path. Note: Files may be moved + * or deleted anytime. + */ + public String getFilePage(String path) { + return homepage + "blob/" + version + "/" + path; + } + + /** The source code scheme for the stdlib module. */ + public String getSourceCodeUrlScheme() { + return homepage + "blob/" + version + "/stdlib%{path}#L%{line}-L%{endLine}"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof SourceCode)) return false; + + var other = (SourceCode) obj; + return homepage.equals(other.homepage) && version.equals(other.version); + } + + @Override + public int hashCode() { + return Objects.hash(homepage, version); + } + } + + /** The documentation of a Pkl release. */ + public static final class Documentation { + private final String homepage; + + /** Constructs a {@link Documentation}. */ + public Documentation(String homepage) { + this.homepage = homepage; + } + + /** The homepage of this documentation. */ + public String homepage() { + return homepage; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Documentation)) return false; + + var other = (Documentation) obj; + return homepage.equals(other.homepage); + } + + @Override + public int hashCode() { + return homepage.hashCode(); + } + } + + /** + * The standard library of a Pkl release. + * + * @since 0.21.0 + */ + public static final class StandardLibrary { + private final Set modules; + + /** Constructs a {@link StandardLibrary}. */ + public StandardLibrary(Set modules) { + this.modules = modules; + } + + /** The modules of this standard library. */ + public Set modules() { + return modules; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof StandardLibrary)) return false; + + var other = (StandardLibrary) obj; + return modules.equals(other.modules); + } + + @Override + public int hashCode() { + return modules.hashCode(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/RendererException.java b/pkl-core/src/main/java/org/pkl/core/RendererException.java new file mode 100644 index 00000000..a6fc7de0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/RendererException.java @@ -0,0 +1,22 @@ +/** + * 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; + +public class RendererException extends RuntimeException { + public RendererException(String message) { + super(message); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/SecurityManager.java b/pkl-core/src/main/java/org/pkl/core/SecurityManager.java new file mode 100644 index 00000000..42e7bed9 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/SecurityManager.java @@ -0,0 +1,43 @@ +/** + * 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; + +import java.net.URI; + +/** + * Enforces a security model during {@link Evaluator evaluation}. + * + *

Use {@link SecurityManagers} to obtain or construct an instance of this type. + */ +public interface SecurityManager { + /** + * Checks if the given module may be resolved. This check is required before any attempt is made + * to access the given URI. + */ + void checkResolveModule(URI uri) throws SecurityManagerException; + + /** Checks if the given importing module may import the given imported module. */ + void checkImportModule(URI importingModule, URI importedModule) throws SecurityManagerException; + + /** Checks if the given resource may be read. */ + void checkReadResource(URI resource) throws SecurityManagerException; + + /** + * Checks if the given resource may be resolved. This check is required before any attempt is made + * to access the given URI. + */ + void checkResolveResource(URI resource) throws SecurityManagerException; +} diff --git a/pkl-core/src/main/java/org/pkl/core/SecurityManagerBuilder.java b/pkl-core/src/main/java/org/pkl/core/SecurityManagerBuilder.java new file mode 100644 index 00000000..d774bdb3 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/SecurityManagerBuilder.java @@ -0,0 +1,52 @@ +/** + * 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; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import org.pkl.core.util.Nullable; + +/** + * Parent interface to builder classes for configuring a {@link SecurityManager}. + * + * @param concrete type of the builder class to maintain covariance in inherited methods + */ +public interface SecurityManagerBuilder> { + B addAllowedModule(Pattern pattern); + + B addAllowedModules(Collection patterns); + + B setAllowedModules(Collection patterns); + + List getAllowedModules(); + + B addAllowedResource(Pattern pattern); + + B addAllowedResources(Collection patterns); + + B setAllowedResources(Collection patterns); + + List getAllowedResources(); + + B setRootDir(@Nullable Path rootDir); + + @Nullable + Path getRootDir(); + + SecurityManager build(); +} diff --git a/pkl-core/src/main/java/org/pkl/core/SecurityManagerException.java b/pkl-core/src/main/java/org/pkl/core/SecurityManagerException.java new file mode 100644 index 00000000..f4c6c315 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/SecurityManagerException.java @@ -0,0 +1,27 @@ +/** + * 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; + +/** + * A SecurityManagerException declares that a violation occured during an external i/o operation. + * + *

{@link SecurityManagerException#getMessage()} is passed to users when errors arise. + */ +public class SecurityManagerException extends Exception { + public SecurityManagerException(String message) { + super(message); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/SecurityManagers.java b/pkl-core/src/main/java/org/pkl/core/SecurityManagers.java new file mode 100644 index 00000000..811b7a22 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/SecurityManagers.java @@ -0,0 +1,304 @@ +/** + * 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; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Pattern; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.Nullable; + +/** A provider for {@link SecurityManager}s. */ +public final class SecurityManagers { + private SecurityManagers() {} + + /** + * Returns the list of module patterns that the {@link #defaultManager default security manager} + * will use to determine if a module import may be resolved. + */ + public static final List defaultAllowedModules = + List.of( + Pattern.compile("repl:"), + Pattern.compile("file:"), + // for evaluating URLs returned by `Class(Loader).getResource()` + Pattern.compile("jar:file:"), + Pattern.compile("modulepath:"), + Pattern.compile("https:"), + Pattern.compile("pkl:"), + Pattern.compile("package:"), + Pattern.compile("projectpackage:")); + + /** + * Returns the list of resource patterns that the {@link #defaultManager default security manager} + * will use to determine if an external resource may be read. + */ + public static final List defaultAllowedResources = + List.of( + Pattern.compile("prop:"), + Pattern.compile("env:"), + Pattern.compile("file:"), + Pattern.compile("modulepath:"), + Pattern.compile("package:"), + Pattern.compile("projectpackage:"), + Pattern.compile("https:")); + + /** + * Returns the mapping from module URIs to trust levels used by the {@link #defaultManager default + * security manager}. + * + *

Trust levels are used to determine whether a module may import another module. Only modules + * with the same or a lower trust level may be imported. + * + *

This mapping supports a fixed set of module URI schemes. Local modules are given a higher + * trust level than remote modules. For example, a local file may import a remote file, but not + * the other way around. + */ + public static final Function defaultTrustLevels = + SecurityManagers::getDefaultTrustLevel; + + private static int getDefaultTrustLevel(URI uri) { + switch (uri.getScheme()) { + case "repl": + return 40; + case "file": + return uri.getHost() == null ? 30 : 10; + case "jar": + // use trust level of embedded URL + return getDefaultTrustLevel(URI.create(uri.toString().substring(4))); + case "modulepath": + return 20; + case "pkl": + return 0; + default: + return 10; + } + } + + /** + * Returns a {@link #standard standard security manager} with {@link #defaultAllowedModules + * default allowed modules}, {@link #defaultAllowedResources default allowed resources}, {@link + * #defaultTrustLevels default trust levels}, and no root directory for modules and resources. + */ + public static final SecurityManager defaultManager = + new Standard(defaultAllowedModules, defaultAllowedResources, defaultTrustLevels, null); + + /** + * Creates a {@link SecurityManager} that determines whether a module can be resolved based on the + * given list of module URI patterns, whether an external resources can be read based on the given + * list of resource URI patterns, and whether a module can import another module based on the + * given module trust levels. A module can only import modules with the same or a lower trust + * level. + * + *

If {@code rootDir} is non-null, access to file-based modules and resources is restricted to + * those located under {@code rootDir}. Any symlinks are resolved before this check is performed. + */ + public static SecurityManager standard( + List allowedModules, + List allowedResources, + Function trustLevels, + @Nullable Path rootDir) { + return new Standard(allowedModules, allowedResources, trustLevels, rootDir); + } + + /** Creates an unconfigured builder for setting up a standard {@link SecurityManager}. */ + public static StandardBuilder standardBuilder() { + return new StandardBuilder(); + } + + private static class Standard implements SecurityManager { + private final List allowedModules; + private final List allowedResources; + private final Function trustLevels; + private final @Nullable Path rootDir; + + Standard( + List allowedModules, + List allowedResources, + Function trustLevels, + @Nullable Path rootDir) { + this.allowedModules = allowedModules; + this.allowedResources = allowedResources; + this.trustLevels = trustLevels; + this.rootDir = normalizePath(rootDir); + } + + @Override + public void checkResolveModule(URI uri) throws SecurityManagerException { + checkRead(uri, allowedModules, "moduleNotInAllowList"); + } + + @Override + public void checkResolveResource(URI resource) throws SecurityManagerException { + checkRead(resource, allowedResources, "resourceNotInAllowList"); + } + + @Override + public void checkReadResource(URI uri) throws SecurityManagerException { + checkRead(uri, allowedResources, "resourceNotInAllowList"); + } + + @Override + public void checkImportModule(URI importingModule, URI importedModule) + throws SecurityManagerException { + var importingTrustLevel = trustLevels.apply(importingModule); + var importedTrustLevel = trustLevels.apply(importedModule); + + if (importingTrustLevel < importedTrustLevel) { + var message = + ErrorMessages.create("insufficientModuleTrustLevel", importedModule, importingModule); + throw new SecurityManagerException(message); + } + } + + private @Nullable Path normalizePath(@Nullable Path path) { + if (path == null) { + return null; + } + try { + if (Files.exists(path)) { + return path.toRealPath(); + } + return path.toAbsolutePath(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void checkRead(URI uri, List allowedPatterns, String errorMessageKey) + throws SecurityManagerException { + for (var pattern : allowedPatterns) { + if (pattern.matcher(uri.toString()).lookingAt()) { + checkIsUnderRootDir(uri, errorMessageKey); + return; + } + } + + var message = ErrorMessages.create(errorMessageKey, uri); + throw new SecurityManagerException(message); + } + + private void checkIsUnderRootDir(URI uri, String errorMessageKey) + throws SecurityManagerException { + if (!uri.isAbsolute()) { + throw new AssertionError("Expected absolute URI but got: " + uri); + } + + if (rootDir == null || !uri.getScheme().equals("file")) return; + + var path = Path.of(uri); + if (Files.exists(path)) { + try { + path = path.toRealPath(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else { + // perform check even if file doesn't exist + // to avoid leaking information on whether files outside root dir exist + path = path.normalize(); + } + + if (!path.startsWith(rootDir)) { + var message = ErrorMessages.create(errorMessageKey, uri); + throw new SecurityManagerException(message); + } + } + } + + public static class StandardBuilder implements SecurityManagerBuilder { + + private final List allowedModules = new ArrayList<>(); + + private final List allowedResources = new ArrayList<>(); + + private Path rootDir; + + private StandardBuilder() {} + + @Override + public StandardBuilder addAllowedModule(Pattern pattern) { + allowedModules.add(pattern); + return this; + } + + @Override + public StandardBuilder addAllowedModules(Collection patterns) { + allowedModules.addAll(patterns); + return this; + } + + @Override + public StandardBuilder setAllowedModules(Collection patterns) { + allowedModules.clear(); + allowedModules.addAll(patterns); + return this; + } + + @Override + public List getAllowedModules() { + return allowedModules; + } + + @Override + public StandardBuilder addAllowedResource(Pattern pattern) { + allowedResources.add(pattern); + return this; + } + + @Override + public StandardBuilder addAllowedResources(Collection patterns) { + allowedResources.addAll(patterns); + return this; + } + + @Override + public StandardBuilder setAllowedResources(Collection patterns) { + allowedResources.clear(); + allowedResources.addAll(patterns); + return this; + } + + @Override + public List getAllowedResources() { + return allowedResources; + } + + @Override + public StandardBuilder setRootDir(@Nullable Path rootDir) { + this.rootDir = rootDir; + return this; + } + + @Override + public @Nullable Path getRootDir() { + return rootDir; + } + + @Override + public SecurityManager build() { + if (allowedResources.isEmpty() && allowedModules.isEmpty()) { + throw new IllegalStateException("No security manager set."); + } + + return new Standard(allowedModules, allowedResources, defaultTrustLevels, rootDir); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/StackFrame.java b/pkl-core/src/main/java/org/pkl/core/StackFrame.java new file mode 100644 index 00000000..6829c171 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/StackFrame.java @@ -0,0 +1,132 @@ +/** + * 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; + +import java.util.List; +import java.util.Objects; +import org.pkl.core.util.Nullable; + +/** An element of a Pkl stack trace. */ +// better name would be `StackTraceElement` +public final class StackFrame { + private final String moduleUri; + + // TODO: can we make this non-null? + private final @Nullable String memberName; + + private final List sourceLines; + private final int startLine; + private final int startColumn; + private final int endLine; + private final int endColumn; + + public StackFrame( + String moduleUri, + @Nullable String memberName, + List sourceLines, + int startLine, + int startColumn, + int endLine, + int endColumn) { + + assert startLine >= 1; + assert startColumn >= 1; + assert endLine >= 1; + assert endColumn >= 1; + assert startLine <= endLine; + assert startLine < endLine || startColumn <= endColumn; + + this.moduleUri = moduleUri; + this.memberName = memberName; + this.sourceLines = sourceLines; + this.startLine = startLine; + this.startColumn = startColumn; + this.endLine = endLine; + this.endColumn = endColumn; + } + + /** Returns the module URI to display for this frame. May not be a syntactically valid URI. */ + public String getModuleUri() { + return moduleUri; + } + + /** Returns a copy of this frame with the given module URI. */ + public StackFrame withModuleUri(String moduleUri) { + return new StackFrame( + moduleUri, memberName, sourceLines, startLine, startColumn, endLine, endColumn); + } + + /** Returns the qualified name of the property or function corresponding to this frame, if any. */ + public @Nullable String getMemberName() { + return memberName; + } + + /** + * Returns the lines of source code corresponding to this frame. The first line has line number + * {@link #getStartLine}. The last line has line number {@link #getEndLine()}. + */ + public List getSourceLines() { + return sourceLines; + } + + /** Returns the start line number (1-based) corresponding to this frame. */ + public int getStartLine() { + return startLine; + } + + /** Returns the start column number (1-based) corresponding to this frame. */ + public int getStartColumn() { + return startColumn; + } + + /** Returns the end line number (1-based) corresponding to this frame. */ + public int getEndLine() { + return endLine; + } + + /** Returns the end column number (1-based) corresponding to this frame. */ + public int getEndColumn() { + return endColumn; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof StackFrame)) return false; + + var other = (StackFrame) obj; + + if (startLine != other.startLine) return false; + if (startColumn != other.startColumn) return false; + if (endLine != other.endLine) return false; + if (endColumn != other.endColumn) return false; + if (!moduleUri.equals(other.moduleUri)) return false; + if (!Objects.equals(memberName, other.memberName)) return false; + return sourceLines.equals(other.sourceLines); + } + + @Override + public int hashCode() { + var result = moduleUri.hashCode(); + result = 31 * result + Objects.hashCode(memberName); + result = 31 * result + sourceLines.hashCode(); + result = 31 * result + startLine; + result = 31 * result + startColumn; + result = 31 * result + endLine; + result = 31 * result + endColumn; + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/StackFrameTransformer.java b/pkl-core/src/main/java/org/pkl/core/StackFrameTransformer.java new file mode 100644 index 00000000..efd421db --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/StackFrameTransformer.java @@ -0,0 +1,26 @@ +/** + * 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; + +import java.util.function.Function; + +/** Transforms stack frames to make Pkl stack trace output more useful. */ +@FunctionalInterface +public interface StackFrameTransformer extends Function { + default StackFrameTransformer andThen(StackFrameTransformer transformer) { + return stackTrace -> transformer.apply(apply(stackTrace)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/StackFrameTransformers.java b/pkl-core/src/main/java/org/pkl/core/StackFrameTransformers.java new file mode 100644 index 00000000..42d4bc7b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/StackFrameTransformers.java @@ -0,0 +1,120 @@ +/** + * 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; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.stream.StreamSupport; +import org.pkl.core.packages.PackageAssetUri; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.settings.PklSettings; +import org.pkl.core.util.IoUtils; + +/** Static factory for stack frame transformers. */ +public final class StackFrameTransformers { + + private StackFrameTransformers() {} + + public static final StackFrameTransformer empty = s -> s; + + public static final StackFrameTransformer convertStdLibUrlToExternalUrl = + frame -> { + var uri = frame.getModuleUri(); + if (uri.startsWith("pkl:")) { + var moduleName = uri.substring(4); + return frame.withModuleUri( + Release.current() + .sourceCode() + .getFilePage("stdlib/" + moduleName + ".pkl#L" + frame.getStartLine())); + } + return frame; + }; + + public static StackFrameTransformer replacePackageUriWithSourceCodeUrl = + frame -> { + var uri = URI.create(frame.getModuleUri()); + if (!uri.getScheme().equalsIgnoreCase("package")) { + return frame; + } + try { + var assetUri = new PackageAssetUri(uri); + var packageResolver = VmContext.get(null).getPackageResolver(); + assert packageResolver != null; + var pkg = packageResolver.getDependencyMetadata(assetUri.getPackageUri(), null); + var sourceCode = pkg.getSourceCodeUrlScheme(); + if (sourceCode == null) { + return frame; + } + return transformUri(frame, uri.getFragment(), sourceCode); + } catch (IOException | URISyntaxException | SecurityManagerException e) { + // should never get here. by this point, we should have already performed all validation + // and downloaded all assets. + throw PklBugException.unreachableCode(); + } + }; + + public static final StackFrameTransformer fromServiceProviders = loadFromServiceProviders(); + + public static final StackFrameTransformer defaultTransformer = + fromServiceProviders + .andThen(convertStdLibUrlToExternalUrl) + .andThen(replacePackageUriWithSourceCodeUrl); + + private static StackFrame transformUri(StackFrame frame, String path, String format) { + var uri = frame.getModuleUri(); + var newUri = + format + .replace("%{path}", path) + .replace("%{url}", uri) + .replace("%{line}", String.valueOf(frame.getStartLine())) + .replace("%{endLine}", String.valueOf(frame.getEndLine())) + .replace("%{column}", String.valueOf(frame.getStartColumn())) + .replace("%{endColumn}", String.valueOf(frame.getEndColumn())); + return frame.withModuleUri(newUri); + } + + public static StackFrameTransformer convertFilePathToUriScheme(String scheme) { + return frame -> { + var uri = frame.getModuleUri(); + if (!uri.startsWith("file:")) return frame; + + return transformUri(frame, Path.of(URI.create(uri)).toString(), scheme); + }; + } + + public static StackFrameTransformer relativizeModuleUri(URI baseUri) { + return frame -> { + var uri = URI.create(frame.getModuleUri()); + var relativized = baseUri.relativize(uri); + return frame.withModuleUri(relativized.toString()); + }; + } + + public static StackFrameTransformer createDefault(PklSettings settings) { + return defaultTransformer + // order is relevant + .andThen(convertFilePathToUriScheme(settings.getEditor().getUrlScheme())); + } + + private static StackFrameTransformer loadFromServiceProviders() { + var loader = IoUtils.createServiceLoader(StackFrameTransformer.class); + return StreamSupport.stream(loader.spliterator(), false) + .reduce((t1, t2) -> t1.andThen(t2)) + .orElse(t -> t); // use no-op transformer if no service providers found + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/TypeAlias.java b/pkl-core/src/main/java/org/pkl/core/TypeAlias.java new file mode 100644 index 00000000..94b5dc8f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/TypeAlias.java @@ -0,0 +1,104 @@ +/** + * 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; + +import java.util.List; +import java.util.Set; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +/** Java representation of a {@code pkl.base#TypeAlias} value. */ +public final class TypeAlias extends Member implements Value { + private static final long serialVersionUID = 0L; + + private final String moduleName; + private final String qualifiedName; + private final List typeParameters; + + @LateInit private PType aliasedType; + + public TypeAlias( + @Nullable String docComment, + SourceLocation sourceLocation, + Set modifiers, + List annotations, + String simpleName, + String moduleName, + String qualifiedName, + List typeParameters) { + super(docComment, sourceLocation, modifiers, annotations, simpleName); + this.moduleName = moduleName; + this.qualifiedName = qualifiedName; + this.typeParameters = typeParameters; + } + + public void initAliasedType(PType type) { + assert aliasedType == null; + aliasedType = type; + } + + /** + * Returns the name of the module that this type alias is declared in. Note that a module name is + * not guaranteed to be unique, especially if it not declared but inferred from the module URI. + */ + public String getModuleName() { + return moduleName; + } + + /** + * Returns the qualified name of this type alias, `moduleName#typeAliasName`. Note that a + * qualified type alias name is not guaranteed to be unique, especially if the module name is not + * declared but inferred from the module URI. + */ + public String getQualifiedName() { + return qualifiedName; + } + + /** Returns the name of this type alias for use in user-facing messages. */ + public String getDisplayName() { + // display `String` rather than `pkl.base#String`, etc. + return moduleName.equals("pkl.base") ? getSimpleName() : qualifiedName; + } + + public List getTypeParameters() { + return typeParameters; + } + + /** Returns the type that this type alias stands for. */ + public PType getAliasedType() { + assert aliasedType != null; + return aliasedType; + } + + @Override + public void accept(ValueVisitor visitor) { + visitor.visitTypeAlias(this); + } + + @Override + public T accept(ValueConverter converter) { + return converter.convertTypeAlias(this); + } + + @Override + public PClassInfo getClassInfo() { + return PClassInfo.TypeAlias; + } + + public String toString() { + return getDisplayName(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/TypeParameter.java b/pkl-core/src/main/java/org/pkl/core/TypeParameter.java new file mode 100644 index 00000000..fd954f5a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/TypeParameter.java @@ -0,0 +1,80 @@ +/** + * 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; + +import java.io.Serializable; +import org.pkl.core.util.LateInit; + +/** A type parameter of a generic class, type alias, or method. */ +public final class TypeParameter implements Serializable { + + private static final long serialVersionUID = 0L; + + private final Variance variance; + private final String name; + private final int index; + + @LateInit private volatile Member owner; + + public TypeParameter(Variance variance, String name, int index) { + this.variance = variance; + this.name = name; + this.index = index; + } + + /** + * Initializes the generic class, type alias, or method that this type parameter belongs to. This + * method must be called as part of initializing this object. It is kept separate from the + * constructor to help clients avoid circular evaluation. + */ + public void initOwner(Member owner) { + assert this.owner == null; + this.owner = owner; + } + + /** Returns the generic class, type alias, or method that this type parameter belongs to. */ + public Member getOwner() { + assert owner != null; + return owner; + } + + /** Returns the variance of this type parameter. */ + public Variance getVariance() { + return variance; + } + + /** Returns the name of this type parameter. */ + public String getName() { + return name; + } + + /** + * Returns the index of this type parameter in its owner's type parameter list, starting from + * zero. + */ + public int getIndex() { + return index; + } + + /** The variance of a type parameter. */ + public enum Variance { + INVARIANT, + /** An `out` parameter. */ + COVARIANT, + /** An `in` parameter. */ + CONTRAVARIANT + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Value.java b/pkl-core/src/main/java/org/pkl/core/Value.java new file mode 100644 index 00000000..a5c11f5d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Value.java @@ -0,0 +1,48 @@ +/** + * 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; + +import java.io.Serializable; + +/** + * Java representation of a Pkl value. + * + *

The following Pkl values aren't represented as {@code Value} but as instances of Java standard + * library classes: + * + *

    + *
  • {@code pkl.base#String}: {@link java.lang.String} + *
  • {@code pkl.base#Boolean}: {@link java.lang.Boolean} + *
  • {@code pkl.base#Int}: {@link java.lang.Long} + *
  • {@code pkl.base#Float}: {@link java.lang.Double} + *
  • {@code pkl.base#List}: {@link java.util.List} + *
  • {@code pkl.base#Set}: {@link java.util.Set} + *
  • {@code pkl.base#Map}: {@link java.util.Map} + *
  • {@code pkl.base#Listing}: {@link java.util.List} + *
  • {@code pkl.base#Mapping}: {@link java.util.Map} + *
  • {@code pkl.base#Regex}: {@link java.util.regex.Pattern} + *
+ */ +public interface Value extends Serializable { + /** Invokes the given visitor's visit method for this {@code Value}. */ + void accept(ValueVisitor visitor); + + /** Invokes the given converters's convert method for this {@code Value}. */ + T accept(ValueConverter converter); + + /** Returns information about the Pkl class associated with this {@code Value}. */ + PClassInfo getClassInfo(); +} diff --git a/pkl-core/src/main/java/org/pkl/core/ValueConverter.java b/pkl-core/src/main/java/org/pkl/core/ValueConverter.java new file mode 100644 index 00000000..613a740e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ValueConverter.java @@ -0,0 +1,81 @@ +/** + * 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; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** Converter for data models generated by [Evaluator]. */ +@SuppressWarnings("unused") +public interface ValueConverter { + T convertNull(); + + T convertString(String value); + + T convertBoolean(Boolean value); + + T convertInt(Long value); + + T convertFloat(Double value); + + T convertDuration(Duration value); + + T convertDataSize(DataSize value); + + T convertPair(Pair value); + + T convertList(List value); + + T convertSet(Set value); + + T convertMap(Map value); + + T convertObject(PObject value); + + T convertModule(PModule value); + + T convertClass(PClass value); + + T convertTypeAlias(TypeAlias value); + + T convertRegex(Pattern value); + + default T convert(Object value) { + if (value instanceof Value) { + return ((Value) value).accept(this); + } else if (value instanceof String) { + return convertString((String) value); + } else if (value instanceof Boolean) { + return convertBoolean((Boolean) value); + } else if (value instanceof Long) { + return convertInt((Long) value); + } else if (value instanceof Double) { + return convertFloat((Double) value); + } else if (value instanceof List) { + return convertList((List) value); + } else if (value instanceof Set) { + return convertSet((Set) value); + } else if (value instanceof Map) { + return convertMap((Map) value); + } else if (value instanceof Pattern) { + return convertRegex((Pattern) value); + } else { + throw new IllegalArgumentException("Cannot convert value with unexpected type: " + value); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ValueFormatter.java b/pkl-core/src/main/java/org/pkl/core/ValueFormatter.java new file mode 100644 index 00000000..0e46cb56 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ValueFormatter.java @@ -0,0 +1,336 @@ +/** + * 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; + +import java.io.IOException; +import org.pkl.core.util.ArrayCharEscaper; + +public final class ValueFormatter { + private static final ArrayCharEscaper charEscaper = + ArrayCharEscaper.builder() + .withEscape('\n', "\\n") + .withEscape('\r', "\\r") + .withEscape('\t', "\\t") + .withEscape('"', "\\\"") + .withEscape('\\', "\\\\") + .build(); + + private static final ValueFormatter BASIC = new ValueFormatter(false, false); + + private static final ValueFormatter WITH_CUSTOM_DELIMITERS = new ValueFormatter(false, true); + + private final boolean useMultilineStrings; + private final boolean useCustomStringDelimiters; + + /** Equivalent to {@code new ValueFormatter(false, false)}. */ + public static ValueFormatter basic() { + return BASIC; + } + + /** Equivalent to {@code new ValueFormatter(false, true)}. */ + public static ValueFormatter withCustomStringDelimiters() { + return WITH_CUSTOM_DELIMITERS; + } + + /** + * Constructs an instance of a {@link ValueFormatter}. + * + *

If {@code useMultilineStrings} is {@code true}, string values containing newline characters + * are formatted as multiline string literals. + * + *

If {@code useCustomStringDelimiters} is {@code true}, custom string delimiters (such as + * {@code #"..."#}) are preferred over escaping quotes and backslashes. + */ + public ValueFormatter(boolean useMultilineStrings, boolean useCustomStringDelimiters) { + this.useMultilineStrings = useMultilineStrings; + this.useCustomStringDelimiters = useCustomStringDelimiters; + } + + /** + * Formats {@code value} as a Pkl/Pcf string literal (including quotes). + * + *

If {@code value} contains a {@code \n} character, a multiline string literal is returned, + * and subsequent lines are indented by {@code lineIndent}. Otherwise, a single line string + * literal is returned. + */ + @SuppressWarnings("unused") + public String formatStringValue(String value, CharSequence lineIndent) { + StringBuilder builder = new StringBuilder(value.length() * 2); + formatStringValue(value, lineIndent, builder); + return builder.toString(); + } + + /** + * Same as {@link #formatStringValue(String, CharSequence)}, except that output goes to {@code + * builder}. + */ + public void formatStringValue(String value, CharSequence lineIndent, StringBuilder builder) { + try { + formatStringValue(value, lineIndent, (Appendable) builder); + } catch (IOException e) { + throw new AssertionError("unreachable"); + } + } + + /** + * Same as {@link #formatStringValue(String, CharSequence)}, except that output goes to {@code + * appendable} (which may cause {@link IOException}). + */ + public void formatStringValue(String value, CharSequence lineIndent, Appendable appendable) + throws IOException { + // Optimization: if we are rendering single line strings and not rendering custom string + // delimiters, there is no need to gather string facts. + if (!useMultilineStrings && !useCustomStringDelimiters) { + formatSingleLineString(value, appendable, ""); + return; + } + var stringFacts = StringFacts.gather(value); + var isMultiline = useMultilineStrings && stringFacts.isMultiline; + if (isMultiline) { + var poundChars = + useCustomStringDelimiters ? "#".repeat(stringFacts.poundCharCountMultiline) : ""; + formatMultilineString(value, lineIndent, appendable, poundChars); + } else { + var poundChars = + useCustomStringDelimiters ? "#".repeat(stringFacts.poundCharCountSingleLine) : ""; + formatSingleLineString(value, appendable, poundChars); + } + } + + private void formatSingleLineString(String value, Appendable appendable, String poundChars) + throws IOException { + appendable.append(poundChars).append('"'); + + var i = 0; + var escapeSequence = "\\" + poundChars; + + if (useCustomStringDelimiters) { + if (value.equals("\"")) { + // Edge case 1: If the string consists of a single quote, we must escape it. Otherwise the + // output is `#"""#`. + appendable.append(escapeSequence).append(value); + i = 1; + } else if (value.startsWith("\"\"")) { + // Edge case 2: If the string starts with two quotes, we must escape the second one. + // Otherwise, it will + // be interpreted as a multiline string start (e.g. `"""`). + appendable.append('"').append(escapeSequence).append('"'); + i = 2; + } + } + + for (; i < value.length(); i++) { + var ch = value.charAt(i); + switch (ch) { + case '\n': + appendable.append(escapeSequence).append('n'); + break; + case '\r': + appendable.append(escapeSequence).append('r'); + break; + case '\t': + appendable.append(escapeSequence).append('t'); + break; + case '\\': + if (useCustomStringDelimiters) { + appendable.append(ch); + } else { + appendable.append("\\\\"); + } + break; + case '"': + if (useCustomStringDelimiters) { + appendable.append(ch); + } else { + appendable.append("\\\""); + } + break; + default: + appendable.append(ch); + } + } + + appendable.append('"').append(poundChars); + } + + private void formatMultilineString( + String value, CharSequence lineIndent, Appendable appendable, String poundChars) + throws IOException { + var consecutiveQuotes = 0; + var escapeSequence = "\\" + poundChars; + + appendable.append(poundChars).append("\"\"\"\n").append(lineIndent); + + for (var i = 0; i < value.length(); i++) { + var ch = value.charAt(i); + switch (ch) { + case '\n': + appendable.append('\n').append(lineIndent); + consecutiveQuotes = 0; + break; + case '\r': + appendable.append(escapeSequence).append('r'); + consecutiveQuotes = 0; + break; + case '\t': + appendable.append(escapeSequence).append('t'); + consecutiveQuotes = 0; + break; + case '\\': + if (useCustomStringDelimiters) { + appendable.append(ch); + } else { + appendable.append("\\\\"); + } + consecutiveQuotes = 0; + break; + case '"': + if (consecutiveQuotes == 2 && !useCustomStringDelimiters) { + appendable.append("\\\""); + consecutiveQuotes = 0; + } else { + appendable.append('"'); + consecutiveQuotes += 1; + } + break; + default: + appendable.append(ch); + consecutiveQuotes = 0; + } + } + + appendable.append("\n").append(lineIndent).append("\"\"\"").append(poundChars); + } + + /** + * Stores basic facts about a string. This is used to assist with pretty-formatting Pcf strings; + * e.g. determining whether to render as a multiline string, and whether to wrap the string with + * {@code #} delimiters. + */ + private static final class StringFacts { + private final boolean isMultiline; + + /** The number of pound characters that should wrap a string if rendering as single line. */ + private final int poundCharCountSingleLine; + + /** The number of pound characters that should wrap a string if rendering as multiline. */ + private final int poundCharCountMultiline; + + private StringFacts( + boolean isMultiline, int poundCharCountSingleLine, int poundCharCountMultiline) { + this.isMultiline = isMultiline; + this.poundCharCountSingleLine = poundCharCountSingleLine; + this.poundCharCountMultiline = poundCharCountMultiline; + } + + /** + * Gathers the following pieces of information about a string: + * + *

    + *
  • What is the maximum number of consecutive pound characters that follow a multiline + * quote ({@code """})? + *
  • What is the maximum number of consecutive pound characters that follow a single line + * quote ({@code "})? + *
  • Are there newline characters in the string? + *
+ * + * This is used to assist with rendering custom delimited strings (e.g. {@code #"..."#}). + * + *

Algorithm: + * + *

    + *
  1. Determine the current token context (backlash, single line quote, multiline quote, + * other). + *
  2. If there is a current token context, count the number of pound characters succeeding + * that token. + *
  3. Keep track of the maximum number of pound characters for each token type. + *
+ */ + static StringFacts gather(final String value) { + var isMultiline = false; + var consecutiveQuoteCount = 0; + var currentPoundContext = PoundContext.OTHER; + var currentPoundCountSingleQuote = 0; + var currentPoundCountMultilineQuote = 0; + var currentPoundCountBackslash = 0; + var poundCountSingleQuote = 0; + var poundCountMultilineQuote = 0; + var poundCountBackslash = 0; + for (var i = 0; i < value.length(); i++) { + var ch = value.charAt(i); + switch (ch) { + case '\\': + currentPoundContext = PoundContext.BACKSLASH; + currentPoundCountBackslash = 1; + poundCountBackslash = Math.max(poundCountBackslash, currentPoundCountBackslash); + break; + case '"': + consecutiveQuoteCount += 1; + if (consecutiveQuoteCount < 3) { + currentPoundContext = PoundContext.SINGLELINE_QUOTE; + currentPoundCountSingleQuote = 1; + poundCountSingleQuote = Math.max(poundCountSingleQuote, currentPoundCountSingleQuote); + } else { + currentPoundContext = PoundContext.MULTILINE_QUOTE; + currentPoundCountMultilineQuote = 1; + poundCountMultilineQuote = + Math.max(poundCountMultilineQuote, currentPoundCountMultilineQuote); + } + break; + case '#': + consecutiveQuoteCount = 0; + switch (currentPoundContext) { + case SINGLELINE_QUOTE: + currentPoundCountSingleQuote += 1; + poundCountSingleQuote = + Math.max(poundCountSingleQuote, currentPoundCountSingleQuote); + break; + case MULTILINE_QUOTE: + currentPoundCountMultilineQuote += 1; + poundCountMultilineQuote = + Math.max(poundCountMultilineQuote, currentPoundCountMultilineQuote); + break; + case BACKSLASH: + currentPoundCountBackslash += 1; + poundCountBackslash = Math.max(poundCountBackslash, currentPoundCountBackslash); + break; + default: + break; + } + break; + case '\n': + isMultiline = true; + default: + consecutiveQuoteCount = 0; + currentPoundContext = PoundContext.OTHER; + break; + } + } + return new StringFacts( + isMultiline, + Math.max(poundCountBackslash, poundCountSingleQuote), + Math.max(poundCountBackslash, poundCountMultilineQuote)); + } + + /** Represents the context in which the pound character ({@code #}) succeeds. */ + private enum PoundContext { + OTHER, + SINGLELINE_QUOTE, + MULTILINE_QUOTE, + BACKSLASH, + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ValueRenderer.java b/pkl-core/src/main/java/org/pkl/core/ValueRenderer.java new file mode 100644 index 00000000..5bdab098 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ValueRenderer.java @@ -0,0 +1,32 @@ +/** + * 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; + +/** Renders Pkl values in some output format. */ +public interface ValueRenderer { + /** + * Renders the given value as a complete document. + * + *

Some renderers impose restrictions on which types of values can be rendered as document. + * + *

A typical implementation of this method renders a document header/footer and otherwise + * delegates to {@link #renderValue}. + */ + void renderDocument(Object value); + + /** Renders the given value. */ + void renderValue(Object value); +} diff --git a/pkl-core/src/main/java/org/pkl/core/ValueRenderers.java b/pkl-core/src/main/java/org/pkl/core/ValueRenderers.java new file mode 100644 index 00000000..f902450a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ValueRenderers.java @@ -0,0 +1,70 @@ +/** + * 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; + +import java.io.Writer; + +/** Predefined {@link ValueRenderer}s for Pcf, JSON, YAML, and XML property lists. */ +public final class ValueRenderers { + private ValueRenderers() {} + + /** + * Creates a renderer for Pcf, a static subset of Pkl. If {@code omitNullProperties} is {@code + * true}, object properties whose value is {@code null} will not be rendered. If {@code + * useCustomDelimiters} is {@code true}, custom string delimiters (such as {@code #"..."#}) are + * preferred over escaping quotes and backslashes. + */ + public static ValueRenderer pcf( + Writer writer, String indent, boolean omitNullProperties, boolean useCustomStringDelimiters) { + return new PcfRenderer(writer, indent, omitNullProperties, useCustomStringDelimiters); + } + + /** + * Creates a renderer for JSON. If {@code omitNullProperties} is {@code true}, object properties + * whose value is {@code null} will not be rendered. + */ + public static ValueRenderer json(Writer writer, String indent, boolean omitNullProperties) { + return new JsonRenderer(writer, indent, omitNullProperties); + } + + /** + * Creates a renderer for YAML. If {@code omitNullProperties} is {@code true}, object properties + * whose value is {@code null} will not be rendered. If {@code isStream} is {@code true}, {@link + * ValueRenderer#renderDocument} expects an argument of type {@link Iterable} and renders it as + * YAML stream. + */ + public static ValueRenderer yaml( + Writer writer, int indent, boolean omitNullProperties, boolean isStream) { + return new YamlRenderer(writer, indent, omitNullProperties, isStream); + } + + /** Creates a renderer for XML property lists. */ + public static ValueRenderer plist(Writer writer, String indent) { + return new PListRenderer(writer, indent); + } + + /** + * Creates a renderer for {@link java.util.Properties} file format. If {@code omitNullProperties} + * is {@code true}, object properties and map entries whose value is {@code null} will not be + * rendered. If {@code restrictCharset} is {@code true} characters outside the printable US-ASCII + * charset range will be rendererd as Unicode escapes (see + * https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.3). + */ + public static ValueRenderer properties( + Writer writer, boolean omitNullProperties, boolean restrictCharset) { + return new PropertiesRenderer(writer, omitNullProperties, restrictCharset); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ValueVisitor.java b/pkl-core/src/main/java/org/pkl/core/ValueVisitor.java new file mode 100644 index 00000000..8725ae60 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ValueVisitor.java @@ -0,0 +1,115 @@ +/** + * 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; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.pkl.core.util.Nullable; + +/** Visitor for data models generated by [Evaluator]. */ +public interface ValueVisitor { + default void visitDefault(@Nullable Object value) {} + + default void visitNull() { + visitDefault(null); + } + + default void visitString(String value) { + visitDefault(value); + } + + default void visitBoolean(Boolean value) { + visitDefault(value); + } + + default void visitInt(Long value) { + visitDefault(value); + } + + default void visitFloat(Double value) { + visitDefault(value); + } + + default void visitDuration(Duration value) { + visitDefault(value); + } + + default void visitDataSize(DataSize value) { + visitDefault(value); + } + + default void visitPair(Pair value) { + visitDefault(value); + } + + default void visitList(List value) { + visitDefault(value); + } + + default void visitSet(Set value) { + visitDefault(value); + } + + default void visitMap(Map value) { + visitDefault(value); + } + + default void visitObject(PObject value) { + visitDefault(value); + } + + default void visitModule(PModule value) { + visitDefault(value); + } + + default void visitClass(PClass value) { + visitDefault(value); + } + + default void visitTypeAlias(TypeAlias value) { + visitDefault(value); + } + + default void visitRegex(Pattern value) { + visitDefault(value); + } + + default void visit(Object value) { + if (value instanceof Value) { + ((Value) value).accept(this); + } else if (value instanceof String) { + visitString((String) value); + } else if (value instanceof Boolean) { + visitBoolean((Boolean) value); + } else if (value instanceof Long) { + visitInt((Long) value); + } else if (value instanceof Double) { + visitFloat((Double) value); + } else if (value instanceof List) { + visitList((List) value); + } else if (value instanceof Set) { + visitSet((Set) value); + } else if (value instanceof Map) { + visitMap((Map) value); + } else if (value instanceof Pattern) { + visitRegex((Pattern) value); + } else { + throw new IllegalArgumentException("Cannot visit value with unexpected type: " + value); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/Version.java b/pkl-core/src/main/java/org/pkl/core/Version.java new file mode 100644 index 00000000..5fd66075 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Version.java @@ -0,0 +1,255 @@ +/** + * 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; + +import java.util.*; +import java.util.regex.*; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +/** + * A semantic version (https://semver.org/spec/v2.0.0.html). + * + *

This class guarantees that valid semantic version numbers are handled correctly, but does + * not guarantee that invalid semantic version numbers are rejected. + */ +// copied by `org.pkl.executor.Version` to avoid dependency on pkl-core +@SuppressWarnings("Duplicates") +public final class Version implements Comparable { + // https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions + private static final Pattern VERSION = + Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)(?:-([^+]+))?(?:\\+(.+))?"); + + private static final Pattern NUMERIC_IDENTIFIER = Pattern.compile("(0|[1-9]\\d*)"); + + private static final Comparator COMPARATOR = + Comparator.comparingInt(Version::getMajor) + .thenComparingInt(Version::getMinor) + .thenComparingInt(Version::getPatch) + .thenComparing( + (v1, v2) -> { + if (v1.preRelease == null) return v2.preRelease == null ? 0 : 1; + if (v2.preRelease == null) return -1; + var ids1 = v1.getPreReleaseIdentifiers(); + var ids2 = v2.getPreReleaseIdentifiers(); + var minSize = Math.min(ids1.length, ids2.length); + for (var i = 0; i < minSize; i++) { + var result = ids1[i].compareTo(ids2[i]); + if (result != 0) return result; + } + return Integer.compare(ids1.length, ids2.length); + }); + + private final int major; + private final int minor; + private final int patch; + private final @Nullable String preRelease; + private final @Nullable String build; + + @LateInit private volatile Identifier[] __preReleaseIdentifiers; + + /** Constructs a semantic version. */ + public Version( + int major, int minor, int patch, @Nullable String preRelease, @Nullable String build) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preRelease = preRelease; + this.build = build; + } + + /** + * Parses the given string as a semantic version number. + * + *

Throws {@link IllegalArgumentException} if the given string could not be parsed as a + * semantic version number or is too large to fit into a {@link Version}. + */ + public static Version parse(String version) { + var result = parseOrNull(version); + if (result != null) return result; + + if (VERSION.matcher(version).matches()) { + throw new IllegalArgumentException( + String.format("`%s` is too large to fit into a Version.", version)); + } + + throw new IllegalArgumentException( + String.format("`%s` could not be parsed as a semantic version number.", version)); + } + + /** + * Parses the given string as a semantic version number. + * + *

Returns {@code null} if the given string could not be parsed as a semantic version number or + * is too large to fit into a {@link Version}. + */ + public static @Nullable Version parseOrNull(String version) { + var matcher = VERSION.matcher(version); + if (!matcher.matches()) return null; + + try { + return new Version( + Integer.parseInt(matcher.group(1)), + Integer.parseInt(matcher.group(2)), + Integer.parseInt(matcher.group(3)), + matcher.group(4), + matcher.group(5)); + } catch (NumberFormatException e) { + return null; + } + } + + /** Returns a comparator for semantic versions. */ + public static Comparator comparator() { + return COMPARATOR; + } + + /** Returns the major version. */ + public int getMajor() { + return major; + } + + /** Returns a copy of this version with the given major version. */ + public Version withMajor(int major) { + return new Version(major, minor, patch, preRelease, build); + } + + /** Returns the minor version. */ + public int getMinor() { + return minor; + } + + /** Returns a copy of this version with the given minor version. */ + public Version withMinor(int minor) { + return new Version(major, minor, patch, preRelease, build); + } + + /** Returns the patch version. */ + public int getPatch() { + return patch; + } + + /** Returns a copy of this version with the given patch version. */ + public Version withPatch(int patch) { + return new Version(major, minor, patch, preRelease, build); + } + + /** Returns the pre-release version (if any). */ + public @Nullable String getPreRelease() { + return preRelease; + } + + /** Returns a copy of this version with the given pre-release version. */ + public Version withPreRelease(@Nullable String preRelease) { + return new Version(major, minor, patch, preRelease, build); + } + + /** Returns the build metadata (if any). */ + public @Nullable String getBuild() { + return build; + } + + /** Returns a copy of this version with the given build metadata. */ + public Version withBuild(@Nullable String build) { + return new Version(major, minor, patch, preRelease, build); + } + + /** Tells if this version has no pre-release version or build metadata. */ + public boolean isNormal() { + return preRelease == null && build == null; + } + + /** Tells if this version has a non-zero major version and no pre-release version. */ + public boolean isStable() { + return major != 0 && preRelease == null; + } + + /** Strips any pre-release version and build metadata from this version. */ + public Version toNormal() { + return preRelease == null && build == null + ? this + : new Version(major, minor, patch, null, null); + } + + /** Compares this version to the given version according to semantic versioning rules. */ + @Override + public int compareTo(Version other) { + return COMPARATOR.compare(this, other); + } + + /** Tells if this version is equal to {@code obj} according to semantic versioning rules. */ + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Version)) return false; + + var other = (Version) obj; + return major == other.major + && minor == other.minor + && patch == other.patch + && Objects.equals(preRelease, other.preRelease); + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch, preRelease); + } + + @Override + public String toString() { + return "" + + major + + "." + + minor + + "." + + patch + + (preRelease != null ? "-" + preRelease : "") + + (build != null ? "+" + build : ""); + } + + private Identifier[] getPreReleaseIdentifiers() { + if (__preReleaseIdentifiers == null) { + __preReleaseIdentifiers = + preRelease == null + ? new Identifier[0] + : Arrays.stream(preRelease.split("\\.")) + .map( + str -> + NUMERIC_IDENTIFIER.matcher(str).matches() + ? new Identifier(Long.parseLong(str), null) + : new Identifier(-1, str)) + .toArray(Identifier[]::new); + } + return __preReleaseIdentifiers; + } + + private static final class Identifier implements Comparable { + private final long numericId; + private final @Nullable String alphanumericId; + + Identifier(long numericId, @Nullable String alphanumericId) { + this.numericId = numericId; + this.alphanumericId = alphanumericId; + } + + @Override + public int compareTo(Identifier other) { + return alphanumericId != null + ? other.alphanumericId != null ? alphanumericId.compareTo(other.alphanumericId) : 1 + : other.alphanumericId != null ? -1 : Long.compare(numericId, other.numericId); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/YamlRenderer.java b/pkl-core/src/main/java/org/pkl/core/YamlRenderer.java new file mode 100644 index 00000000..b2bb082e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/YamlRenderer.java @@ -0,0 +1,236 @@ +/** + * 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; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.*; +import java.util.regex.Pattern; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.yaml.snake.YamlUtils; +import org.snakeyaml.engine.v2.api.DumpSettings; +import org.snakeyaml.engine.v2.api.StreamDataWriter; +import org.snakeyaml.engine.v2.common.FlowStyle; +import org.snakeyaml.engine.v2.emitter.Emitter; +import org.snakeyaml.engine.v2.events.*; +import org.snakeyaml.engine.v2.nodes.Tag; +import org.snakeyaml.engine.v2.resolver.ScalarResolver; + +final class YamlRenderer implements ValueRenderer { + private final ScalarResolver resolver = YamlUtils.getEmitterResolver("compat"); + private final Visitor visitor = new Visitor(); + private final Emitter emitter; + private final boolean omitNullProperties; + private final boolean isStream; + + public YamlRenderer(Writer writer, int indent, boolean omitNullProperties, boolean isStream) { + var dumpSettings = + DumpSettings.builder() + .setIndent(indent) + .setBestLineBreak("\n") + .setScalarResolver(resolver) + .build(); + + emitter = + new Emitter( + dumpSettings, + new StreamDataWriter() { + @Override + public void write(String str) { + try { + writer.write(str); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void write(String str, int off, int len) { + try { + writer.write(str, off, len); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }); + + this.omitNullProperties = omitNullProperties; + this.isStream = isStream; + } + + @Override + public void renderDocument(Object value) { + if (isStream) { + if (!(value instanceof Iterable)) { + throw new RendererException( + String.format( + "The top-level value of a YAML stream must have type `Collection`, but got type `%s`.", + value.getClass().getTypeName())); + } + var iterable = (Iterable) value; + emitter.emit(new StreamStartEvent()); + for (var elem : iterable) { + emitter.emit(new DocumentStartEvent(false, Optional.empty(), Map.of())); + visitor.visit(elem); + emitter.emit(new DocumentEndEvent(false)); + } + emitter.emit(new StreamEndEvent()); + } else { + // a top-level YAML value can have any type + renderValue(value); + } + } + + @Override + public void renderValue(Object value) { + emitter.emit(new StreamStartEvent()); + emitter.emit(new DocumentStartEvent(false, Optional.empty(), Map.of())); + + visitor.visit(value); + + emitter.emit(new DocumentEndEvent(false)); + emitter.emit(new StreamEndEvent()); + } + + protected class Visitor implements ValueVisitor { + @Override + public void visitString(String value) { + emitter.emit(YamlUtils.stringScalar(value, resolver)); + } + + @Override + public void visitInt(Long value) { + emitter.emit(YamlUtils.plainScalar(value.toString(), Tag.INT)); + } + + @Override + public void visitFloat(Double value) { + emitter.emit(YamlUtils.plainScalar(value.toString(), Tag.FLOAT)); + } + + @Override + public void visitBoolean(Boolean value) { + emitter.emit(YamlUtils.plainScalar(value.toString(), Tag.BOOL)); + } + + @Override + public void visitDuration(Duration value) { + throw new RendererException( + String.format("Values of type `Duration` cannot be rendered as YAML. Value: %s", value)); + } + + @Override + public void visitDataSize(DataSize value) { + throw new RendererException( + String.format("Values of type `DataSize` cannot be rendered as YAML. Value: %s", value)); + } + + @Override + public void visitPair(Pair value) { + doVisitIterable(value, null); + } + + @Override + public void visitList(List value) { + doVisitIterable(value, null); + } + + @Override + public void visitSet(Set value) { + doVisitIterable(value, "!!set"); + } + + @Override + public void visitMap(Map value) { + for (var key : value.keySet()) { + if (!(key instanceof String)) { + // http://stackoverflow.com/questions/33987316/what-is-a-complex-mapping-key-in-yaml + throw new RendererException( + String.format( + "Maps with non-String keys cannot currently be rendered as YAML. Key: %s", key)); + } + } + + @SuppressWarnings("unchecked") + var mapValue = (Map) value; + doVisitProperties(mapValue); + } + + @Override + public void visitObject(PObject value) { + doVisitProperties(value.getProperties()); + } + + @Override + public void visitModule(PModule value) { + doVisitProperties(value.getProperties()); + } + + @Override + public void visitClass(PClass value) { + throw new RendererException( + String.format( + "Values of type `Class` cannot be rendered as YAML. Value: %s", + value.getSimpleName())); + } + + @Override + public void visitTypeAlias(TypeAlias value) { + throw new RendererException( + String.format( + "Values of type `TypeAlias` cannot be rendered as YAML. Value: %s", + value.getSimpleName())); + } + + @Override + public void visitNull() { + emitter.emit(YamlUtils.plainScalar("null", Tag.NULL)); + } + + @Override + public void visitRegex(Pattern value) { + throw new RendererException( + String.format("Values of type `Regex` cannot be rendered as YAML. Value: %s", value)); + } + + private void doVisitIterable(Iterable iterable, @Nullable String tag) { + emitter.emit( + new SequenceStartEvent( + Optional.empty(), Optional.ofNullable(tag), true, FlowStyle.BLOCK)); + for (var elem : iterable) visit(elem); + emitter.emit(new SequenceEndEvent(Optional.empty(), Optional.empty())); + } + + private void doVisitProperties(Map properties) { + emitter.emit( + new MappingStartEvent(Optional.empty(), Optional.empty(), true, FlowStyle.BLOCK)); + + for (var entry : properties.entrySet()) { + var value = entry.getValue(); + + if (omitNullProperties && value instanceof PNull) { + continue; + } + + emitter.emit(YamlUtils.stringScalar(entry.getKey(), resolver)); + visit(value); + } + + emitter.emit(new MappingEndEvent(Optional.empty(), Optional.empty())); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/ConstantNode.java b/pkl-core/src/main/java/org/pkl/core/ast/ConstantNode.java new file mode 100644 index 00000000..29fc8374 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/ConstantNode.java @@ -0,0 +1,20 @@ +/** + * 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.ast; + +public interface ConstantNode { + Object getValue(); +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/ConstantValueNode.java b/pkl-core/src/main/java/org/pkl/core/ast/ConstantValueNode.java new file mode 100644 index 00000000..d2c592b0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/ConstantValueNode.java @@ -0,0 +1,44 @@ +/** + * 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.ast; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; + +@NodeInfo(shortName = "const") +public final class ConstantValueNode extends ExpressionNode implements ConstantNode { + private final Object value; + + public ConstantValueNode(SourceSection sourceSection, Object value) { + super(sourceSection); + this.value = value; + } + + public ConstantValueNode(Object value) { + this.value = value; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return value; + } + + @Override + public Object getValue() { + return value; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/ExpressionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/ExpressionNode.java new file mode 100644 index 00000000..a2ed8b1d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/ExpressionNode.java @@ -0,0 +1,46 @@ +/** + * 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.ast; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmTypesGen; +import org.pkl.core.runtime.VmUtils; + +public abstract class ExpressionNode extends PklNode { + protected ExpressionNode(SourceSection sourceSection) { + super(sourceSection); + } + + protected ExpressionNode() { + this(VmUtils.unavailableSourceSection()); + } + + public abstract Object executeGeneric(VirtualFrame frame); + + public long executeInt(VirtualFrame frame) throws UnexpectedResultException { + return VmTypesGen.expectLong(executeGeneric(frame)); + } + + public double executeFloat(VirtualFrame frame) throws UnexpectedResultException { + return VmTypesGen.expectDouble(executeGeneric(frame)); + } + + public boolean executeBoolean(VirtualFrame frame) throws UnexpectedResultException { + return VmTypesGen.expectBoolean(executeGeneric(frame)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/MemberLookupMode.java b/pkl-core/src/main/java/org/pkl/core/ast/MemberLookupMode.java new file mode 100644 index 00000000..aaf46156 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/MemberLookupMode.java @@ -0,0 +1,33 @@ +/** + * 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.ast; + +public enum MemberLookupMode { + /** Lookup of a local member in the lexical scope. */ + IMPLICIT_LOCAL, + + /** Lookup of a non-local member in the lexical scope. */ + IMPLICIT_LEXICAL, + + /** Member lookup whose implicit receiver is the {@code pkl.base} module. */ + IMPLICIT_BASE, + + /** Member lookup whose implicit receiver is {@code this}. */ + IMPLICIT_THIS, + + /** Member lookup with explicit receiver (e.g., {@code foo.bar}). */ + EXPLICIT_RECEIVER +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/MemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/MemberNode.java new file mode 100644 index 00000000..beb6b5af --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/MemberNode.java @@ -0,0 +1,95 @@ +/** + * 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.ast; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import java.util.function.Function; +import org.pkl.core.ast.member.DefaultPropertyBodyNode; +import org.pkl.core.ast.member.Member; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; + +public abstract class MemberNode extends PklRootNode { + protected final Member member; + @Child protected ExpressionNode bodyNode; + + protected MemberNode( + @Nullable VmLanguage language, + FrameDescriptor descriptor, + Member member, + ExpressionNode bodyNode) { + + super(language, descriptor); + this.member = member; + this.bodyNode = bodyNode; + } + + @Override + public final SourceSection getSourceSection() { + return member.getSourceSection(); + } + + public final SourceSection getHeaderSection() { + return member.getHeaderSection(); + } + + public final SourceSection getBodySection() { + return bodyNode.getSourceSection(); + } + + public final ExpressionNode getBodyNode() { + return bodyNode; + } + + @Override + public final String getName() { + return member.getQualifiedName(); + } + + public final void replaceBody(Function replacer) { + bodyNode = insert(replacer.apply(bodyNode)); + } + + protected final Object executeBody(VirtualFrame frame) { + return executeBody(frame, bodyNode); + } + + protected final VmExceptionBuilder exceptionBuilder() { + return new VmExceptionBuilder().withSourceSection(member.getHeaderSection()); + } + + /** + * If true, the property value computed by this node is not the final value exposed to user code + * but will still be amended. + * + *

Used to disable type check for to-be-amended properties. See {@link + * org.pkl.core.runtime.VmUtils#SKIP_TYPECHECK_MARKER}. IDEA: might be more appropriate to only + * skip constraints check + */ + protected final boolean shouldRunTypecheck(VirtualFrame frame) { + return frame.getArguments().length == 4 + && frame.getArguments()[3] == VmUtils.SKIP_TYPECHECK_MARKER; + } + + public boolean isUndefined() { + return bodyNode instanceof DefaultPropertyBodyNode + && ((DefaultPropertyBodyNode) bodyNode).isUndefined(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/PklNode.java b/pkl-core/src/main/java/org/pkl/core/ast/PklNode.java new file mode 100644 index 00000000..136b8fc2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/PklNode.java @@ -0,0 +1,64 @@ +/** + * 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.ast; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.TypeSystemReference; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmTypes; +import org.pkl.core.runtime.VmUtils; + +@NodeInfo(language = "Pkl") +@TypeSystemReference(VmTypes.class) +public abstract class PklNode extends Node { + protected final SourceSection sourceSection; + + protected PklNode(SourceSection sourceSection) { + this.sourceSection = sourceSection; + } + + protected PklNode() { + this(VmUtils.unavailableSourceSection()); + } + + @Override + public SourceSection getSourceSection() { + return sourceSection; + } + + @TruffleBoundary + protected VmExceptionBuilder exceptionBuilder() { + return new VmExceptionBuilder().withLocation(this); + } + + @TruffleBoundary + protected final String getShortName() { + return VmUtils.getNodeInfo(this).shortName(); + } + + @Override + @TruffleBoundary + public String toString() { + return String.format( + "(%s:%d) %s", + sourceSection.getSource().getName(), + sourceSection.getStartLine(), + sourceSection.getCharacters()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/PklRootNode.java b/pkl-core/src/main/java/org/pkl/core/ast/PklRootNode.java new file mode 100644 index 00000000..cb6de163 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/PklRootNode.java @@ -0,0 +1,57 @@ +/** + * 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.ast; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.TypeSystemReference; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.nodes.RootNode; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +@NodeInfo(language = "Pkl") +@TypeSystemReference(VmTypes.class) +public abstract class PklRootNode extends RootNode { + protected PklRootNode(@Nullable VmLanguage language, FrameDescriptor descriptor) { + super(language, descriptor); + } + + public abstract SourceSection getSourceSection(); + + public abstract @Nullable String getName(); + + protected final Object executeBody(VirtualFrame frame, ExpressionNode bodyNode) { + try { + return bodyNode.executeGeneric(frame); + } catch (VmException e) { + CompilerDirectives.transferToInterpreter(); + throw e; + } catch (StackOverflowError e) { + CompilerDirectives.transferToInterpreter(); + throw new VmStackOverflowException(e); + } catch (Exception e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().bug(e.getMessage()).withCause(e).build(); + } + } + + protected VmExceptionBuilder exceptionBuilder() { + return new VmExceptionBuilder().withLocation(this); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/SimpleRootNode.java b/pkl-core/src/main/java/org/pkl/core/ast/SimpleRootNode.java new file mode 100644 index 00000000..ac7ee1ee --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/SimpleRootNode.java @@ -0,0 +1,55 @@ +/** + * 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.ast; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmLanguage; + +public final class SimpleRootNode extends PklRootNode { + private final SourceSection sourceSection; + private final String qualifiedName; + @Child private ExpressionNode bodyNode; + + public SimpleRootNode( + VmLanguage language, + FrameDescriptor descriptor, + SourceSection sourceSection, + String qualifiedName, + ExpressionNode bodyNode) { + + super(language, descriptor); + this.sourceSection = sourceSection; + this.qualifiedName = qualifiedName; + this.bodyNode = bodyNode; + } + + @Override + public SourceSection getSourceSection() { + return sourceSection; + } + + @Override + public String getName() { + return qualifiedName; + } + + @Override + public Object execute(VirtualFrame frame) { + return executeBody(frame, bodyNode); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java b/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java new file mode 100644 index 00000000..7b86b23a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java @@ -0,0 +1,193 @@ +/** + * 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.ast; + +import java.util.EnumSet; +import java.util.Set; +import org.pkl.core.Modifier; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmSet; + +public final class VmModifier { + // user-facing modifiers + + public static final int ABSTRACT = 0x1; + + public static final int OPEN = 0x2; + + public static final int LOCAL = 0x4; + + // absent from rendered output but present in module schema (e.g. for pkldoc purposes) + public static final int HIDDEN = 0x8; + + public static final int EXTERNAL = 0x10; + + public static final int FIXED = 0x20; + + public static final int CONST = 0x40; + + // internal modifiers + + public static final int IMPORT = 0x80; + + public static final int CLASS = 0x100; + + public static final int TYPE_ALIAS = 0x200; + + public static final int ENTRY = 0x400; + + public static final int ELEMENT = 0x800; + + public static final int GLOB = 0x1000; + + // modifier sets + + public static final int NONE = 0; + + public static final int VALID_MODULE_MODIFIERS = ABSTRACT | OPEN; + + public static final int VALID_AMENDING_MODULE_MODIFIERS = 0; + + public static final int VALID_CLASS_MODIFIERS = ABSTRACT | OPEN | LOCAL | EXTERNAL; + + public static final int VALID_TYPE_ALIAS_MODIFIERS = LOCAL | EXTERNAL; + + public static final int VALID_METHOD_MODIFIERS = ABSTRACT | LOCAL | EXTERNAL | CONST; + + public static final int VALID_PROPERTY_MODIFIERS = + ABSTRACT | LOCAL | HIDDEN | EXTERNAL | FIXED | CONST; + + public static final int VALID_OBJECT_MEMBER_MODIFIERS = LOCAL; + + public static boolean isLocal(int modifiers) { + return (modifiers & LOCAL) != 0; + } + + public static boolean isAbstract(int modifiers) { + return (modifiers & ABSTRACT) != 0; + } + + public static boolean isFixed(int modifiers) { + return (modifiers & FIXED) != 0; + } + + public static boolean isOpen(int modifiers) { + return (modifiers & OPEN) != 0; + } + + public static boolean isHidden(int modifiers) { + return (modifiers & HIDDEN) != 0; + } + + public static boolean isExternal(int modifiers) { + return (modifiers & EXTERNAL) != 0; + } + + public static boolean isClass(int modifiers) { + return (modifiers & CLASS) != 0; + } + + public static boolean isTypeAlias(int modifiers) { + return (modifiers & TYPE_ALIAS) != 0; + } + + public static boolean isImport(int modifiers) { + return (modifiers & IMPORT) != 0; + } + + public static boolean isGlob(int modifiers) { + return (modifiers & GLOB) != 0; + } + + public static boolean isConst(int modifiers) { + return (modifiers & CONST) != 0; + } + + public static boolean isElement(int modifiers) { + return (modifiers & ELEMENT) != 0; + } + + public static boolean isEntry(int modifiers) { + return (modifiers & ENTRY) != 0; + } + + public static boolean isType(int modifiers) { + return (modifiers & (CLASS | TYPE_ALIAS | IMPORT)) != 0 && (modifiers & GLOB) == 0; + } + + public static boolean isLocalOrExternalOrHidden(int modifiers) { + return (modifiers & (LOCAL | EXTERNAL | HIDDEN)) != 0; + } + + public static boolean isLocalOrExternalOrAbstract(int modifiers) { + return (modifiers & (LOCAL | EXTERNAL | ABSTRACT)) != 0; + } + + public static boolean isConstOrFixed(int modifiers) { + return (modifiers & (CONST | FIXED)) != 0; + } + + public static Set export(int modifiers, boolean isClass) { + var result = EnumSet.noneOf(Modifier.class); + + if (isAbstract(modifiers)) result.add(Modifier.ABSTRACT); + if (isOpen(modifiers)) result.add(Modifier.OPEN); + if (isHidden(modifiers)) result.add(Modifier.HIDDEN); + // `external` modifier is part of class contract but not part of property/method contract + if (isExternal(modifiers) && isClass) result.add(Modifier.EXTERNAL); + + return result; + } + + public static String toString(int modifier) { + switch (modifier) { + case ABSTRACT: + return "abstract"; + case OPEN: + return "open"; + case LOCAL: + return "local"; + case HIDDEN: + return "hidden"; + case EXTERNAL: + return "external"; + default: + throw new VmExceptionBuilder() + .bug("Cannot convert internal modifier `%s` to a string.", toString(modifier)) + .build(); + } + } + + public static VmSet getMirrors(int modifiers, boolean isClass) { + var builder = VmSet.EMPTY.builder(); + + if (isAbstract(modifiers)) builder.add(toString(ABSTRACT)); + if (isOpen(modifiers)) builder.add(toString(OPEN)); + if (isHidden(modifiers)) builder.add(toString(HIDDEN)); + // `external` modifier is part of class contract but not part of property/method contract + if (isExternal(modifiers) && isClass) builder.add(toString(EXTERNAL)); + + return builder.build(); + } + + public static boolean isClosed(int modifiers) { + return (modifiers & (ABSTRACT | OPEN)) == 0; + } + + public static boolean isInstantiable(int modifiers) { + return (modifiers & (ABSTRACT | EXTERNAL)) == 0; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/AbstractAstBuilder.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/AbstractAstBuilder.java new file mode 100644 index 00000000..f2e2b760 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/AbstractAstBuilder.java @@ -0,0 +1,150 @@ +/** + * 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.ast.builder; + +import com.oracle.truffle.api.source.Source; +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.pkl.core.parser.antlr.PklLexer; +import org.pkl.core.parser.antlr.PklParser.ModifierContext; +import org.pkl.core.parser.antlr.PklParserBaseVisitor; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.util.Nullable; + +public abstract class AbstractAstBuilder extends PklParserBaseVisitor { + + protected final Source source; + + protected AbstractAstBuilder(Source source) { + this.source = source; + } + + protected abstract VmExceptionBuilder exceptionBuilder(); + + protected String doVisitSingleLineConstantStringPart(List ts) { + if (ts.isEmpty()) return ""; + + var builder = new StringBuilder(); + for (var token : ts) { + switch (token.getType()) { + case PklLexer.SLCharacters: + builder.append(token.getText()); + break; + case PklLexer.SLCharacterEscape: + builder.append(parseCharacterEscapeSequence(token)); + break; + case PklLexer.SLUnicodeEscape: + builder.appendCodePoint(parseUnicodeEscapeSequence(token)); + break; + default: + throw exceptionBuilder().unreachableCode().build(); + } + } + + return builder.toString(); + } + + protected int parseUnicodeEscapeSequence(Token token) { + var text = token.getText(); + var lastIndex = text.length() - 1; + + if (text.charAt(lastIndex) != '}') { + throw exceptionBuilder() + .evalError("unterminatedUnicodeEscapeSequence", token.getText()) + .withSourceSection(createSourceSection(token)) + .build(); + } + + var startIndex = text.indexOf('{', 2); + assert startIndex != -1; // guaranteed by lexer + + try { + return Integer.parseInt(text.substring(startIndex + 1, lastIndex), 16); + } catch (NumberFormatException e) { + throw exceptionBuilder() + .evalError("invalidUnicodeEscapeSequence", token.getText(), text.substring(0, startIndex)) + .withSourceSection(createSourceSection(token)) + .build(); + } + } + + protected String parseCharacterEscapeSequence(Token token) { + var text = token.getText(); + var lastChar = text.charAt(text.length() - 1); + + switch (lastChar) { + case 'n': + return "\n"; + case 'r': + return "\r"; + case 't': + return "\t"; + case '"': + return "\""; + case '\\': + return "\\"; + default: + throw exceptionBuilder() + .evalError("invalidCharacterEscapeSequence", text, text.substring(0, text.length() - 1)) + .withSourceSection(createSourceSection(token)) + .build(); + } + } + + protected final SourceSection createSourceSection(ParserRuleContext ctx) { + return createSourceSection(ctx.getStart(), ctx.getStop()); + } + + protected final SourceSection createSourceSection(TerminalNode node) { + return createSourceSection(node.getSymbol()); + } + + protected final @Nullable SourceSection createSourceSection(@Nullable Token token) { + return token != null ? createSourceSection(token, token) : null; + } + + protected final SourceSection createSourceSection(Token start, Token stop) { + return source.createSection( + start.getStartIndex(), stop.getStopIndex() - start.getStartIndex() + 1); + } + + protected final SourceSection createSourceSection( + List modifierCtxs, int symbol) { + + var modifierCtx = + modifierCtxs.stream().filter(ctx -> ctx.t.getType() == symbol).findFirst().orElseThrow(); + + return createSourceSection(modifierCtx); + } + + protected static SourceSection createSourceSection(Source source, ParserRuleContext ctx) { + var start = ctx.start.getStartIndex(); + var stop = ctx.stop.getStopIndex(); + return source.createSection(start, stop - start + 1); + } + + protected static @Nullable SourceSection createSourceSection( + Source source, @Nullable Token token) { + if (token == null) return null; + + var start = token.getStartIndex(); + var stop = token.getStopIndex(); + return source.createSection(start, stop - start + 1); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java new file mode 100644 index 00000000..8daaf4a9 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java @@ -0,0 +1,2985 @@ +/** + * 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.ast.builder; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameSlotKind; +import com.oracle.truffle.api.source.Source; +import com.oracle.truffle.api.source.SourceSection; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.PClassInfo; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.TypeParameter; +import org.pkl.core.TypeParameter.Variance; +import org.pkl.core.ast.*; +import org.pkl.core.ast.builder.SymbolTable.AnnotationScope; +import org.pkl.core.ast.builder.SymbolTable.ClassScope; +import org.pkl.core.ast.builder.SymbolTable.EntryScope; +import org.pkl.core.ast.builder.SymbolTable.Scope; +import org.pkl.core.ast.expression.binary.*; +import org.pkl.core.ast.expression.generator.*; +import org.pkl.core.ast.expression.literal.*; +import org.pkl.core.ast.expression.member.*; +import org.pkl.core.ast.expression.primary.*; +import org.pkl.core.ast.expression.ternary.IfElseNode; +import org.pkl.core.ast.expression.unary.*; +import org.pkl.core.ast.internal.GetBaseModuleClassNode; +import org.pkl.core.ast.internal.GetClassNodeGen; +import org.pkl.core.ast.internal.ToStringNodeGen; +import org.pkl.core.ast.lambda.ApplyVmFunction1NodeGen; +import org.pkl.core.ast.member.*; +import org.pkl.core.ast.type.*; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.module.ModuleKeys; +import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.parser.antlr.PklLexer; +import org.pkl.core.parser.antlr.PklParser.*; +import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.stdlib.LanguageAwareNode; +import org.pkl.core.stdlib.registry.ExternalMemberRegistry; +import org.pkl.core.stdlib.registry.MemberRegistryFactory; +import org.pkl.core.util.CollectionUtils; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; + +public final class AstBuilder extends AbstractAstBuilder { + private final VmLanguage language; + private final ModuleInfo moduleInfo; + + private final ModuleKey moduleKey; + private final ModuleResolver moduleResolver; + private final boolean isBaseModule; + private final boolean isStdLibModule; + private final ExternalMemberRegistry externalMemberRegistry; + private final SymbolTable symbolTable; + private final boolean isMethodReturnTypeChecked; + + public AstBuilder( + Source source, VmLanguage language, ModuleInfo moduleInfo, ModuleResolver moduleResolver) { + super(source); + this.language = language; + this.moduleInfo = moduleInfo; + + moduleKey = moduleInfo.getModuleKey(); + this.moduleResolver = moduleResolver; + isBaseModule = ModuleKeys.isBaseModule(moduleKey); + isStdLibModule = ModuleKeys.isStdLibModule(moduleKey); + externalMemberRegistry = MemberRegistryFactory.get(moduleKey); + symbolTable = new SymbolTable(moduleInfo); + isMethodReturnTypeChecked = !isStdLibModule || IoUtils.isTestMode(); + } + + public static AstBuilder create( + Source source, + VmLanguage language, + ModuleContext ctx, + ModuleKey moduleKey, + ResolvedModuleKey resolvedModuleKey, + ModuleResolver moduleResolver) { + var moduleDecl = ctx.moduleDecl(); + var moduleHeader = moduleDecl != null ? moduleDecl.moduleHeader() : null; + var sourceSection = createSourceSection(source, ctx); + var headerSection = + moduleHeader != null + ? createSourceSection(source, moduleHeader) + : + // no explicit module declaration; designate start of file as header section + source.createSection(0, 0); + var docComment = moduleDecl != null ? createSourceSection(source, moduleDecl.t) : null; + + ModuleInfo moduleInfo; + if (moduleDecl == null) { + var moduleName = IoUtils.inferModuleName(moduleKey); + moduleInfo = + new ModuleInfo( + sourceSection, headerSection, null, moduleName, moduleKey, resolvedModuleKey, false); + } else { + var declaredModuleName = moduleDecl.moduleHeader().qualifiedIdentifier(); + var moduleName = + declaredModuleName != null + ? declaredModuleName.getText() + : IoUtils.inferModuleName(moduleKey); + var clause = moduleDecl.moduleHeader().moduleExtendsOrAmendsClause(); + var isAmend = clause != null && clause.t.getType() == PklLexer.AMENDS; + moduleInfo = + new ModuleInfo( + sourceSection, + headerSection, + docComment, + moduleName, + moduleKey, + resolvedModuleKey, + isAmend); + } + + return new AstBuilder(source, language, moduleInfo, moduleResolver); + } + + @Override + public PklRootNode visitModule(ModuleContext ctx) { + var moduleDecl = ctx.moduleDecl(); + var moduleHeader = moduleDecl != null ? moduleDecl.moduleHeader() : null; + + var annotationNodes = + moduleDecl != null ? doVisitAnnotations(moduleDecl.annotation()) : new ExpressionNode[] {}; + + int modifiers; + if (moduleHeader == null) { + modifiers = VmModifier.NONE; + } else { + var modifierCtxs = moduleHeader.modifier(); + modifiers = + doVisitModifiers( + modifierCtxs, VmModifier.VALID_MODULE_MODIFIERS, "invalidModuleModifier"); + // doing this in a second step gives better error messages + if (moduleInfo.isAmend()) { + modifiers = + doVisitModifiers( + modifierCtxs, + VmModifier.VALID_AMENDING_MODULE_MODIFIERS, + "invalidAmendingModuleModifier"); + } + } + + var extendsOrAmendsClause = + moduleHeader != null ? moduleHeader.moduleExtendsOrAmendsClause() : null; + + var supermoduleNode = + extendsOrAmendsClause == null + ? resolveBaseModuleClass(Identifier.MODULE, BaseModule::getModuleClass) + : doVisitImport( + PklLexer.IMPORT, extendsOrAmendsClause, extendsOrAmendsClause.stringConstant()); + + var propertyNames = + CollectionUtils.newHashSet( + ctx.is.size() + ctx.cs.size() + ctx.ts.size() + ctx.ps.size()); + + if (!moduleInfo.isAmend()) { + var supertypeNode = + new UnresolvedTypeNode.Declared(supermoduleNode.getSourceSection(), supermoduleNode); + var moduleProperties = + doVisitModuleProperties(ctx.is, ctx.cs, ctx.ts, List.of(), propertyNames, moduleInfo); + var unresolvedPropertyNodes = doVisitClassProperties(ctx.ps, propertyNames); + + var classNode = + new ClassNode( + moduleInfo.getSourceSection(), + moduleInfo.getHeaderSection(), + moduleInfo.getDocComment(), + annotationNodes, + modifiers, + PClassInfo.forModuleClass( + moduleInfo.getModuleName(), moduleInfo.getModuleKey().getUri()), + List.of(), + moduleInfo, + supertypeNode, + moduleProperties, + unresolvedPropertyNodes, + doVisitMethodDefs(ctx.ms)); + + return new ModuleNode( + language, moduleInfo.getSourceSection(), moduleInfo.getModuleName(), classNode); + } + + var moduleProperties = + doVisitModuleProperties(ctx.is, ctx.cs, ctx.ts, ctx.ps, propertyNames, moduleInfo); + + for (var methodCtx : ctx.ms) { + var localMethod = doVisitObjectMethod(methodCtx.methodHeader(), methodCtx.expr(), true); + EconomicMaps.put(moduleProperties, localMethod.getName(), localMethod); + } + + var moduleNode = + AmendModuleNodeGen.create( + moduleInfo.getSourceSection(), + language, + annotationNodes, + moduleProperties, + moduleInfo, + supermoduleNode); + + return new ModuleNode( + language, moduleInfo.getSourceSection(), moduleInfo.getModuleName(), moduleNode); + } + + @Override + public ObjectMember visitClazz(ClazzContext ctx) { + var headerCtx = ctx.classHeader(); + + var sourceSection = createSourceSection(ctx); + var headerSection = createSourceSection(headerCtx); + + var bodyCtx = ctx.classBody(); + if (bodyCtx != null) { + checkClosingDelimiter(bodyCtx.err, "}", bodyCtx.stop); + } + + var typeParameters = visitTypeParameterList(headerCtx.typeParameterList()); + + List propertyCtxs = bodyCtx == null ? List.of() : bodyCtx.ps; + List methodCtxs = bodyCtx == null ? List.of() : bodyCtx.ms; + + var modifiers = + doVisitModifiers( + headerCtx.modifier(), VmModifier.VALID_CLASS_MODIFIERS, "invalidClassModifier") + | VmModifier.CLASS; + + var className = + Identifier.property(headerCtx.Identifier().getText(), VmModifier.isLocal(modifiers)); + + return symbolTable.enterClass( + className, + typeParameters, + scope -> { + var supertypeCtx = headerCtx.type(); + + // needs to be inside `enterClass` so that class' type parameters are in scope + var supertypeNode = + supertypeCtx != null + ? visitType(supertypeCtx) + : isBaseModule && className == Identifier.ANY + ? null + : new UnresolvedTypeNode.Declared( + VmUtils.unavailableSourceSection(), + resolveBaseModuleClass(Identifier.TYPED, BaseModule::getTypedClass)); + + if (!(supertypeNode == null + || supertypeNode instanceof UnresolvedTypeNode.Declared + || supertypeNode instanceof UnresolvedTypeNode.Parameterized + || supertypeNode instanceof UnresolvedTypeNode.Module)) { + throw exceptionBuilder() + .evalError("invalidSupertype", supertypeNode.getSourceSection().getCharacters()) + .withSourceSection(supertypeNode.getSourceSection()) + .build(); + } + + var classInfo = + PClassInfo.get( + moduleInfo.getModuleName(), + className.toString(), + moduleInfo.getModuleKey().getUri()); + var propertyNames = CollectionUtils.newHashSet(propertyCtxs.size()); + + var classNode = + new ClassNode( + sourceSection, + headerSection, + createSourceSection(ctx.t), + doVisitAnnotations(ctx.annotation()), + modifiers, + classInfo, + typeParameters, + null, + supertypeNode, + EconomicMaps.create(), + doVisitClassProperties(propertyCtxs, propertyNames), + doVisitMethodDefs(methodCtxs)); + + var result = + new ObjectMember( + sourceSection, + headerSection, + modifiers | VmModifier.CONST, + scope.getName(), + scope.getQualifiedName()); + + result.initMemberNode( + new UntypedObjectMemberNode( + language, scope.buildFrameDescriptor(), result, classNode)); + + return result; + }); + } + + @Override + public ObjectMember visitTypeAlias(TypeAliasContext ctx) { + var headerCtx = ctx.typeAliasHeader(); + var sourceSection = createSourceSection(ctx); + var headerSection = createSourceSection(headerCtx); + + var modifiers = + doVisitModifiers( + headerCtx.modifier(), + VmModifier.VALID_TYPE_ALIAS_MODIFIERS, + "invalidTypeAliasModifier") + | VmModifier.TYPE_ALIAS; + + var isLocal = VmModifier.isLocal(modifiers); + var name = Identifier.property(headerCtx.Identifier().getText(), isLocal); + + var typeParameters = visitTypeParameterList(headerCtx.typeParameterList()); + + return symbolTable.enterTypeAlias( + name, + typeParameters, + scope -> { + var scopeName = scope.getName(); + var typeAliasNode = + new TypeAliasNode( + sourceSection, + headerSection, + createSourceSection(ctx.t), + doVisitAnnotations(ctx.annotation()), + modifiers, + scopeName.toString(), + scope.getQualifiedName(), + typeParameters, + (UnresolvedTypeNode) ctx.type().accept(this)); + + var result = + new ObjectMember( + sourceSection, + headerSection, + modifiers | VmModifier.CONST, + scopeName, + scope.getQualifiedName()); + + result.initMemberNode( + new UntypedObjectMemberNode( + language, scope.buildFrameDescriptor(), result, typeAliasNode)); + + return result; + }); + } + + @Override + public UnresolvedTypeNode[] visitTypeArgumentList(@Nullable TypeArgumentListContext ctx) { + if (ctx == null) return new UnresolvedTypeNode[0]; + + checkCommaSeparatedElements(ctx, ctx.ts, ctx.errs); + checkClosingDelimiter(ctx.err, ">", ctx.stop); + + var result = new UnresolvedTypeNode[ctx.ts.size()]; + for (int i = 0; i < ctx.ts.size(); i++) { + result[i] = (UnresolvedTypeNode) ctx.ts.get(i).accept(this); + } + return result; + } + + @Override + public List visitTypeParameterList(@Nullable TypeParameterListContext ctx) { + if (ctx == null) return List.of(); + + checkCommaSeparatedElements(ctx, ctx.ts, ctx.errs); + checkClosingDelimiter(ctx.err, ">", ctx.stop); + + if (!(ctx.parent instanceof TypeAliasHeaderContext) && !isStdLibModule) { + throw exceptionBuilder() + .evalError("cannotDeclareTypeParameter") + .withSourceSection(createSourceSection(ctx.ts.get(0))) + .build(); + } + + var size = ctx.ts.size(); + var result = new ArrayList(size); + for (var i = 0; i < size; i++) { + var paramCtx = ctx.ts.get(i); + Variance variance; + if (paramCtx.t == null) { + variance = TypeParameter.Variance.INVARIANT; + } else if (paramCtx.t.getType() == PklLexer.IN) { + variance = TypeParameter.Variance.CONTRAVARIANT; + } else { + assert paramCtx.t.getType() == PklLexer.OUT; + variance = TypeParameter.Variance.COVARIANT; + } + var parameterName = paramCtx.Identifier().getText(); + if (result.stream().anyMatch(it -> it.getName().equals(parameterName))) { + throw exceptionBuilder() + .evalError("duplicateTypeParameter", parameterName) + .withSourceSection(createSourceSection(paramCtx)) + .build(); + } + result.add(new TypeParameter(variance, parameterName, i)); + } + return result; + } + + @Override + public @Nullable UnresolvedTypeNode visitTypeAnnotation(@Nullable TypeAnnotationContext ctx) { + return ctx == null ? null : (UnresolvedTypeNode) ctx.type().accept(this); + } + + @Override + public Object visitNewExpr(NewExprContext ctx) { + var typeCtx = ctx.type(); + return typeCtx != null + ? doVisitNewExprWithExplicitParent(ctx, typeCtx) + : doVisitNewExprWithInferredParent(ctx); + } + + private Object doVisitNewExprWithExplicitParent(NewExprContext ctx, TypeContext typeCtx) { + return doVisitObjectBody( + ctx.objectBody(), + new GetParentForTypeNode( + createSourceSection(ctx), + visitType(typeCtx), + symbolTable.getCurrentScope().getQualifiedName())); + } + + private Object doVisitNewExprWithInferredParent(NewExprContext ctx) { + ExpressionNode inferredParentNode; + + ParserRuleContext child = ctx; + var parent = ctx.getParent(); + var scope = symbolTable.getCurrentScope(); + var levelsUp = 0; + + while (parent instanceof IfExprContext + || parent instanceof TraceExprContext + || parent instanceof LetExprContext && ((LetExprContext) parent).r == child) { + + if (parent instanceof LetExprContext) { + assert scope != null; + scope = scope.getParent(); + levelsUp += 1; + } + child = parent; + parent = parent.getParent(); + } + + assert scope != null; + + if (parent instanceof ClassPropertyContext || parent instanceof ObjectPropertyContext) { + inferredParentNode = + InferParentWithinPropertyNodeGen.create( + createSourceSection(ctx.t), + scope.getName(), + levelsUp == 0 ? new GetOwnerNode() : new GetEnclosingOwnerNode(levelsUp)); + } else if (parent instanceof ObjectElementContext + || parent instanceof ObjectEntryContext && ((ObjectEntryContext) parent).v == child) { + inferredParentNode = + ApplyVmFunction1NodeGen.create( + ReadPropertyNodeGen.create( + createSourceSection(ctx.t), + Identifier.DEFAULT, + levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp)), + new GetMemberKeyNode()); + } else if (parent instanceof ClassMethodContext || parent instanceof ObjectMethodContext) { + var isObjectMethod = + parent instanceof ObjectMethodContext + || parent.getParent() instanceof ModuleContext && moduleInfo.isAmend(); + Identifier scopeName = scope.getName(); + inferredParentNode = + isObjectMethod + ? new InferParentWithinObjectMethodNode( + createSourceSection(ctx.t), + language, + scopeName, + levelsUp == 0 ? new GetOwnerNode() : new GetEnclosingOwnerNode(levelsUp)) + : new InferParentWithinMethodNode( + createSourceSection(ctx.t), + language, + scopeName, + levelsUp == 0 ? new GetOwnerNode() : new GetEnclosingOwnerNode(levelsUp)); + } else if (parent instanceof LetExprContext && ((LetExprContext) parent).l == child) { + // TODO (unclear how to infer type now that let-expression is implemented as lambda + // invocation) + throw exceptionBuilder() + .evalError("cannotInferParent") + .withSourceSection(createSourceSection(ctx.t)) + .build(); + } else { + throw exceptionBuilder() + .evalError("cannotInferParent") + .withSourceSection(createSourceSection(ctx.t)) + .build(); + } + + return doVisitObjectBody(ctx.objectBody(), inferredParentNode); + } + + @Override + public Object visitAmendExpr(AmendExprContext ctx) { + var parentExpr = ctx.expr(); + + if (!(parentExpr instanceof NewExprContext + || parentExpr instanceof AmendExprContext + || parentExpr instanceof ParenthesizedExprContext)) { + throw exceptionBuilder() + .evalError("unexpectedCurlyProbablyAmendsExpression", parentExpr.getText()) + .withSourceSection(createSourceSection(ctx.objectBody().start)) + .build(); + } + + return doVisitObjectBody(ctx.objectBody(), visitExpr(parentExpr)); + } + + @Override + public UnresolvedPropertyNode visitClassProperty(ClassPropertyContext ctx) { + var docComment = createSourceSection(ctx.t); + var annotationNodes = doVisitAnnotations(ctx.annotation()); + var modifierCtxs = ctx.modifier(); + var identifier = ctx.Identifier(); + var typeAnnCtx = ctx.typeAnnotation(); + var sourceSection = createSourceSection(ctx); + var identifierSymbol = identifier.getSymbol(); + var headerSection = + createSourceSection( + !modifierCtxs.isEmpty() ? modifierCtxs.get(0).start : identifierSymbol, + typeAnnCtx != null ? typeAnnCtx.getStop() : identifierSymbol); + + var modifiers = + doVisitModifiers( + ctx.modifier(), VmModifier.VALID_PROPERTY_MODIFIERS, "invalidPropertyModifier"); + + var isLocal = VmModifier.isLocal(modifiers); + var propertyName = Identifier.property(identifier.getText(), isLocal); + + return symbolTable.enterProperty( + propertyName, + getConstLevel(modifiers), + scope -> { + var exprCtx = ctx.expr(); + var objBodyCtx = ctx.objectBody(); + ExpressionNode bodyNode; + + if (exprCtx != null) { // prop = expr + if (VmModifier.isExternal(modifiers)) { + throw exceptionBuilder() + .evalError("externalMemberCannotHaveBody") + .withSourceSection(headerSection) + .build(); + } + if (VmModifier.isAbstract(modifiers)) { + throw exceptionBuilder() + .evalError("abstractMemberCannotHaveBody") + .withSourceSection(headerSection) + .build(); + } + bodyNode = visitExpr(exprCtx); + } else if (objBodyCtx != null && !objBodyCtx.isEmpty()) { // prop { ... } + if (typeAnnCtx != null) { + throw exceptionBuilder() + .evalError("cannotAmendPropertyDefinition") + .withSourceSection(createSourceSection(ctx)) + .build(); + } + bodyNode = + doVisitObjectBody( + objBodyCtx, + new ReadSuperPropertyNode( + unavailableSourceSection(), + scope.getName(), + scope.getConstLevel() == ConstLevel.ALL)); + } else { // no value given + if (isLocal) { + assert typeAnnCtx != null; + throw missingLocalPropertyValue(typeAnnCtx); + } + if (VmModifier.isExternal(modifiers)) { + bodyNode = + externalMemberRegistry.getPropertyBody(scope.getQualifiedName(), headerSection); + if (bodyNode instanceof LanguageAwareNode) { + ((LanguageAwareNode) bodyNode).initLanguage(language); + } + } else if (VmModifier.isAbstract(modifiers)) { + bodyNode = + new CannotInvokeAbstractPropertyNode(headerSection, scope.getQualifiedName()); + } else { + bodyNode = null; // will be given a default by UnresolvedPropertyNode + } + } + + var typeAnnNode = visitTypeAnnotation(typeAnnCtx); + + return new UnresolvedPropertyNode( + language, + sourceSection, + headerSection, + createSourceSection(identifier), + scope.buildFrameDescriptor(), + docComment, + annotationNodes, + modifiers, + scope.getName(), + scope.getQualifiedName(), + typeAnnNode, + bodyNode); + }); + } + + private VmException missingLocalPropertyValue(TypeAnnotationContext typeAnnCtx) { + var stop = typeAnnCtx.stop.getStopIndex(); + return exceptionBuilder() + .evalError("missingLocalPropertyValue") + .withSourceSection(source.createSection(stop + 1, 0)) + .build(); + } + + private ObjectMember doVisitObjectProperty(ObjectPropertyContext ctx) { + return doVisitObjectProperty( + ctx, ctx.modifier(), ctx.Identifier(), ctx.typeAnnotation(), ctx.expr(), ctx.objectBody()); + } + + private ObjectMember doVisitObjectMethod(ObjectMethodContext ctx) { + return doVisitObjectMethod(ctx.methodHeader(), ctx.expr(), false); + } + + private ObjectMember doVisitObjectMethod( + MethodHeaderContext headerCtx, ExprContext exprCtx, boolean isModuleMethod) { + var modifiers = + doVisitModifiers( + headerCtx.modifier(), + VmModifier.VALID_OBJECT_MEMBER_MODIFIERS, + "invalidObjectMemberModifier"); + + if (!VmModifier.isLocal(modifiers)) { + throw exceptionBuilder() + .evalError(isModuleMethod ? "moduleMethodMustBeLocal" : "objectMethodMustBeLocal") + .withSourceSection(createSourceSection(headerCtx)) + .build(); + } + + var methodName = Identifier.method(headerCtx.Identifier().getText(), true); + + var paramListCtx = headerCtx.parameterList(); + var frameDescriptorBuilder = createFrameDescriptorBuilder(paramListCtx); + + return symbolTable.enterMethod( + methodName, + getConstLevel(modifiers), + frameDescriptorBuilder, + List.of(), + scope -> { + if (headerCtx.typeParameterList() != null) { + throw exceptionBuilder() + .evalError("cannotDeclareTypeParameter") + .withSourceSection(createSourceSection(headerCtx.typeParameterList())) + .build(); + } + + var member = + new ObjectMember( + createSourceSection(headerCtx.getParent()), + createSourceSection(headerCtx), + modifiers, + scope.getName(), + scope.getQualifiedName()); + var body = visitExpr(exprCtx); + var node = + new ObjectMethodNode( + language, + scope.buildFrameDescriptor(), + member, + body, + paramListCtx.ts.size(), + doVisitParameterTypes(paramListCtx), + visitTypeAnnotation(headerCtx.typeAnnotation())); + + member.initMemberNode(node); + return member; + }); + } + + private ObjectMember doVisitObjectProperty( + ParserRuleContext ctx, + List modifierCtxs, + TerminalNode propertyName, + @Nullable TypeAnnotationContext typeAnnCtx, + @Nullable ExprContext exprCtx, + @Nullable List bodyCtx) { + + return doVisitObjectProperty( + createSourceSection(ctx), + createSourceSection(propertyName), + doVisitModifiers( + modifierCtxs, VmModifier.VALID_OBJECT_MEMBER_MODIFIERS, "invalidObjectMemberModifier"), + propertyName.getText(), + typeAnnCtx, + exprCtx, + bodyCtx); + } + + private ObjectMember doVisitObjectProperty( + SourceSection sourceSection, + SourceSection headerSection, + int modifiers, + String propertyName, + @Nullable TypeAnnotationContext typeAnnCtx, + @Nullable ExprContext exprCtx, + @Nullable List bodyCtx) { + + var isLocal = VmModifier.isLocal(modifiers); + var identifier = Identifier.property(propertyName, isLocal); + + return symbolTable.enterProperty( + identifier, + getConstLevel(modifiers), + scope -> { + if (isLocal) { + if (exprCtx == null + && typeAnnCtx != null) { // module property that has type annotation but no value + throw missingLocalPropertyValue(typeAnnCtx); + } + } else { + if (typeAnnCtx != null) { + throw exceptionBuilder() + .evalError("nonLocalObjectPropertyCannotHaveTypeAnnotation") + .withSourceSection(createSourceSection(typeAnnCtx.type())) + .build(); + } + } + + ExpressionNode bodyNode; + if (bodyCtx != null && !bodyCtx.isEmpty()) { // foo { ... } + if (isLocal) { + throw exceptionBuilder() + .evalError("cannotAmendLocalPropertyDefinition") + .withSourceSection(createSourceSection(bodyCtx.get(0).start)) + .build(); + } + bodyNode = + doVisitObjectBody( + bodyCtx, + new ReadSuperPropertyNode( + unavailableSourceSection(), + scope.getName(), + // Never need a const check for amends declarations. In `foo { ... }`: + // 1. if `foo` is const (i.e. `const foo { ... }`, `super.foo` is required + // to be const (the const-ness of a property cannot be changed) + // 2. if in a const scope (i.e. `const bar = new { foo { ... } }`), + // `super.foo` does not reference something outside the scope. + false)); + } else { // foo = ... + assert exprCtx != null; + bodyNode = visitExpr(exprCtx); + } + + return isLocal + ? VmUtils.createLocalObjectProperty( + language, + sourceSection, + headerSection, + scope.getName(), + scope.getQualifiedName(), + scope.buildFrameDescriptor(), + modifiers, + bodyNode, + visitTypeAnnotation(typeAnnCtx)) + : VmUtils.createObjectProperty( + language, + sourceSection, + headerSection, + scope.getName(), + scope.getQualifiedName(), + scope.buildFrameDescriptor(), + modifiers, + bodyNode, + null); + }); + } + + private GeneratorMemberNode[] doVisitGeneratorMemberNodes( + List memberCtxs) { + var result = new GeneratorMemberNode[memberCtxs.size()]; + for (var i = 0; i < result.length; i++) { + result[i] = (GeneratorMemberNode) memberCtxs.get(i).accept(this); + } + return result; + } + + private GeneratorObjectLiteralNode doVisitGeneratorObjectBody( + ObjectBodyContext ctx, ExpressionNode parentNode) { + var parametersDescriptor = createFrameDescriptorBuilder(ctx); + var parameterTypes = doVisitParameterTypes(ctx); + var memberNodes = doVisitGeneratorMemberNodes(ctx.objectMember()); + var currentScope = symbolTable.getCurrentScope(); + //noinspection ConstantConditions + return GeneratorObjectLiteralNodeGen.create( + createSourceSection(ctx.getParent()), + language, + currentScope.getQualifiedName(), + currentScope.isCustomThisScope(), + parametersDescriptor == null ? null : parametersDescriptor.build(), + parameterTypes, + memberNodes, + parentNode); + } + + @Override + public GeneratorPropertyNode visitObjectProperty(ObjectPropertyContext ctx) { + checkHasNoForGenerator(ctx, "forGeneratorCannotGenerateProperties"); + var member = doVisitObjectProperty(ctx); + return GeneratorPropertyNodeGen.create(member); + } + + @Override + public GeneratorMemberNode visitObjectMethod(ObjectMethodContext ctx) { + checkHasNoForGenerator(ctx, "forGeneratorCannotGenerateMethods"); + var member = doVisitObjectMethod(ctx); + return GeneratorPropertyNodeGen.create(member); + } + + private void checkHasNoForGenerator(ParserRuleContext ctx, String errorMessageKey) { + if (symbolTable.getCurrentScope().getForGeneratorVariables().isEmpty()) { + return; + } + var forExprCtx = ctx.getParent(); + while (forExprCtx.getClass() != ForGeneratorContext.class) { + forExprCtx = forExprCtx.getParent(); + } + throw exceptionBuilder() + .evalError(errorMessageKey) + .withSourceSection(createSourceSection(((ForGeneratorContext) forExprCtx).FOR())) + .build(); + } + + @Override + public GeneratorMemberNode visitMemberPredicate(MemberPredicateContext ctx) { + var keyNodeAndMember = doVisitMemberPredicate(ctx); + var keyNode = keyNodeAndMember.first; + var member = keyNodeAndMember.second; + insertWriteForGeneratorVarsToFrameSlotsNode(member.getMemberNode()); + + return GeneratorPredicateMemberNodeGen.create(keyNode, member); + } + + @Override + public GeneratorMemberNode visitObjectEntry(ObjectEntryContext ctx) { + var keyNodeAndMember = doVisitObjectEntry(ctx); + var keyNode = keyNodeAndMember.first; + var member = keyNodeAndMember.second; + insertWriteForGeneratorVarsToFrameSlotsNode(member.getMemberNode()); + + return GeneratorEntryNodeGen.create(keyNode, member); + } + + @Override + public GeneratorMemberNode visitObjectSpread(ObjectSpreadContext ctx) { + return GeneratorSpreadNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.expr()), ctx.QSPREAD() != null); + } + + private void insertWriteForGeneratorVarsToFrameSlotsNode(@Nullable MemberNode memberNode) { + if (memberNode == null) return; // member has constant value + + var descriptor = memberNode.getFrameDescriptor(); + var forGeneratorVars = symbolTable.getCurrentScope().getForGeneratorVariables(); + if (forGeneratorVars.isEmpty()) { + return; // node is not within a for generator + } + var slots = new int[forGeneratorVars.size()]; + var i = 0; + for (var variable : forGeneratorVars) { + slots[i] = descriptor.findOrAddAuxiliarySlot(variable); + i++; + } + memberNode.replaceBody((bodyNode) -> new WriteForVariablesNode(slots, bodyNode)); + } + + @Override + public GeneratorElementNode visitObjectElement(ObjectElementContext ctx) { + var member = doVisitObjectElement(ctx); + insertWriteForGeneratorVarsToFrameSlotsNode(member.getMemberNode()); + return GeneratorElementNodeGen.create(member); + } + + private GeneratorMemberNode[] doVisitForWhenBody(ObjectBodyContext ctx) { + if (!ctx.ps.isEmpty()) { + throw exceptionBuilder() + .evalError("forWhenBodyCannotHaveParameters") + .withSourceSection(createSourceSection(ctx.ps.get(0))) + .build(); + } + return doVisitGeneratorMemberNodes(ctx.objectMember()); + } + + @Override + public GeneratorWhenNode visitWhenGenerator(WhenGeneratorContext ctx) { + checkClosingDelimiter(ctx.err, ")", ctx.e.stop); + + var sourceSection = createSourceSection(ctx); + var thenNodes = doVisitForWhenBody(ctx.b1); + var elseNodes = ctx.b2 == null ? new GeneratorMemberNode[0] : doVisitForWhenBody(ctx.b2); + + return new GeneratorWhenNode(sourceSection, visitExpr(ctx.e), thenNodes, elseNodes); + } + + private int pushForGeneratorVariableContext(ParameterContext ctx) { + var currentScope = symbolTable.getCurrentScope(); + var slot = currentScope.pushForGeneratorVariableContext(ctx); + if (slot == -1) { + throw exceptionBuilder() + .evalError("duplicateDefinition", ctx.typedIdentifier().Identifier().getText()) + .withSourceSection(createSourceSection(ctx)) + .build(); + } + return slot; + } + + private static boolean isIgnored(@Nullable ParameterContext param) { + return param != null && param.UNDERSCORE() != null; + } + + @Override + public GeneratorForNode visitForGenerator(ForGeneratorContext ctx) { + checkClosingDelimiter(ctx.err, ")", ctx.e.stop); + var sourceSection = createSourceSection(ctx); + int keyVariableSlot; + int valueVariableSlot; + UnresolvedTypeNode unresolvedKeyTypeNode; + UnresolvedTypeNode unresolvedValueTypeNode; + var currentScope = symbolTable.getCurrentScope(); + var ignoreT1 = isIgnored(ctx.t1); + var ignoreT2 = ctx.t2 == null ? ignoreT1 : isIgnored(ctx.t2); + + if (ctx.t2 != null) { + keyVariableSlot = ignoreT1 ? -1 : pushForGeneratorVariableContext(ctx.t1); + valueVariableSlot = ignoreT2 ? -1 : pushForGeneratorVariableContext(ctx.t2); + unresolvedKeyTypeNode = + ignoreT1 ? null : visitTypeAnnotation(ctx.t1.typedIdentifier().typeAnnotation()); + unresolvedValueTypeNode = + ignoreT2 ? null : visitTypeAnnotation(ctx.t2.typedIdentifier().typeAnnotation()); + } else { + keyVariableSlot = -1; + valueVariableSlot = ignoreT1 ? -1 : pushForGeneratorVariableContext(ctx.t1); + unresolvedKeyTypeNode = null; + unresolvedValueTypeNode = + ignoreT1 ? null : visitTypeAnnotation(ctx.t1.typedIdentifier().typeAnnotation()); + } + + var iterableNode = visitExpr(ctx.e); + var memberNodes = doVisitForWhenBody(ctx.objectBody()); + if (keyVariableSlot != -1) { + currentScope.popForGeneratorVariable(); + } + if (valueVariableSlot != -1) { + currentScope.popForGeneratorVariable(); + } + //noinspection ConstantConditions + return GeneratorForNodeGen.create( + sourceSection, + keyVariableSlot, + valueVariableSlot, + iterableNode, + unresolvedKeyTypeNode, + unresolvedValueTypeNode, + memberNodes, + ctx.t2 != null && !ignoreT1, + !ignoreT2); + } + + private void checkSpaceSeparatedObjectMembers(ObjectBodyContext objectBodyContext) { + assert objectBodyContext.objectMember() != null; + if (objectBodyContext.objectMember().size() < 2) { + return; + } + ObjectMemberContext prevMember = null; + for (var member : objectBodyContext.objectMember()) { + if (prevMember == null) { + prevMember = member; + continue; + } + var startIndex = member.getStart().getStartIndex(); + var prevStopIndex = prevMember.getStop().getStopIndex(); + if (startIndex - prevStopIndex == 1) { + throw exceptionBuilder() + .evalError("unseparatedObjectMembers") + .withSourceSection(createSourceSection(member)) + .build(); + } + } + } + + private ExpressionNode doVisitObjectBody( + List ctxs, ExpressionNode parentNode) { + for (var ctx : ctxs) { + parentNode = doVisitObjectBody(ctx, parentNode); + } + return parentNode; + } + + private ExpressionNode doVisitObjectBody(ObjectBodyContext ctx, ExpressionNode parentNode) { + checkClosingDelimiter(ctx.err, "}", ctx.stop); + return symbolTable.enterObjectScope( + (scope) -> { + var objectMemberCtx = ctx.objectMember(); + if (objectMemberCtx.isEmpty()) { + return EmptyObjectLiteralNodeGen.create( + createSourceSection(ctx.getParent()), parentNode); + } + var sourceSection = createSourceSection(ctx.getParent()); + + var parametersDescriptorBuilder = createFrameDescriptorBuilder(ctx); + var parameterTypes = doVisitParameterTypes(ctx); + + var members = EconomicMaps.create(); + var elements = new ArrayList(); + var keyNodes = new ArrayList(); + var values = new ArrayList(); + var isConstantKeyNodes = true; + + checkSpaceSeparatedObjectMembers(ctx); + for (var memberCtx : objectMemberCtx) { + if (memberCtx instanceof ObjectPropertyContext) { + var propertyCtx = (ObjectPropertyContext) memberCtx; + addProperty(members, doVisitObjectProperty(propertyCtx)); + continue; + } + + if (memberCtx instanceof ObjectEntryContext) { + var entryCtx = (ObjectEntryContext) memberCtx; + + var keyAndValue = doVisitObjectEntry(entryCtx); + var key = keyAndValue.first; + keyNodes.add(key); + isConstantKeyNodes = isConstantKeyNodes && key instanceof ConstantNode; + values.add(keyAndValue.second); + continue; + } + + if (memberCtx instanceof ObjectElementContext) { + var elementCtx = (ObjectElementContext) memberCtx; + var element = doVisitObjectElement(elementCtx); + elements.add(element); + continue; + } + + if (memberCtx instanceof ObjectMethodContext) { + var methodCtx = (ObjectMethodContext) memberCtx; + addProperty(members, doVisitObjectMethod(methodCtx)); + continue; + } + + assert memberCtx instanceof ForGeneratorContext + || memberCtx instanceof WhenGeneratorContext + || memberCtx instanceof MemberPredicateContext + || memberCtx instanceof ObjectSpreadContext; + // bail out and create GeneratorObjectLiteralNode instead + // (but can't we easily reuse members/elements/keyNodes/values?) + return doVisitGeneratorObjectBody(ctx, parentNode); + } + + var currentScope = symbolTable.getCurrentScope(); + var parametersDescriptor = + parametersDescriptorBuilder == null ? null : parametersDescriptorBuilder.build(); + if (!elements.isEmpty()) { + if (isConstantKeyNodes) { // true if zero key nodes + addConstantEntries(members, keyNodes, values); + //noinspection ConstantConditions + return ElementsLiteralNodeGen.create( + sourceSection, + language, + currentScope.getQualifiedName(), + currentScope.isCustomThisScope(), + parametersDescriptor, + parameterTypes, + members, + elements.toArray(new ObjectMember[0]), + parentNode); + } + //noinspection ConstantConditions + return ElementsEntriesLiteralNodeGen.create( + sourceSection, + language, + currentScope.getQualifiedName(), + currentScope.isCustomThisScope(), + parametersDescriptor, + parameterTypes, + members, + elements.toArray(new ObjectMember[0]), + keyNodes.toArray(new ExpressionNode[0]), + values.toArray(new ObjectMember[0]), + parentNode); + } + + if (!keyNodes.isEmpty()) { + if (isConstantKeyNodes) { + addConstantEntries(members, keyNodes, values); + //noinspection ConstantConditions + return ConstantEntriesLiteralNodeGen.create( + sourceSection, + language, + currentScope.getQualifiedName(), + currentScope.isCustomThisScope(), + parametersDescriptor, + parameterTypes, + members, + parentNode); + } + //noinspection ConstantConditions + return EntriesLiteralNodeGen.create( + sourceSection, + language, + currentScope.getQualifiedName(), + currentScope.isCustomThisScope(), + parametersDescriptor, + parameterTypes, + members, + keyNodes.toArray(new ExpressionNode[0]), + values.toArray(new ObjectMember[0]), + parentNode); + } + //noinspection ConstantConditions + return PropertiesLiteralNodeGen.create( + sourceSection, + language, + currentScope.getQualifiedName(), + currentScope.isCustomThisScope(), + parametersDescriptor, + parameterTypes, + members, + parentNode); + }); + } + + private void addConstantEntries( + EconomicMap members, + List keyNodes, + List values) { + + for (var i = 0; i < keyNodes.size(); i++) { + var key = ((ConstantNode) keyNodes.get(i)).getValue(); + var value = values.get(i); + var previousValue = EconomicMaps.put(members, key, value); + if (previousValue != null) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("duplicateDefinition", new ProgramValue("", key)) + .withSourceSection(value.getHeaderSection()) + .build(); + } + } + } + + private ObjectMember doVisitObjectElement(ObjectElementContext ctx) { + return symbolTable.enterEntry( + null, + scope -> { + var elementNode = visitExpr(ctx.expr()); + + var member = + new ObjectMember( + createSourceSection(ctx), + elementNode.getSourceSection(), + VmModifier.ELEMENT, + null, + scope.getQualifiedName()); + + if (elementNode instanceof ConstantNode) { + member.initConstantValue((ConstantNode) elementNode); + } else { + member.initMemberNode( + new UntypedObjectMemberNode( + language, scope.buildFrameDescriptor(), member, elementNode)); + } + + return member; + }); + } + + private Pair doVisitMemberPredicate(MemberPredicateContext ctx) { + if (ctx.err1 == null && ctx.err2 == null) { + throw missingDelimiter("]]", ctx.k.stop.getStopIndex() + 1); + } else if (ctx.err1 != null + && (ctx.err2 == null || ctx.err1.getStartIndex() != ctx.err2.getStartIndex() - 1)) { + // There shouldn't be any whitespace between the first and second ']'. + throw wrongDelimiter("]]", "]", ctx.err1.getStartIndex()); + } + + var keyNode = symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.k)); + + return symbolTable.enterEntry( + keyNode, objectMemberInserter(createSourceSection(ctx), keyNode, ctx.v, ctx.objectBody())); + } + + private Pair doVisitObjectEntry(ObjectEntryContext ctx) { + checkClosingDelimiter(ctx.err1, "]", ctx.k.stop); + if (ctx.err2 != null) { + throw ctx.err1.getStartIndex() == ctx.err2.getStartIndex() - 1 + ? wrongDelimiter("]", "]]", ctx.err1.getStartIndex()) + : danglingDelimiter("]", ctx.err2.getStartIndex()); + } + + var keyNode = visitExpr(ctx.k); + + return symbolTable.enterEntry( + keyNode, objectMemberInserter(createSourceSection(ctx), keyNode, ctx.v, ctx.objectBody())); + } + + private Function> objectMemberInserter( + SourceSection sourceSection, + ExpressionNode keyNode, + @Nullable ExprContext valueCtx, + List objectBodyCtxs) { + return scope -> { + var member = + new ObjectMember( + sourceSection, + keyNode.getSourceSection(), + VmModifier.ENTRY, + null, + scope.getQualifiedName()); + + if (valueCtx != null) { // ["key"] = value + var valueNode = visitExpr(valueCtx); + if (valueNode instanceof ConstantNode) { + member.initConstantValue((ConstantNode) valueNode); + } else { + member.initMemberNode( + new UntypedObjectMemberNode( + language, scope.buildFrameDescriptor(), member, valueNode)); + } + } else { // ["key"] { ... } + var objectBody = + doVisitObjectBody( + objectBodyCtxs, + new ReadSuperEntryNode(unavailableSourceSection(), new GetMemberKeyNode())); + member.initMemberNode( + new UntypedObjectMemberNode( + language, scope.buildFrameDescriptor(), member, objectBody)); + } + + return Pair.of(keyNode, member); + }; + } + + @Override + public ExpressionNode visitAnnotation(AnnotationContext ctx) { + var verifyNode = new CheckIsAnnotationClassNode(visitType(ctx.type())); + + var bodyCtx = ctx.objectBody(); + if (bodyCtx == null) { + var currentScope = symbolTable.getCurrentScope(); + //noinspection ConstantConditions + return PropertiesLiteralNodeGen.create( + createSourceSection(ctx), + language, + currentScope.getQualifiedName(), + currentScope.isCustomThisScope(), + null, + new UnresolvedTypeNode[0], + EconomicMaps.create(), + verifyNode); + } + + return symbolTable.enterAnnotationScope((scope) -> doVisitObjectBody(bodyCtx, verifyNode)); + } + + private ExpressionNode[] doVisitAnnotations(List ctxs) { + return ctxs.stream().map(this::visitAnnotation).toArray(ExpressionNode[]::new); + } + + @Override + public Integer visitModifier(ModifierContext ctx) { + switch (ctx.t.getType()) { + case PklLexer.EXTERNAL: + return VmModifier.EXTERNAL; + case PklLexer.ABSTRACT: + return VmModifier.ABSTRACT; + case PklLexer.OPEN: + return VmModifier.OPEN; + case PklLexer.LOCAL: + return VmModifier.LOCAL; + case PklLexer.HIDDEN_: + return VmModifier.HIDDEN; + case PklLexer.FIXED: + return VmModifier.FIXED; + case PklLexer.CONST: + return VmModifier.CONST; + default: + throw createUnexpectedTokenError(ctx.t); + } + } + + private int doVisitModifiers( + List contexts, int validModifiers, String errorMessage) { + + var result = VmModifier.NONE; + for (var ctx : contexts) { + int modifier = visitModifier(ctx); + if ((modifier & validModifiers) == 0) { + throw exceptionBuilder() + .evalError(errorMessage, ctx.t.getText()) + .withSourceSection(createSourceSection(ctx)) + .build(); + } + result += modifier; + } + + // flag modifier combinations that are never valid right away + + if (VmModifier.isExternal(result) && !ModuleKeys.isStdLibModule(moduleKey)) { + throw exceptionBuilder() + .evalError("cannotDefineExternalMember") + .withSourceSection(createSourceSection(contexts, PklLexer.EXTERNAL)) + .build(); + } + + if (VmModifier.isLocal(result) && VmModifier.isHidden(result)) { + throw exceptionBuilder() + .evalError("redundantHiddenModifier") + .withSourceSection(createSourceSection(contexts, PklLexer.HIDDEN_)) + .build(); + } + + if (VmModifier.isLocal(result) && VmModifier.isFixed(result)) { + throw exceptionBuilder() + .evalError("redundantFixedModifier") + .withSourceSection(createSourceSection(contexts, PklLexer.FIXED)) + .build(); + } + + if (VmModifier.isAbstract(result) && VmModifier.isOpen(result)) { + throw exceptionBuilder() + .evalError("redundantOpenModifier") + .withSourceSection(createSourceSection(contexts, PklLexer.OPEN)) + .build(); + } + + return result; + } + + @Override + public UnresolvedMethodNode visitClassMethod(ClassMethodContext ctx) { + var headerCtx = ctx.methodHeader(); + var headerSection = createSourceSection(headerCtx); + + var typeParameters = visitTypeParameterList(headerCtx.typeParameterList()); + + var modifiers = + doVisitModifiers( + headerCtx.modifier(), VmModifier.VALID_METHOD_MODIFIERS, "invalidMethodModifier"); + + var isLocal = VmModifier.isLocal(modifiers); + var methodName = Identifier.method(headerCtx.Identifier().getText(), isLocal); + + var bodyContext = ctx.expr(); + var paramListCtx = headerCtx.parameterList(); + var descriptorBuilder = createFrameDescriptorBuilder(paramListCtx); + var paramCount = paramListCtx.ts.size(); + + return symbolTable.enterMethod( + methodName, + getConstLevel(modifiers), + descriptorBuilder, + typeParameters, + scope -> { + ExpressionNode bodyNode; + if (bodyContext != null) { + if (VmModifier.isExternal(modifiers)) { + throw exceptionBuilder() + .evalError("externalMemberCannotHaveBody") + .withSourceSection(headerSection) + .build(); + } + if (VmModifier.isAbstract(modifiers)) { + throw exceptionBuilder() + .evalError("abstractMemberCannotHaveBody") + .withSourceSection(headerSection) + .build(); + } + bodyNode = visitExpr(bodyContext); + } else { + if (VmModifier.isExternal(modifiers)) { + bodyNode = + externalMemberRegistry.getFunctionBody( + scope.getQualifiedName(), headerSection, paramCount); + if (bodyNode instanceof LanguageAwareNode) { + ((LanguageAwareNode) bodyNode).initLanguage(language); + } + } else if (VmModifier.isAbstract(modifiers)) { + bodyNode = + new CannotInvokeAbstractFunctionNode(headerSection, scope.getQualifiedName()); + } else { + throw exceptionBuilder() + .evalError("missingMethodBody", methodName) + .withSourceSection(headerSection) + .build(); + } + } + + return new UnresolvedMethodNode( + language, + createSourceSection(ctx), + headerSection, + scope.buildFrameDescriptor(), + createSourceSection(ctx.t), + doVisitAnnotations(ctx.annotation()), + modifiers, + methodName, + scope.getQualifiedName(), + paramCount, + typeParameters, + doVisitParameterTypes(paramListCtx), + visitTypeAnnotation(headerCtx.typeAnnotation()), + isMethodReturnTypeChecked, + bodyNode); + }); + } + + @Override + public ExpressionNode visitFunctionLiteral(FunctionLiteralContext ctx) { + var sourceSection = createSourceSection(ctx); + var paramCtx = ctx.parameterList(); + var descriptorBuilder = createFrameDescriptorBuilder(paramCtx); + var paramCount = paramCtx.ts.size(); + + if (paramCount > 5) { + throw exceptionBuilder() + .evalError("tooManyFunctionParameters") + .withSourceSection(sourceSection) + .build(); + } + + var isCustomThisScope = symbolTable.getCurrentScope().isCustomThisScope(); + + return symbolTable.enterLambda( + descriptorBuilder, + scope -> { + var expr = visitExpr(ctx.expr()); + var functionNode = + new UnresolvedFunctionNode( + language, + scope.buildFrameDescriptor(), + new Lambda(sourceSection, scope.getQualifiedName()), + paramCount, + doVisitParameterTypes(paramCtx), + null, + expr); + + return new FunctionLiteralNode(sourceSection, functionNode, isCustomThisScope); + }); + } + + @Override + public ConstantValueNode visitNullLiteral(NullLiteralContext ctx) { + return new ConstantValueNode(createSourceSection(ctx), VmNull.withoutDefault()); + } + + @Override + public ExpressionNode visitTrueLiteral(TrueLiteralContext ctx) { + return new TrueLiteralNode(createSourceSection(ctx)); + } + + @Override + public Object visitFalseLiteral(FalseLiteralContext ctx) { + return new FalseLiteralNode(createSourceSection(ctx)); + } + + @Override + public IntLiteralNode visitIntLiteral(IntLiteralContext ctx) { + var section = createSourceSection(ctx); + var text = ctx.IntLiteral().getText(); + + var radix = 10; + if (text.startsWith("0x") || text.startsWith("0b") || text.startsWith("0o")) { + var type = text.charAt(1); + if (type == 'x') { + radix = 16; + } else if (type == 'b') { + radix = 2; + } else { + radix = 8; + } + + text = text.substring(2); + if (text.startsWith("_")) { + invalidSeparatorPosition(source.createSection(ctx.getStart().getStartIndex() + 2, 1)); + } + } + + // relies on grammar rule nesting depth, but a breakage won't go unnoticed by tests + if (ctx.getParent() instanceof UnaryMinusExprContext) { + // handle negation here to make parsing of base.MinInt work + // also moves negation from runtime to parse time + text = "-" + text; + } + + text = text.replaceAll("_", ""); + try { + var num = Long.parseLong(text, radix); + return new IntLiteralNode(section, num); + } catch (NumberFormatException e) { + throw exceptionBuilder().evalError("intTooLarge", text).withSourceSection(section).build(); + } + } + + @Override + public FloatLiteralNode visitFloatLiteral(FloatLiteralContext ctx) { + var section = createSourceSection(ctx); + var text = ctx.FloatLiteral().getText(); + // relies on grammar rule nesting depth, but a breakage won't go unnoticed by tests + if (ctx.getParent() instanceof UnaryMinusExprContext) { + // handle negation here for consistency with visitIntegerLiteral + // also moves negation from runtime to parse time + text = "-" + text; + } + + var dotIdx = text.indexOf('.'); + if (dotIdx != -1 && text.charAt(dotIdx + 1) == '_') { + invalidSeparatorPosition( + source.createSection(ctx.getStart().getStartIndex() + dotIdx + 1, 1)); + } + var exponentIdx = text.indexOf('e'); + if (exponentIdx == -1) { + exponentIdx = text.indexOf('E'); + } + if (exponentIdx != -1 && text.charAt(exponentIdx + 1) == '_') { + invalidSeparatorPosition( + source.createSection(ctx.getStart().getStartIndex() + exponentIdx + 1, 1)); + } + + text = text.replaceAll("_", ""); + try { + var num = Double.parseDouble(text); + return new FloatLiteralNode(section, num); + } catch (NumberFormatException e) { + throw exceptionBuilder().evalError("floatTooLarge", text).withSourceSection(section).build(); + } + } + + @Override + public Object visitSingleLineStringLiteral(SingleLineStringLiteralContext ctx) { + checkSingleLineStringDelimiters(ctx.t, ctx.t2); + + var singleParts = ctx.singleLineStringPart(); + if (singleParts.isEmpty()) { + return new ConstantValueNode(createSourceSection(ctx), ""); + } + + if (singleParts.size() == 1) { + var ts = singleParts.get(0).ts; + if (!ts.isEmpty()) { + return new ConstantValueNode( + createSourceSection(ctx), doVisitSingleLineConstantStringPart(ts)); + } + } + + return new InterpolatedStringLiteralNode( + createSourceSection(ctx), + singleParts.stream().map(this::visitSingleLineStringPart).toArray(ExpressionNode[]::new)); + } + + @Override + public Object visitMultiLineStringLiteral(MultiLineStringLiteralContext ctx) { + var multiPart = ctx.multiLineStringPart(); + + if (multiPart.isEmpty()) { + throw exceptionBuilder() + .evalError("stringContentMustBeginOnNewLine") + .withSourceSection(createSourceSection(ctx.t2)) + .build(); + } + + var firstPart = multiPart.get(0); + if (firstPart.e != null || firstPart.ts.get(0).getType() != PklLexer.MLNewline) { + throw exceptionBuilder() + .evalError("stringContentMustBeginOnNewLine") + .withSourceSection( + firstPart.e != null + ? startOf(firstPart.MLInterpolation()) + : startOf(firstPart.ts.get(0))) + .build(); + } + + var lastPart = multiPart.get(multiPart.size() - 1); + var commonIndent = getCommonIndent(lastPart, ctx.t2); + + if (multiPart.size() == 1) { + return new ConstantValueNode( + createSourceSection(ctx), + doVisitMultiLineConstantStringPart(firstPart.ts, commonIndent, true, true)); + } + + final var multiPartExprs = new ExpressionNode[multiPart.size()]; + var lastIndex = multiPart.size() - 1; + + for (var i = 0; i <= lastIndex; i++) { + multiPartExprs[i] = + doVisitMultiLineStringPart(multiPart.get(i), commonIndent, i == 0, i == lastIndex); + } + + return new InterpolatedStringLiteralNode(createSourceSection(ctx), multiPartExprs); + } + + @Override + public String visitStringConstant(StringConstantContext ctx) { + checkSingleLineStringDelimiters(ctx.t, ctx.t2); + return doVisitSingleLineConstantStringPart(ctx.ts); + } + + @Override + public ExpressionNode visitSingleLineStringPart(SingleLineStringPartContext ctx) { + if (ctx.e != null) { + return ToStringNodeGen.create(createSourceSection(ctx), visitExpr(ctx.e)); + } + + return new ConstantValueNode( + createSourceSection(ctx), doVisitSingleLineConstantStringPart(ctx.ts)); + } + + @Override + public ExpressionNode visitMultiLineStringPart(MultiLineStringPartContext ctx) { + throw exceptionBuilder().unreachableCode().build(); + } + + private ExpressionNode createResolveVariableNode(SourceSection section, Identifier propertyName) { + var scope = symbolTable.getCurrentScope(); + return new ResolveVariableNode( + section, + propertyName, + isBaseModule, + scope.isCustomThisScope(), + scope.getConstLevel(), + scope.getConstDepth()); + } + + private ExpressionNode doVisitListLiteral(ExprContext ctx, ArgumentListContext argListCtx) { + var elementNodes = createCollectionArgumentNodes(argListCtx); + + if (elementNodes.first.length == 0) { + return new ConstantValueNode(VmList.EMPTY); + } + + return elementNodes.second + ? new ConstantValueNode( + createSourceSection(ctx), VmList.createFromConstantNodes(elementNodes.first)) + : new ListLiteralNode(createSourceSection(ctx), elementNodes.first); + } + + private ExpressionNode doVisitSetLiteral(ExprContext ctx, ArgumentListContext argListCtx) { + var elementNodes = createCollectionArgumentNodes(argListCtx); + + if (elementNodes.first.length == 0) { + return new ConstantValueNode(VmSet.EMPTY); + } + + return elementNodes.second + ? new ConstantValueNode( + createSourceSection(ctx), VmSet.createFromConstantNodes(elementNodes.first)) + : new SetLiteralNode(createSourceSection(ctx), elementNodes.first); + } + + private ExpressionNode doVisitMapLiteral(ExprContext ctx, ArgumentListContext argListCtx) { + var keyAndValueNodes = createCollectionArgumentNodes(argListCtx); + + if (keyAndValueNodes.first.length == 0) { + return new ConstantValueNode(VmMap.EMPTY); + } + + if (keyAndValueNodes.first.length % 2 != 0) { + throw exceptionBuilder() + .evalError("missingMapValue") + .withSourceSection(createSourceSection(ctx.stop)) + .build(); + } + + return keyAndValueNodes.second + ? new ConstantValueNode( + createSourceSection(ctx), VmMap.createFromConstantNodes(keyAndValueNodes.first)) + : new MapLiteralNode(createSourceSection(ctx), keyAndValueNodes.first); + } + + private Pair createCollectionArgumentNodes(ArgumentListContext ctx) { + checkCommaSeparatedElements(ctx, ctx.es, ctx.errs); + checkClosingDelimiter(ctx.err, ")", ctx.stop); + + var exprCtxs = ctx.expr(); + var elementNodes = new ExpressionNode[exprCtxs.size()]; + var isConstantNodes = true; + + for (var i = 0; i < elementNodes.length; i++) { + var exprNode = visitExpr(exprCtxs.get(i)); + elementNodes[i] = exprNode; + isConstantNodes = isConstantNodes && exprNode instanceof ConstantNode; + } + + return Pair.of(elementNodes, isConstantNodes); + } + + @Override + public ExpressionNode visitExpr(ExprContext ctx) { + return (ExpressionNode) ctx.accept(this); + } + + @Override + public Object visitComparisonExpr(ComparisonExprContext ctx) { + switch (ctx.t.getType()) { + case PklLexer.LT: + return LessThanNodeGen.create(createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.GT: + return GreaterThanNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.LTE: + return LessThanOrEqualNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.GTE: + return GreaterThanOrEqualNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + default: + throw createUnexpectedTokenError(ctx.t); + } + } + + @Override + public Object visitEqualityExpr(EqualityExprContext ctx) { + switch (ctx.t.getType()) { + case PklLexer.EQUAL: + return EqualNodeGen.create(createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.NOT_EQUAL: + return NotEqualNodeGen.create(createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + default: + throw createUnexpectedTokenError(ctx.t); + } + } + + @Override + public ObjectMember visitImportClause(ImportClauseContext ctx) { + var importNode = doVisitImport(ctx.t.getType(), ctx, ctx.stringConstant()); + var moduleKey = moduleResolver.resolve(importNode.getImportUri()); + var importName = + Identifier.property( + ctx.Identifier() != null + ? ctx.Identifier().getText() + : IoUtils.inferModuleName(moduleKey), + true); + + return symbolTable.enterProperty( + importName, + ConstLevel.NONE, + scope -> { + var modifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST; + if (ctx.IMPORT_GLOB() != null) { + modifiers = modifiers | VmModifier.GLOB; + } + var result = + new ObjectMember( + importNode.getSourceSection(), + importNode.getSourceSection(), + modifiers, + scope.getName(), + scope.getQualifiedName()); + + result.initMemberNode( + new UntypedObjectMemberNode( + language, scope.buildFrameDescriptor(), result, importNode)); + + return result; + }); + } + + private URI resolveImport(String importUri, StringConstantContext importUriCtx) { + URI parsedUri; + try { + parsedUri = IoUtils.toUri(importUri); + } catch (URISyntaxException e) { + throw exceptionBuilder() + .evalError("invalidModuleUri", importUri) + .withHint(e.getReason()) + .withSourceSection(createSourceSection(importUriCtx)) + .build(); + } + URI resolvedUri; + var context = VmContext.get(null); + try { + resolvedUri = IoUtils.resolve(context.getSecurityManager(), moduleKey, parsedUri); + } catch (FileNotFoundException e) { + throw exceptionBuilder() + .evalError("cannotFindModule", importUri) + .withSourceSection(createSourceSection(importUriCtx)) + .build(); + } catch (URISyntaxException e) { + throw exceptionBuilder() + .evalError("invalidModuleUri", importUri) + .withHint(e.getReason()) + .withSourceSection(createSourceSection(importUriCtx)) + .build(); + } catch (IOException e) { + throw exceptionBuilder() + .evalError("ioErrorLoadingModule", importUri) + .withCause(e) + .withSourceSection(createSourceSection(importUriCtx)) + .build(); + } catch (SecurityManagerException | PackageLoadError e) { + throw exceptionBuilder() + .withSourceSection(createSourceSection(importUriCtx)) + .withCause(e) + .build(); + } catch (VmException e) { + throw exceptionBuilder() + .evalError(e.getMessage(), e.getMessageArguments()) + .withSourceSection(createSourceSection(importUriCtx)) + .build(); + } + + if (!resolvedUri.isAbsolute()) { + throw exceptionBuilder() + .evalError("cannotHaveRelativeImport", moduleKey.getUri()) + .withSourceSection(createSourceSection(importUriCtx)) + .build(); + } + return resolvedUri; + } + + @Override + public ExpressionNode visitQualifiedIdentifier(QualifiedIdentifierContext ctx) { + var firstToken = ctx.ts.get(0); + var result = + createResolveVariableNode(createSourceSection(firstToken), toIdentifier(firstToken)); + + for (var i = 1; i < ctx.ts.size(); i++) { + var token = ctx.ts.get(i); + result = ReadPropertyNodeGen.create(createSourceSection(token), toIdentifier(token), result); + } + + return result; + } + + @Override + public Object visitNonNullExpr(NonNullExprContext ctx) { + return new NonNullNode(createSourceSection(ctx), visitExpr(ctx.expr())); + } + + @Override + public ExpressionNode visitUnaryMinusExpr(UnaryMinusExprContext ctx) { + var childExpr = visitExpr(ctx.expr()); + if (childExpr instanceof IntLiteralNode || childExpr instanceof FloatLiteralNode) { + // negation already handled in child expr (see corresponding code) + return childExpr; + } + + return UnaryMinusNodeGen.create(createSourceSection(ctx), childExpr); + } + + @Override + public ExpressionNode visitAdditiveExpr(AdditiveExprContext ctx) { + switch (ctx.t.getType()) { + case PklLexer.PLUS: + return AdditionNodeGen.create(createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.MINUS: + return SubtractionNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + default: + throw createUnexpectedTokenError(ctx.t); + } + } + + @Override + public ExpressionNode visitMultiplicativeExpr(MultiplicativeExprContext ctx) { + switch (ctx.t.getType()) { + case PklLexer.STAR: + return MultiplicationNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.DIV: + return DivisionNodeGen.create(createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.INT_DIV: + return TruncatingDivisionNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + case PklLexer.MOD: + return RemainderNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + default: + throw createUnexpectedTokenError(ctx.t); + } + } + + @Override + public Object visitExponentiationExpr(ExponentiationExprContext ctx) { + return ExponentiationNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + } + + @Override + public ExpressionNode visitLogicalAndExpr(LogicalAndExprContext ctx) { + return LogicalAndNodeGen.create(createSourceSection(ctx), visitExpr(ctx.r), visitExpr(ctx.l)); + } + + @Override + public ExpressionNode visitLogicalOrExpr(LogicalOrExprContext ctx) { + return LogicalOrNodeGen.create(createSourceSection(ctx), visitExpr(ctx.r), visitExpr(ctx.l)); + } + + @Override + public ExpressionNode visitLogicalNotExpr(LogicalNotExprContext ctx) { + return LogicalNotNodeGen.create(createSourceSection(ctx), visitExpr(ctx.expr())); + } + + @Override + public ExpressionNode visitQualifiedAccessExpr(QualifiedAccessExprContext ctx) { + if (ctx.argumentList() != null) { + return doVisitMethodAccessExpr(ctx); + } + + return doVisitPropertyInvocationExpr(ctx); + } + + private ExpressionNode doVisitMethodAccessExpr(QualifiedAccessExprContext ctx) { + var sourceSection = createSourceSection(ctx); + var functionName = toIdentifier(ctx.Identifier()); + var argCtx = ctx.argumentList(); + var receiver = visitExpr(ctx.expr()); + + var currentScope = symbolTable.getCurrentScope(); + var constLevel = currentScope.getConstLevel(); + var needsConst = false; + if (receiver instanceof OuterNode) { + var outerScope = getParentLexicalScope(); + if (outerScope != null) { + switch (constLevel) { + case MODULE: + needsConst = outerScope.isModuleScope(); + break; + case ALL: + needsConst = outerScope.getConstLevel() != ConstLevel.ALL; + break; + } + } + } else if (receiver instanceof GetModuleNode) { + needsConst = constLevel != ConstLevel.NONE; + } else if (receiver instanceof ThisNode) { + var constDepth = currentScope.getConstDepth(); + needsConst = constLevel == ConstLevel.ALL && constDepth == -1; + } + + if (ctx.t.getType() == PklLexer.QDOT) { + //noinspection ConstantConditions + return new NullPropagatingOperationNode( + sourceSection, + InvokeMethodVirtualNodeGen.create( + sourceSection, + functionName, + visitArgumentList(argCtx), + MemberLookupMode.EXPLICIT_RECEIVER, + needsConst, + PropagateNullReceiverNodeGen.create(unavailableSourceSection(), receiver), + GetClassNodeGen.create(null))); + } + + assert ctx.t.getType() == PklLexer.DOT; + //noinspection ConstantConditions + return InvokeMethodVirtualNodeGen.create( + sourceSection, + functionName, + visitArgumentList(argCtx), + MemberLookupMode.EXPLICIT_RECEIVER, + needsConst, + receiver, + GetClassNodeGen.create(null)); + } + + private ExpressionNode doVisitPropertyInvocationExpr(QualifiedAccessExprContext ctx) { + var sourceSection = createSourceSection(ctx); + var propertyName = toIdentifier(ctx.Identifier()); + var receiver = visitExpr(ctx.expr()); + + if (receiver instanceof IntLiteralNode) { + var durationUnit = VmDuration.toUnit(propertyName); + if (durationUnit != null) { + //noinspection ConstantConditions + return new ConstantValueNode( + sourceSection, + new VmDuration(((IntLiteralNode) receiver).executeInt(null), durationUnit)); + } + var dataSizeUnit = VmDataSize.toUnit(propertyName); + if (dataSizeUnit != null) { + //noinspection ConstantConditions + return new ConstantValueNode( + sourceSection, + new VmDataSize(((IntLiteralNode) receiver).executeInt(null), dataSizeUnit)); + } + } + + if (receiver instanceof FloatLiteralNode) { + var durationUnit = VmDuration.toUnit(propertyName); + if (durationUnit != null) { + //noinspection ConstantConditions + return new ConstantValueNode( + sourceSection, + new VmDuration(((FloatLiteralNode) receiver).executeFloat(null), durationUnit)); + } + var dataSizeUnit = VmDataSize.toUnit(propertyName); + if (dataSizeUnit != null) { + //noinspection ConstantConditions + return new ConstantValueNode( + sourceSection, + new VmDataSize(((FloatLiteralNode) receiver).executeFloat(null), dataSizeUnit)); + } + } + + var constLevel = symbolTable.getCurrentScope().getConstLevel(); + var needsConst = false; + if (receiver instanceof OuterNode) { + var outerScope = getParentLexicalScope(); + if (outerScope != null) { + switch (constLevel) { + case MODULE: + needsConst = outerScope.isModuleScope(); + break; + case ALL: + needsConst = outerScope.getConstLevel() != ConstLevel.ALL; + break; + } + } + } else if (receiver instanceof GetModuleNode) { + needsConst = constLevel != ConstLevel.NONE; + } else if (receiver instanceof ThisNode) { + var constDepth = symbolTable.getCurrentScope().getConstDepth(); + needsConst = constLevel == ConstLevel.ALL && constDepth == -1; + } + if (ctx.t.getType() == PklLexer.QDOT) { + return new NullPropagatingOperationNode( + sourceSection, + ReadPropertyNodeGen.create( + sourceSection, + propertyName, + needsConst, + PropagateNullReceiverNodeGen.create(unavailableSourceSection(), receiver))); + } + + assert ctx.t.getType() == PklLexer.DOT; + return ReadPropertyNodeGen.create(sourceSection, propertyName, needsConst, receiver); + } + + @Override + public ExpressionNode visitSuperAccessExpr(SuperAccessExprContext ctx) { + var sourceSection = createSourceSection(ctx); + var memberName = toIdentifier(ctx.Identifier()); + var argCtx = ctx.argumentList(); + var currentScope = symbolTable.getCurrentScope(); + var needsConst = + currentScope.getConstLevel() == ConstLevel.ALL && currentScope.getConstDepth() == -1; + + if (argCtx != null) { // supermethod call + if (!symbolTable.getCurrentScope().isClassMemberScope()) { + throw exceptionBuilder() + .evalError("cannotInvokeSupermethodFromHere") + .withSourceSection(sourceSection) + .build(); + } + + return InvokeSuperMethodNodeGen.create( + sourceSection, memberName, visitArgumentList(argCtx), needsConst); + } + + // superproperty call + return new ReadSuperPropertyNode(createSourceSection(ctx), memberName, needsConst); + } + + @Override + public ExpressionNode visitSuperSubscriptExpr(SuperSubscriptExprContext ctx) { + checkClosingDelimiter(ctx.err, "]", ctx.e.stop); + + return new ReadSuperEntryNode(createSourceSection(ctx), visitExpr(ctx.e)); + } + + @Override + public Object visitPipeExpr(PipeExprContext ctx) { + return PipeNodeGen.create(createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + } + + @Override + public ExpressionNode visitNullCoalesceExpr(NullCoalesceExprContext ctx) { + return NullCoalescingNodeGen.create( + createSourceSection(ctx), visitExpr(ctx.r), visitExpr(ctx.l)); + } + + @Override + public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExprContext ctx) { + var identifier = toIdentifier(ctx.Identifier()); + var argListCtx = ctx.argumentList(); + + if (argListCtx == null) { + return createResolveVariableNode(createSourceSection(ctx), identifier); + } + + // 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 + if (identifier == Identifier.LIST) { + return doVisitListLiteral(ctx, argListCtx); + } + + if (identifier == Identifier.SET) { + return doVisitSetLiteral(ctx, argListCtx); + } + + if (identifier == Identifier.MAP) { + return doVisitMapLiteral(ctx, argListCtx); + } + + var scope = symbolTable.getCurrentScope(); + + return new ResolveMethodNode( + createSourceSection(ctx), + identifier, + visitArgumentList(argListCtx), + isBaseModule, + scope.isCustomThisScope(), + scope.getConstLevel(), + scope.getConstDepth()); + } + + @Override + public ExpressionNode[] visitArgumentList(ArgumentListContext ctx) { + checkCommaSeparatedElements(ctx, ctx.es, ctx.errs); + checkClosingDelimiter(ctx.err, ")", ctx.stop); + + return ctx.es.stream().map(this::visitExpr).toArray(ExpressionNode[]::new); + } + + private Identifier toIdentifier(TerminalNode node) { + return Identifier.get(node.getText()); + } + + private Identifier toIdentifier(Token token) { + return Identifier.get(token.getText()); + } + + private FrameDescriptor.Builder createFrameDescriptorBuilder(ParameterListContext ctx) { + checkCommaSeparatedElements(ctx, ctx.ts, ctx.errs); + checkClosingDelimiter(ctx.err, ")", ctx.stop); + + var builder = FrameDescriptor.newBuilder(ctx.ts.size()); + for (var param : ctx.ts) { + var ident = isIgnored(param) ? null : toIdentifier(param.typedIdentifier().Identifier()); + builder.addSlot(FrameSlotKind.Illegal, ident, null); + } + return builder; + } + + private FrameDescriptor.@Nullable Builder createFrameDescriptorBuilder(ObjectBodyContext ctx) { + if (ctx.ps.isEmpty()) return null; + + checkCommaSeparatedElements(ctx, ctx.ps, ctx.errs); + + var builder = FrameDescriptor.newBuilder(ctx.ps.size()); + for (var param : ctx.ps) { + var ident = isIgnored(param) ? null : toIdentifier(param.typedIdentifier().Identifier()); + builder.addSlot(FrameSlotKind.Illegal, ident, null); + } + return builder; + } + + private UnresolvedTypeNode[] doVisitParameterTypes(ParameterListContext ctx) { + return ctx.ts.stream() + .map( + it -> isIgnored(it) ? null : visitTypeAnnotation(it.typedIdentifier().typeAnnotation())) + .toArray(UnresolvedTypeNode[]::new); + } + + private UnresolvedTypeNode[] doVisitParameterTypes(ObjectBodyContext ctx) { + return ctx.ps.stream() + .map( + it -> isIgnored(it) ? null : visitTypeAnnotation(it.typedIdentifier().typeAnnotation())) + .toArray(UnresolvedTypeNode[]::new); + } + + @Override + public Object visitTypedIdentifier(TypedIdentifierContext ctx) { + throw exceptionBuilder().unreachableCode().build(); // handled directly + } + + @Override + public Object visitThrowExpr(ThrowExprContext ctx) { + var exprCtx = ctx.expr(); + checkClosingDelimiter(ctx.err, ")", exprCtx.stop); + + return ThrowNodeGen.create(createSourceSection(ctx), visitExpr(exprCtx)); + } + + @Override + public Object visitTraceExpr(TraceExprContext ctx) { + var exprCtx = ctx.expr(); + checkClosingDelimiter(ctx.err, ")", exprCtx.stop); + + return new TraceNode(createSourceSection(ctx), visitExpr(exprCtx)); + } + + @Override + public Object visitImportExpr(ImportExprContext ctx) { + var importUriCtx = ctx.stringConstant(); + checkClosingDelimiter(ctx.err, ")", importUriCtx.stop); + return doVisitImport(ctx.t.getType(), ctx, importUriCtx); + } + + @Override + public Object visitIfExpr(IfExprContext ctx) { + checkClosingDelimiter(ctx.err, ")", ctx.c.stop); + + return new IfElseNode( + createSourceSection(ctx), visitExpr(ctx.c), visitExpr(ctx.l), visitExpr(ctx.r)); + } + + @Override + public Object visitReadExpr(ReadExprContext ctx) { + var exprCtx = ctx.expr(); + checkClosingDelimiter(ctx.err, ")", exprCtx.stop); + + var tokenType = ctx.t.getType(); + + if (tokenType == PklLexer.READ) { + return ReadNodeGen.create(createSourceSection(ctx), moduleKey, visitExpr(exprCtx)); + } + if (tokenType == PklLexer.READ_OR_NULL) { + return ReadOrNullNodeGen.create(createSourceSection(ctx), moduleKey, visitExpr(exprCtx)); + } + assert tokenType == PklLexer.READ_GLOB; + return ReadGlobNodeGen.create( + language, createSourceSection(ctx), moduleKey, visitExpr(exprCtx)); + } + + @Override + public Object visitLetExpr(LetExprContext ctx) { + checkClosingDelimiter(ctx.err, ")", ctx.l.stop); + + var sourceSection = createSourceSection(ctx); + var idCtx = ctx.parameter(); + var frameBuilder = FrameDescriptor.newBuilder(); + var isIgnored = isIgnored(idCtx); + var typeNodes = + isIgnored + ? new UnresolvedTypeNode[0] + : new UnresolvedTypeNode[] { + visitTypeAnnotation(idCtx.typedIdentifier().typeAnnotation()) + }; + if (!isIgnored) { + frameBuilder.addSlot( + FrameSlotKind.Illegal, toIdentifier(idCtx.typedIdentifier().Identifier()), null); + } + + var isCustomThisScope = symbolTable.getCurrentScope().isCustomThisScope(); + + UnresolvedFunctionNode functionNode = + symbolTable.enterLambda( + frameBuilder, + scope -> { + var expr = visitExpr(ctx.r); + return new UnresolvedFunctionNode( + language, + scope.buildFrameDescriptor(), + new Lambda(createSourceSection(ctx.r), scope.getQualifiedName()), + 1, + typeNodes, + null, + expr); + }); + + return new LetExprNode(sourceSection, functionNode, visitExpr(ctx.l), isCustomThisScope); + } + + @Override + public ExpressionNode visitThisExpr(ThisExprContext ctx) { + if (!(ctx.parent instanceof QualifiedAccessExprContext)) { + var currentScope = symbolTable.getCurrentScope(); + var needsConst = + currentScope.getConstLevel() == ConstLevel.ALL + && currentScope.getConstDepth() == -1 + && !currentScope.isCustomThisScope(); + if (needsConst) { + throw exceptionBuilder() + .withSourceSection(createSourceSection(ctx)) + .evalError("thisIsNotConst") + .build(); + } + } + return VmUtils.createThisNode( + createSourceSection(ctx), symbolTable.getCurrentScope().isCustomThisScope()); + } + + // TODO: `outer.` should probably have semantics similar to `super.`, + // rather than just performing a lookup in the immediately enclosing object + // also, consider interpreting `x = ... x ...` as `x = ... outer.x ...` + @Override + public OuterNode visitOuterExpr(OuterExprContext ctx) { + if (!(ctx.parent instanceof QualifiedAccessExprContext)) { + var constLevel = symbolTable.getCurrentScope().getConstLevel(); + var outerScope = getParentLexicalScope(); + if (outerScope != null && constLevel.bigger(outerScope.getConstLevel())) { + throw exceptionBuilder() + .evalError("outerIsNotConst") + .withSourceSection(createSourceSection(ctx)) + .build(); + } + } + return new OuterNode(createSourceSection(ctx)); + } + + @Override + public Object visitModuleExpr(ModuleExprContext ctx) { + // cannot use unqualified `module` in a const context + if (symbolTable.getCurrentScope().getConstLevel().isConst() + && !(ctx.parent instanceof QualifiedAccessExprContext)) { + var scope = symbolTable.getCurrentScope(); + while (scope != null + && !(scope instanceof AnnotationScope) + && !(scope instanceof ClassScope)) { + scope = scope.getParent(); + } + if (scope == null) { + throw exceptionBuilder() + .evalError("moduleIsNotConst", symbolTable.getCurrentScope().getName().toString()) + .withSourceSection(createSourceSection(ctx)) + .build(); + } + var messageKey = + scope instanceof AnnotationScope ? "moduleIsNotConstAnnotation" : "moduleIsNotConstClass"; + throw exceptionBuilder() + .evalError(messageKey) + .withSourceSection(createSourceSection(ctx)) + .build(); + } + return new GetModuleNode(createSourceSection(ctx)); + } + + @Override + public ExpressionNode visitParenthesizedExpr(ParenthesizedExprContext ctx) { + checkClosingDelimiter(ctx.err, ")", ctx.stop); + + return visitExpr(ctx.expr()); + } + + @Override + public Object visitSubscriptExpr(SubscriptExprContext ctx) { + checkClosingDelimiter(ctx.err, "]", ctx.stop); + + return SubscriptNodeGen.create(createSourceSection(ctx), visitExpr(ctx.l), visitExpr(ctx.r)); + } + + @Override + public Object visitTypeTestExpr(TypeTestExprContext ctx) { + if (ctx.t.getType() == PklLexer.IS) { + return new TypeTestNode(createSourceSection(ctx), visitExpr(ctx.l), visitType(ctx.r)); + } + + assert ctx.t.getType() == PklLexer.AS; + return new TypeCastNode(createSourceSection(ctx), visitExpr(ctx.l), visitType(ctx.r)); + } + + @Override + public Object visitUnknownType(UnknownTypeContext ctx) { + return new UnresolvedTypeNode.Unknown(createSourceSection(ctx)); + } + + @Override + public Object visitNothingType(NothingTypeContext ctx) { + return new UnresolvedTypeNode.Nothing(createSourceSection(ctx)); + } + + @Override + public Object visitModuleType(ModuleTypeContext ctx) { + return new UnresolvedTypeNode.Module(createSourceSection(ctx)); + } + + @Override + public Object visitStringLiteralType(StringLiteralTypeContext ctx) { + return new UnresolvedTypeNode.StringLiteral( + createSourceSection(ctx), visitStringConstant(ctx.stringConstant())); + } + + @Override + public UnresolvedTypeNode visitType(TypeContext ctx) { + return (UnresolvedTypeNode) ctx.accept(this); + } + + @Override + public UnresolvedTypeNode visitDeclaredType(DeclaredTypeContext ctx) { + var idCtx = ctx.qualifiedIdentifier(); + var argCtx = ctx.typeArgumentList(); + + if (argCtx == null) { + if (idCtx.ts.size() == 1) { + String text = idCtx.ts.get(0).getText(); + TypeParameter typeParameter = symbolTable.findTypeParameter(text); + if (typeParameter != null) { + return new UnresolvedTypeNode.TypeVariable(createSourceSection(ctx), typeParameter); + } + } + + return new UnresolvedTypeNode.Declared(createSourceSection(ctx), doVisitTypeName(idCtx)); + } + + checkCommaSeparatedElements(argCtx, argCtx.ts, argCtx.errs); + checkClosingDelimiter(argCtx.err, ">", argCtx.stop); + + return new UnresolvedTypeNode.Parameterized( + createSourceSection(ctx), + doVisitTypeName(idCtx), + argCtx.ts.stream().map(this::visitType).toArray(UnresolvedTypeNode[]::new)); + } + + @Override + public UnresolvedTypeNode visitParenthesizedType(ParenthesizedTypeContext ctx) { + checkClosingDelimiter(ctx.err, ")", ctx.stop); + + return visitType(ctx.type()); + } + + @Override + public Object visitDefaultUnionType(DefaultUnionTypeContext ctx) { + throw exceptionBuilder() + .evalError("notAUnion") + .withSourceSection(createSourceSection(ctx)) + .build(); + } + + @Override + public UnresolvedTypeNode visitUnionType(UnionTypeContext ctx) { + var elementTypeCtxs = new ArrayList(); + + var result = flattenUnionType(ctx, elementTypeCtxs); + boolean isUnionOfStringLiterals = result.first; + int defaultIndex = result.second; + + if (isUnionOfStringLiterals) { + return new UnresolvedTypeNode.UnionOfStringLiterals( + createSourceSection(ctx), + defaultIndex, + elementTypeCtxs.stream() + .map(it -> visitStringConstant(((StringLiteralTypeContext) it).stringConstant())) + .collect(Collectors.toCollection(LinkedHashSet::new))); + } + + return new UnresolvedTypeNode.Union( + createSourceSection(ctx), + defaultIndex, + elementTypeCtxs.stream().map(this::visitType).toArray(UnresolvedTypeNode[]::new)); + } + + private Pair flattenUnionType( + UnionTypeContext ctx, List collector) { + boolean isUnionOfStringLiterals = true; + int index = 0; + int defaultIndex = -1; + var list = new ArrayDeque(); + list.addLast(ctx.l); + list.addLast(ctx.r); + + while (!list.isEmpty()) { + var current = list.removeFirst(); + if (current instanceof UnionTypeContext) { + var union = (UnionTypeContext) current; + list.addFirst(union.r); + list.addFirst(union.l); + continue; + } + if (current instanceof DefaultUnionTypeContext) { + if (defaultIndex == -1) { + defaultIndex = index; + } else { + throw exceptionBuilder() + .evalError("multipleUnionDefaults") + .withSourceSection(createSourceSection(ctx)) + .build(); + } + var def = (DefaultUnionTypeContext) current; + isUnionOfStringLiterals = + isUnionOfStringLiterals && def.type() instanceof StringLiteralTypeContext; + collector.add(def.type()); + } else { + isUnionOfStringLiterals = + isUnionOfStringLiterals && current instanceof StringLiteralTypeContext; + collector.add(current); + } + index++; + } + return Pair.of(isUnionOfStringLiterals, defaultIndex); + } + + @Override + public UnresolvedTypeNode visitNullableType(NullableTypeContext ctx) { + return new UnresolvedTypeNode.Nullable( + createSourceSection(ctx), (UnresolvedTypeNode) ctx.type().accept(this)); + } + + @Override + public UnresolvedTypeNode visitConstrainedType(ConstrainedTypeContext ctx) { + checkCommaSeparatedElements(ctx, ctx.es, ctx.errs); + checkClosingDelimiter(ctx.err, ")", ctx.stop); + + var childNode = (UnresolvedTypeNode) ctx.type().accept(this); + + return symbolTable.enterCustomThisScope( + scope -> + new UnresolvedTypeNode.Constrained( + createSourceSection(ctx), + childNode, + ctx.es.stream() + .map(this::visitExpr) + .map(it -> TypeConstraintNodeGen.create(it.getSourceSection(), it)) + .toArray(TypeConstraintNode[]::new))); + } + + @Override + public UnresolvedTypeNode visitFunctionType(FunctionTypeContext ctx) { + checkCommaSeparatedElements(ctx, ctx.ps, ctx.errs); + checkClosingDelimiter( + ctx.err, ")", ctx.ps.isEmpty() ? ctx.t : ctx.ps.get(ctx.ps.size() - 1).stop); + + return new UnresolvedTypeNode.Function( + createSourceSection(ctx), + ctx.ps.stream().map(this::visitType).toArray(UnresolvedTypeNode[]::new), + (UnresolvedTypeNode) ctx.r.accept(this)); + } + + private ExpressionNode resolveBaseModuleClass(Identifier className, Supplier clazz) { + return isBaseModule + ? + // Can't access BaseModule.getXYZClass() while parsing base module + new GetBaseModuleClassNode(className) + : new ConstantValueNode(clazz.get()); + } + + private UnresolvedPropertyNode[] doVisitClassProperties( + List propertyContexts, Set propertyNames) { + var propertyNodes = new UnresolvedPropertyNode[propertyContexts.size()]; + + for (var i = 0; i < propertyNodes.length; i++) { + var propertyCtx = propertyContexts.get(i); + var propertyNode = visitClassProperty(propertyCtx); + checkDuplicateMember(propertyNode.getName(), propertyNode.getHeaderSection(), propertyNames); + propertyNodes[i] = propertyNode; + } + + return propertyNodes; + } + + private UnresolvedMethodNode[] doVisitMethodDefs(List methodDefs) { + var methodNodes = new UnresolvedMethodNode[methodDefs.size()]; + var methodNames = CollectionUtils.newHashSet(methodDefs.size()); + + for (var i = 0; i < methodNodes.length; i++) { + var methodNode = visitClassMethod(methodDefs.get(i)); + checkDuplicateMember(methodNode.getName(), methodNode.getHeaderSection(), methodNames); + methodNodes[i] = methodNode; + } + + return methodNodes; + } + + private EconomicMap doVisitModuleProperties( + List importCtxs, + List classCtxs, + List typeAliasCtxs, + List propertyCtxs, + Set propertyNames, + ModuleInfo moduleInfo) { + + var totalSize = importCtxs.size() + classCtxs.size() + typeAliasCtxs.size(); + var result = EconomicMaps.create(totalSize); + + for (var ctx : importCtxs) { + var member = visitImportClause(ctx); + checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); + EconomicMaps.put(result, member.getName(), member); + } + + for (var ctx : classCtxs) { + ObjectMember member = visitClazz(ctx); + + if (moduleInfo.isAmend() && !member.isLocal()) { + throw exceptionBuilder() + .evalError("classMustBeLocal") + .withSourceSection(member.getHeaderSection()) + .build(); + } + + checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); + EconomicMaps.put(result, member.getName(), member); + } + + for (TypeAliasContext ctx : typeAliasCtxs) { + var member = visitTypeAlias(ctx); + + if (moduleInfo.isAmend() && !member.isLocal()) { + throw exceptionBuilder() + .evalError("typeAliasMustBeLocal") + .withSourceSection(member.getHeaderSection()) + .build(); + } + + checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); + EconomicMaps.put(result, member.getName(), member); + } + + for (var ctx : propertyCtxs) { + var member = + doVisitObjectProperty( + ctx, + ctx.modifier(), + ctx.Identifier(), + ctx.typeAnnotation(), + ctx.expr(), + ctx.objectBody()); + + if (moduleInfo.isAmend() && !member.isLocal() && ctx.typeAnnotation() != null) { + throw exceptionBuilder() + .evalError("nonLocalObjectPropertyCannotHaveTypeAnnotation") + .withSourceSection(createSourceSection(ctx.typeAnnotation().type())) + .build(); + } + + checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); + EconomicMaps.put(result, member.getName(), member); + } + + return result; + } + + private void checkDuplicateMember( + Identifier memberName, + SourceSection headerSection, + // use Set rather than Set + // to detect conflicts between local and non-local identifiers + Set visited) { + + if (!visited.add(memberName.toString())) { + throw exceptionBuilder() + .evalError("duplicateDefinition", memberName) + .withSourceSection(headerSection) + .build(); + } + } + + // TODO: use Set and checkDuplicateMember() to find duplicates between local and non-local + // properties + private void addProperty(EconomicMap objectMembers, ObjectMember property) { + if (EconomicMaps.put(objectMembers, property.getName(), property) != null) { + throw exceptionBuilder() + .evalError("duplicateDefinition", property.getName()) + .withSourceSection(property.getHeaderSection()) + .build(); + } + } + + private void invalidSeparatorPosition(SourceSection source) { + throw exceptionBuilder() + .evalError("invalidSeparatorPosition") + .withSourceSection(source) + .build(); + } + + private AbstractImportNode doVisitImport( + int lexerToken, ParserRuleContext ctx, StringConstantContext importUriCtx) { + var isGlobImport = lexerToken == PklLexer.IMPORT_GLOB; + var section = createSourceSection(ctx); + var importUri = visitStringConstant(importUriCtx); + if (isGlobImport && importUri.startsWith("...")) { + throw exceptionBuilder().evalError("cannotGlobTripleDots").withSourceSection(section).build(); + } + var resolvedUri = resolveImport(importUri, importUriCtx); + if (isGlobImport) { + return new ImportGlobNode( + language, section, moduleInfo.getResolvedModuleKey(), resolvedUri, importUri); + } + return new ImportNode(language, section, moduleInfo.getResolvedModuleKey(), resolvedUri); + } + + private SourceSection startOf(TerminalNode node) { + return startOf(node.getSymbol()); + } + + private SourceSection startOf(Token token) { + return source.createSection(token.getStartIndex(), 1); + } + + private SourceSection shrinkLeft(SourceSection section, int length) { + return source.createSection(section.getCharIndex() + length, section.getCharLength() - length); + } + + private VmException createUnexpectedTokenError(Token token) { + return exceptionBuilder().bug("Unexpected token `%s`.", token).build(); + } + + @Override + protected VmExceptionBuilder exceptionBuilder() { + return new VmExceptionBuilder() + .withMemberName(symbolTable.getCurrentScope().getQualifiedName()); + } + + private static SourceSection unavailableSourceSection() { + return VmUtils.unavailableSourceSection(); + } + + private String getCommonIndent(MultiLineStringPartContext lastPart, Token endQuoteToken) { + if (lastPart.e != null) { + throw exceptionBuilder() + .evalError("closingStringDelimiterMustBeginOnNewLine") + .withSourceSection(startOf(endQuoteToken)) + .build(); + } + + var tokens = lastPart.ts; + assert tokens.size() >= 1; + var lastToken = tokens.get(tokens.size() - 1); + + if (lastToken.getType() == PklLexer.MLNewline) { + return ""; + } + + if (tokens.size() > 1) { + var lastButOneToken = tokens.get(tokens.size() - 2); + if (lastButOneToken.getType() == PklLexer.MLNewline && isIndentChars(lastToken)) { + return lastToken.getText(); + } + } + + throw exceptionBuilder() + .evalError("closingStringDelimiterMustBeginOnNewLine") + .withSourceSection(startOf(endQuoteToken)) + .build(); + } + + private static boolean isIndentChars(Token token) { + var text = token.getText(); + + for (var i = 0; i < text.length(); i++) { + switch (text.charAt(i)) { + case ' ': + case '\t': + continue; + default: + return false; + } + } + + return true; + } + + private static String getLeadingIndent(Token token) { + var text = token.getText(); + + for (var i = 0; i < text.length(); i++) { + switch (text.charAt(i)) { + case ' ': + case '\t': + continue; + default: + return text.substring(0, i); + } + } + + return text; + } + + private ExpressionNode doVisitMultiLineStringPart( + MultiLineStringPartContext ctx, + String commonIndent, + boolean isStringStart, + boolean isStringEnd) { + + if (ctx.e != null) { + return ToStringNodeGen.create(createSourceSection(ctx), visitExpr(ctx.e)); + } + + return new ConstantValueNode( + createSourceSection(ctx), + doVisitMultiLineConstantStringPart(ctx.ts, commonIndent, isStringStart, isStringEnd)); + } + + private String doVisitMultiLineConstantStringPart( + List tokens, String commonIndent, boolean isStringStart, boolean isStringEnd) { + + int startIndex = 0; + if (isStringStart) { + // skip leading newline token + startIndex = 1; + } + + var endIndex = tokens.size() - 1; + if (isStringEnd) { + if (tokens.get(endIndex).getType() == PklLexer.MLNewline) { + // skip trailing newline token + endIndex -= 1; + } else { + // skip trailing newline and whitespace (common indent) tokens + endIndex -= 2; + } + } + + var builder = new StringBuilder(); + var isLineStart = isStringStart; + + for (var i = startIndex; i <= endIndex; i++) { + Token token = tokens.get(i); + + switch (token.getType()) { + case PklLexer.MLNewline: + builder.append('\n'); + isLineStart = true; + break; + case PklLexer.MLCharacters: + var text = token.getText(); + if (isLineStart) { + if (text.startsWith(commonIndent)) { + builder.append(text, commonIndent.length(), text.length()); + } else { + String actualIndent = getLeadingIndent(token); + if (actualIndent.length() > commonIndent.length()) { + actualIndent = actualIndent.substring(0, commonIndent.length()); + } + throw exceptionBuilder() + .evalError("stringIndentationMustMatchLastLine") + .withSourceSection(shrinkLeft(createSourceSection(token), actualIndent.length())) + .build(); + } + } else { + builder.append(text); + } + isLineStart = false; + break; + case PklLexer.MLCharacterEscape: + if (isLineStart && !commonIndent.isEmpty()) { + throw exceptionBuilder() + .evalError("stringIndentationMustMatchLastLine") + .withSourceSection(createSourceSection(token)) + .build(); + } + builder.append(parseCharacterEscapeSequence(token)); + isLineStart = false; + break; + case PklLexer.MLUnicodeEscape: + if (isLineStart && !commonIndent.isEmpty()) { + throw exceptionBuilder() + .evalError("stringIndentationMustMatchLastLine") + .withSourceSection(createSourceSection(token)) + .build(); + } + builder.appendCodePoint(parseUnicodeEscapeSequence(token)); + isLineStart = false; + break; + default: + throw exceptionBuilder().unreachableCode().build(); + } + } + + return builder.toString(); + } + + private ResolveDeclaredTypeNode doVisitTypeName(QualifiedIdentifierContext ctx) { + var tokens = ctx.ts; + switch (tokens.size()) { + case 1: + var token = tokens.get(0); + return new ResolveSimpleDeclaredTypeNode( + createSourceSection(token), Identifier.get(token.getText()), isBaseModule); + case 2: + var token1 = tokens.get(0); + var token2 = tokens.get(1); + return new ResolveQualifiedDeclaredTypeNode( + createSourceSection(ctx), + createSourceSection(token1), + createSourceSection(token2), + Identifier.localProperty(token1.getText()), + Identifier.get(token2.getText())); + default: + throw exceptionBuilder() + .evalError("invalidTypeName", ctx.getText()) + .withSourceSection(createSourceSection(ctx)) + .build(); + } + } + + private void checkCommaSeparatedElements( + ParserRuleContext ctx, List elements, List separators) { + + if (elements.isEmpty() || separators.size() == elements.size() - 1) return; + + // determine location of missing separator + // O(n^2) but only runs once a syntax error has been detected + ParseTree prevChild = null; + for (ParseTree child : ctx.children) { + @SuppressWarnings("SuspiciousMethodCalls") + var index = elements.indexOf(child); + if (index > 0) { // 0 rather than -1 because no separator is expected before first element + assert prevChild != null; + if (!(prevChild instanceof TerminalNode) + || !separators.contains(((TerminalNode) prevChild).getSymbol())) { + var prevToken = + prevChild instanceof TerminalNode + ? ((TerminalNode) prevChild).getSymbol() + : ((ParserRuleContext) prevChild).getStop(); + throw exceptionBuilder() + .evalError("missingCommaSeparator") + .withSourceSection(source.createSection(prevToken.getStopIndex() + 1, 1)) + .build(); + } + } + prevChild = child; + } + + throw exceptionBuilder().unreachableCode().build(); + } + + private void checkClosingDelimiter( + @Nullable Token delimiter, String delimiterSymbol, Token tokenBeforeDelimiter) { + + if (delimiter == null) { + throw missingDelimiter(delimiterSymbol, tokenBeforeDelimiter.getStopIndex() + 1); + } + } + + private void checkSingleLineStringDelimiters(Token openingDelimiter, Token closingDelimiter) { + var closingText = closingDelimiter.getText(); + var lastChar = closingText.charAt(closingText.length() - 1); + if (lastChar == '"' || lastChar == '#') return; + + assert lastChar == '\n' || lastChar == '\r'; + var openingText = openingDelimiter.getText(); + throw missingDelimiter( + "\"" + openingText.substring(0, openingText.length() - 1), closingDelimiter.getStopIndex()); + } + + private VmException missingDelimiter(String delimiter, int charIndex) { + return exceptionBuilder() + .evalError("missingDelimiter", delimiter) + .withSourceSection(source.createSection(charIndex, 0)) + .build(); + } + + private VmException wrongDelimiter(String expected, String actual, int charIndex) { + return exceptionBuilder() + .evalError("wrongDelimiter", expected, actual) + .withSourceSection(source.createSection(charIndex, 0)) + .build(); + } + + private VmException danglingDelimiter(String delimiter, int charIndex) { + return exceptionBuilder() + .evalError("danglingDelimiter", delimiter) + .withSourceSection(source.createSection(charIndex, 0)) + .build(); + } + + private @Nullable Scope getParentLexicalScope() { + var parent = symbolTable.getCurrentScope().getLexicalScope().getParent(); + if (parent != null) return parent.getLexicalScope(); + return null; + } + + private ConstLevel getConstLevel(int modifiers) { + if (VmModifier.isConst(modifiers)) return ConstLevel.ALL; + return symbolTable.getCurrentScope().getConstLevel(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/CannotInvokeAbstractFunctionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/CannotInvokeAbstractFunctionNode.java new file mode 100644 index 00000000..b0cb08d0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/CannotInvokeAbstractFunctionNode.java @@ -0,0 +1,36 @@ +/** + * 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.ast.builder; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; + +public final class CannotInvokeAbstractFunctionNode extends ExpressionNode { + private final String functionName; + + public CannotInvokeAbstractFunctionNode(SourceSection section, String functionName) { + super(section); + this.functionName = functionName; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("cannotInvokeAbstractMethod", functionName).build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/CannotInvokeAbstractPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/CannotInvokeAbstractPropertyNode.java new file mode 100644 index 00000000..a7cc08b4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/CannotInvokeAbstractPropertyNode.java @@ -0,0 +1,36 @@ +/** + * 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.ast.builder; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; + +public final class CannotInvokeAbstractPropertyNode extends ExpressionNode { + private final String propertyName; + + public CannotInvokeAbstractPropertyNode(SourceSection section, String propertyName) { + super(section); + this.propertyName = propertyName; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("cannotInvokeAbstractProperty", propertyName).build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/ConstLevel.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/ConstLevel.java new file mode 100644 index 00000000..3c6a89bb --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/ConstLevel.java @@ -0,0 +1,34 @@ +/** + * 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.ast.builder; + +public enum ConstLevel { + NONE, + MODULE, + ALL; + + public boolean isConst() { + return this != NONE; + } + + public boolean biggerOrEquals(ConstLevel other) { + return this.ordinal() >= other.ordinal(); + } + + public boolean bigger(ConstLevel other) { + return this.ordinal() > other.ordinal(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/ImportsAndReadsParser.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/ImportsAndReadsParser.java new file mode 100644 index 00000000..42e8fbec --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/ImportsAndReadsParser.java @@ -0,0 +1,133 @@ +/** + * 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.ast.builder; + +import com.oracle.truffle.api.source.Source; +import com.oracle.truffle.api.source.SourceSection; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.parser.Parser; +import org.pkl.core.parser.antlr.PklParser.ImportClauseContext; +import org.pkl.core.parser.antlr.PklParser.ImportExprContext; +import org.pkl.core.parser.antlr.PklParser.ModuleExtendsOrAmendsClauseContext; +import org.pkl.core.parser.antlr.PklParser.ReadExprContext; +import org.pkl.core.parser.antlr.PklParser.SingleLineStringLiteralContext; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; + +/** + * Collects module uris and resource uris imported within a module. + * + *

Gathers the following: + * + *

    + *
  • amends/extends URI's + *
  • import declarations + *
  • import expressions + *
  • read expressions + *
+ */ +public class ImportsAndReadsParser + extends AbstractAstBuilder<@Nullable List>> { + + /** Parses a module, and collects all imports and reads. */ + public static @Nullable List> parse( + ModuleKey moduleKey, ResolvedModuleKey resolvedModuleKey) throws IOException { + var parser = new Parser(); + var text = resolvedModuleKey.loadSource(); + var source = VmUtils.createSource(moduleKey, text); + var importListParser = new ImportsAndReadsParser(source); + return parser.parseModule(text).accept(importListParser); + } + + public ImportsAndReadsParser(Source source) { + super(source); + } + + @Override + protected VmExceptionBuilder exceptionBuilder() { + return new VmExceptionBuilder(); + } + + @Override + public @Nullable List> visitModuleExtendsOrAmendsClause( + ModuleExtendsOrAmendsClauseContext ctx) { + var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts); + var sourceSection = createSourceSection(ctx.stringConstant()); + return Collections.singletonList(Pair.of(importStr, sourceSection)); + } + + @Override + public List> visitImportClause(ImportClauseContext ctx) { + var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts); + var sourceSection = createSourceSection(ctx.stringConstant()); + return Collections.singletonList(Pair.of(importStr, sourceSection)); + } + + @Override + public List> visitImportExpr(ImportExprContext ctx) { + var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts); + var sourceSection = createSourceSection(ctx.stringConstant()); + return Collections.singletonList(Pair.of(importStr, sourceSection)); + } + + @Override + public List> visitReadExpr(ReadExprContext ctx) { + var expr = ctx.expr(); + if (!(expr instanceof SingleLineStringLiteralContext)) { + return Collections.emptyList(); + } + // best-effort approach; only collect read expressions that are string constants. + var slCtx = (SingleLineStringLiteralContext) expr; + var singleParts = slCtx.singleLineStringPart(); + String importString; + if (singleParts.isEmpty()) { + importString = ""; + } else if (singleParts.size() == 1) { + var ts = singleParts.get(0).ts; + if (!ts.isEmpty()) { + importString = doVisitSingleLineConstantStringPart(ts); + } else { + return Collections.emptyList(); + } + } else { + return Collections.emptyList(); + } + return Collections.singletonList(Pair.of(importString, createSourceSection(slCtx))); + } + + @Override + protected @Nullable List> aggregateResult( + @Nullable List> aggregate, + @Nullable List> nextResult) { + if (aggregate == null || aggregate.isEmpty()) { + return nextResult; + } + if (nextResult == null || nextResult.isEmpty()) { + return aggregate; + } + var ret = new ArrayList>(aggregate.size() + nextResult.size()); + ret.addAll(aggregate); + ret.addAll(nextResult); + return ret; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java new file mode 100644 index 00000000..bdea1944 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java @@ -0,0 +1,485 @@ +/** + * 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.ast.builder; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameDescriptor.Builder; +import com.oracle.truffle.api.frame.FrameSlotKind; +import java.util.*; +import java.util.function.Function; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.ConstantNode; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.parser.Lexer; +import org.pkl.core.parser.antlr.PklParser.ParameterContext; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.ModuleInfo; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; +import org.pkl.core.util.Nullable; + +public final class SymbolTable { + private Scope currentScope; + + public static Object FOR_GENERATOR_VARIABLE = new Object(); + + public SymbolTable(ModuleInfo moduleInfo) { + currentScope = new ModuleScope(moduleInfo); + } + + public Scope getCurrentScope() { + return currentScope; + } + + public @Nullable TypeParameter findTypeParameter(String name) { + TypeParameter result; + for (var scope = currentScope; scope != null; scope = scope.getParent()) { + result = scope.getTypeParameter(name); + if (result != null) return result; + } + return null; + } + + public ObjectMember enterClass( + Identifier name, + List typeParameters, + Function nodeFactory) { + return doEnter( + new ClassScope( + currentScope, + name, + toQualifiedName(name), + FrameDescriptor.newBuilder(), + typeParameters), + nodeFactory); + } + + public ObjectMember enterTypeAlias( + Identifier name, + List typeParameters, + Function nodeFactory) { + return doEnter( + new TypeAliasScope( + currentScope, + name, + toQualifiedName(name), + FrameDescriptor.newBuilder(), + typeParameters), + nodeFactory); + } + + public T enterMethod( + Identifier name, + ConstLevel constLevel, + Builder frameDescriptorBuilder, + List typeParameters, + Function nodeFactory) { + return doEnter( + new MethodScope( + currentScope, + name, + toQualifiedName(name), + constLevel, + frameDescriptorBuilder, + typeParameters), + nodeFactory); + } + + public T enterLambda( + FrameDescriptor.Builder frameDescriptorBuilder, Function nodeFactory) { + + // flatten names of lambdas nested inside other lambdas for presentation purposes + var parentScope = currentScope; + while (parentScope instanceof LambdaScope) { + parentScope = parentScope.getParent(); + } + + assert parentScope != null; + var qualifiedName = parentScope.qualifiedName + "." + parentScope.getNextLambdaName(); + + return doEnter( + new LambdaScope(currentScope, qualifiedName, frameDescriptorBuilder), nodeFactory); + } + + public T enterProperty( + Identifier name, ConstLevel constLevel, Function nodeFactory) { + return doEnter( + new PropertyScope( + currentScope, name, toQualifiedName(name), constLevel, FrameDescriptor.newBuilder()), + nodeFactory); + } + + public T enterEntry( + @Nullable ExpressionNode keyNode, // null for listing elements + Function nodeFactory) { + + var qualifiedName = currentScope.getQualifiedName() + currentScope.getNextEntryName(keyNode); + + return doEnter( + new EntryScope(currentScope, qualifiedName, FrameDescriptor.newBuilder()), nodeFactory); + } + + public T enterCustomThisScope(Function nodeFactory) { + return doEnter( + new CustomThisScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); + } + + public T enterAnnotationScope(Function nodeFactory) { + return doEnter( + new AnnotationScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); + } + + public T enterObjectScope(Function nodeFactory) { + return doEnter(new ObjectScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); + } + + private T doEnter(S scope, Function nodeFactory) { + var parentScope = currentScope; + currentScope = scope; + try { + return nodeFactory.apply(scope); + } finally { + currentScope = parentScope; + } + } + + private String toQualifiedName(Identifier name) { + var separator = currentScope instanceof ModuleScope ? "#" : "."; + return currentScope.qualifiedName + separator + Lexer.maybeQuoteIdentifier(name.toString()); + } + + public abstract static class Scope { + private final @Nullable Scope parent; + private final @Nullable Identifier name; + private final String qualifiedName; + private final Deque forGeneratorVariables = new ArrayDeque<>(); + private int lambdaCount = 0; + private int entryCount = 0; + private final FrameDescriptor.Builder frameDescriptorBuilder; + private final ConstLevel constLevel; + + private Scope( + @Nullable Scope parent, + @Nullable Identifier name, + String qualifiedName, + ConstLevel constLevel, + FrameDescriptor.Builder frameDescriptorBuilder) { + this.parent = parent; + this.name = name; + this.qualifiedName = qualifiedName; + this.frameDescriptorBuilder = frameDescriptorBuilder; + // const level can never decrease + this.constLevel = + parent != null && parent.constLevel.biggerOrEquals(constLevel) + ? parent.constLevel + : constLevel; + } + + public final @Nullable Scope getParent() { + return parent; + } + + public final Identifier getName() { + assert name != null; + return name; + } + + public final @Nullable Identifier getNameOrNull() { + return name; + } + + public final String getQualifiedName() { + return qualifiedName; + } + + public FrameDescriptor buildFrameDescriptor() { + return frameDescriptorBuilder.build(); + } + + public @Nullable TypeParameter getTypeParameter(String name) { + return null; + } + + public final Scope getLexicalScope() { + var scope = this; + while (!scope.isLexicalScope()) { + scope = scope.parent; + assert scope != null; + } + return scope; + } + + private boolean isConst() { + return constLevel.isConst(); + } + + /** + * Returns the lexical depth from the current scope to the top-most scope that is const. Depth + * is 0-indexed, and -1 means that the scope is not a const scope. + * + *

A const scope is a lexical scope on the right-hand side of a const property. + * + *

{@code
+     * const foo = new {
+     *   bar {
+     *     baz // <-- depth == 1
+     *   }
+     * }
+     * }
+ */ + public int getConstDepth() { + var depth = -1; + var lexicalScope = getLexicalScope(); + while (lexicalScope.getConstLevel() == ConstLevel.ALL) { + depth += 1; + var parent = lexicalScope.getParent(); + if (parent == null) { + return depth; + } + lexicalScope = parent.getLexicalScope(); + } + return depth; + } + + /** + * Adds the for generator variable to the frame descriptor. + * + *

Returns {@code -1} if a for-generator variable already exists with this name. + */ + public int pushForGeneratorVariableContext(ParameterContext ctx) { + var variable = Identifier.localProperty(ctx.typedIdentifier().Identifier().getText()); + if (forGeneratorVariables.contains(variable)) { + return -1; + } + var slot = + frameDescriptorBuilder.addSlot(FrameSlotKind.Illegal, variable, FOR_GENERATOR_VARIABLE); + forGeneratorVariables.addLast(variable); + return slot; + } + + public void popForGeneratorVariable() { + forGeneratorVariables.removeLast(); + } + + public Deque getForGeneratorVariables() { + return forGeneratorVariables; + } + + private String getNextLambdaName() { + return ""; + } + + private String getNextEntryName(@Nullable ExpressionNode keyNode) { + if (keyNode instanceof ConstantNode) { + var value = ((ConstantNode) keyNode).getValue(); + if (value instanceof String) { + return "[\"" + value + "\"]"; + } + + if (value instanceof Long + || value instanceof Double + || value instanceof Boolean + || value instanceof VmDuration + || value instanceof VmDataSize) { + return "[" + value + "]"; + } + } + + return "[#" + (++entryCount) + "]"; + } + + public final Scope skipLambdaScopes() { + var curr = this; + while (curr.isLambdaScope()) { + curr = curr.getParent(); + assert curr != null : "Lambda scope always has a parent"; + } + return curr; + } + + public final boolean isModuleScope() { + return this instanceof ModuleScope; + } + + public final boolean isClassScope() { + return this instanceof ClassScope; + } + + public final boolean isClassMemberScope() { + if (parent == null) return false; + + return parent.isClassScope() + || parent.isModuleScope() && !((ModuleScope) parent).moduleInfo.isAmend(); + } + + public final boolean isLambdaScope() { + return this instanceof LambdaScope; + } + + public final boolean isCustomThisScope() { + return this instanceof CustomThisScope; + } + + public final boolean isLexicalScope() { + return this instanceof LexicalScope; + } + + public ConstLevel getConstLevel() { + return constLevel; + } + } + + private interface LexicalScope {} + + public static class ObjectScope extends Scope implements LexicalScope { + private ObjectScope(Scope parent, Builder frameDescriptorBuilder) { + super( + parent, + parent.getNameOrNull(), + parent.getQualifiedName(), + ConstLevel.NONE, + frameDescriptorBuilder); + } + } + + public abstract static class TypeParameterizableScope extends Scope { + private final List typeParameters; + + public TypeParameterizableScope( + Scope parent, + Identifier name, + String qualifiedName, + ConstLevel constLevel, + Builder frameDescriptorBuilder, + List typeParameters) { + super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder); + this.typeParameters = typeParameters; + } + + @Override + public @Nullable TypeParameter getTypeParameter(String name) { + for (var param : typeParameters) { + if (name.equals(param.getName())) return param; + } + return null; + } + } + + public static final class ModuleScope extends Scope implements LexicalScope { + private final ModuleInfo moduleInfo; + + public ModuleScope(ModuleInfo moduleInfo) { + super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, FrameDescriptor.newBuilder()); + this.moduleInfo = moduleInfo; + } + } + + public static final class MethodScope extends TypeParameterizableScope { + public MethodScope( + Scope parent, + Identifier name, + String qualifiedName, + ConstLevel constLevel, + Builder frameDescriptorBuilder, + List typeParameters) { + super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder, typeParameters); + } + } + + public static final class LambdaScope extends Scope implements LexicalScope { + public LambdaScope( + Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) { + super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); + } + } + + public static final class PropertyScope extends Scope { + public PropertyScope( + Scope parent, + Identifier name, + String qualifiedName, + ConstLevel constLevel, + FrameDescriptor.Builder frameDescriptorBuilder) { + super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder); + } + } + + public static final class EntryScope extends Scope { + public EntryScope( + Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) { + super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); + } + } + + public static final class ClassScope extends TypeParameterizableScope implements LexicalScope { + public ClassScope( + Scope parent, + Identifier name, + String qualifiedName, + Builder frameDescriptorBuilder, + List typeParameters) { + super(parent, name, qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder, typeParameters); + } + } + + public static final class TypeAliasScope extends TypeParameterizableScope { + public TypeAliasScope( + Scope parent, + Identifier name, + String qualifiedName, + FrameDescriptor.Builder frameDescriptorBuilder, + List typeParameters) { + super(parent, name, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder, typeParameters); + } + } + + /** + * A scope where {@code this} has a special meaning (type constraint, object member predicate). + * + *

Technically, a scope where {@code this} isn't {@code frame.getArguments()[0]}, but the value + * at an auxiliary slot identified by {@link CustomThisScope#FRAME_SLOT_ID}. + */ + public static final class CustomThisScope extends Scope { + public static final Object FRAME_SLOT_ID = + new Object() { + @Override + public String toString() { + return "customThisSlot"; + } + }; + + public CustomThisScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuilder) { + super( + parent, + parent.getNameOrNull(), + parent.getQualifiedName(), + ConstLevel.NONE, + frameDescriptorBuilder); + } + } + + public static final class AnnotationScope extends Scope implements LexicalScope { + public AnnotationScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuilder) { + super( + parent, + parent.getNameOrNull(), + parent.getQualifiedName(), + ConstLevel.MODULE, + frameDescriptorBuilder); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/package-info.java new file mode 100644 index 00000000..04308f62 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.builder; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/AdditionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/AdditionNode.java new file mode 100644 index 00000000..a281a772 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/AdditionNode.java @@ -0,0 +1,81 @@ +/** + * 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.ast.expression.binary; + +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.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; + +@NodeInfo(shortName = "+") +public abstract class AdditionNode extends BinaryExpressionNode { + protected AdditionNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + @TruffleBoundary + protected String eval(String left, String right) { + return left + right; + } + + @Specialization + protected long eval(long left, long right) { + try { + return StrictMath.addExact(left, right); + } catch (ArithmeticException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("integerOverflow").build(); + } + } + + @Specialization + protected double eval(long left, double right) { + return left + right; + } + + @Specialization + protected double eval(double left, long right) { + return left + right; + } + + @Specialization + protected double eval(double left, double right) { + return left + right; + } + + @Specialization + protected VmDuration eval(VmDuration left, VmDuration right) { + return left.add(right); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, VmDataSize right) { + return left.add(right); + } + + @Specialization + protected VmCollection eval(VmCollection left, VmCollection right) { + return left.concatenate(right); + } + + @Specialization + protected VmMap eval(VmMap left, VmMap right) { + return left.concatenate(right); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/BinaryExpressionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/BinaryExpressionNode.java new file mode 100644 index 00000000..4e34366d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/BinaryExpressionNode.java @@ -0,0 +1,46 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +@NodeChild(value = "leftNode", type = ExpressionNode.class) +@NodeChild(value = "rightNode", type = ExpressionNode.class) +public abstract class BinaryExpressionNode extends ExpressionNode { + protected BinaryExpressionNode(SourceSection sourceSection) { + super(sourceSection); + } + + protected abstract ExpressionNode getLeftNode(); + + protected abstract ExpressionNode getRightNode(); + + @Fallback + @TruffleBoundary + protected Object fallback(Object left, Object right) { + throw exceptionBuilder() + .evalError( + "operatorNotDefined2", getShortName(), VmUtils.getClass(left), VmUtils.getClass(right)) + .withProgramValue("Left operand", left) + .withProgramValue("Right operand", right) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ComparatorNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ComparatorNode.java new file mode 100644 index 00000000..8c3ad09a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ComparatorNode.java @@ -0,0 +1,26 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.source.SourceSection; + +public abstract class ComparatorNode extends BinaryExpressionNode { + public ComparatorNode(SourceSection sourceSection) { + super(sourceSection); + } + + public abstract boolean executeWith(Object left, Object right); +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/DivisionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/DivisionNode.java new file mode 100644 index 00000000..196f7ccd --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/DivisionNode.java @@ -0,0 +1,78 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; + +@NodeInfo(shortName = "/") +public abstract class DivisionNode extends BinaryExpressionNode { + protected DivisionNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected double eval(long left, long right) { + return (double) left / (double) right; + } + + @Specialization + protected double eval(long left, double right) { + return (double) left / right; + } + + @Specialization + protected double eval(double left, long right) { + return left / right; + } + + @Specialization + protected double eval(double left, double right) { + return left / right; + } + + @Specialization + protected VmDuration eval(VmDuration left, long right) { + return left.divide(right); + } + + @Specialization + protected VmDuration eval(VmDuration left, double right) { + return left.divide(right); + } + + @Specialization + protected double eval(VmDuration left, VmDuration right) { + return left.divide(right); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, long right) { + return left.divide(right); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, double right) { + return left.divide(right); + } + + @Specialization + protected double eval(VmDataSize left, VmDataSize right) { + return left.divide(right); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java new file mode 100644 index 00000000..cc42e0be --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/EqualNode.java @@ -0,0 +1,111 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +@NodeInfo(shortName = "==") +@NodeChild(value = "leftNode", type = ExpressionNode.class) +@NodeChild(value = "rightNode", type = ExpressionNode.class) +// not extending BinaryExpressionNode because we don't want the latter's fallback +public abstract class EqualNode extends ExpressionNode { + protected EqualNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected boolean eval(String left, String right) { + return left.equals(right); + } + + @Specialization + protected boolean eval(long left, long right) { + return left == right; + } + + @Specialization + protected boolean eval(long left, double right) { + return left == right; + } + + @Specialization + protected boolean eval(double left, long right) { + return left == right; + } + + @Specialization + protected boolean eval(double left, double right) { + return left == right; + } + + @Specialization + protected boolean eval(boolean left, boolean right) { + return left == right; + } + + /** + * This method effectively covers `VmValue left, VmValue right` but is implemented in a more + * efficient way. See: + * https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces + */ + @Specialization( + guards = {"left.getClass() == leftJavaClass", "right.getClass() == leftJavaClass"}, + limit = "99") + protected boolean eval( + Object left, + Object right, + @SuppressWarnings("unused") @Cached("getVmValueJavaClassOrNull(left)") + Class leftJavaClass) { + return equals(left, right); + } + + // TODO: Putting the equals call behind a boundary make the above optimization moot. + // Without the boundary, native-image 22.0 complains that Object.equals is reachable for + // runtime compilation, but with the above optimization, this isn't actually a problem. + @TruffleBoundary + private boolean equals(Object left, Object right) { + return left.equals(right); + } + + protected static @Nullable Class getVmValueJavaClassOrNull(Object value) { + // OK to perform slow cast here (not a guard) + return value instanceof VmValue ? ((VmValue) value).getClass() : null; + } + + // covers all remaining cases (else it's a bug) + @Specialization(guards = "isIncompatibleTypes(left, right)") + protected boolean eval( + @SuppressWarnings("unused") Object left, @SuppressWarnings("unused") Object right) { + return false; + } + + protected static boolean isIncompatibleTypes(Object left, Object right) { + var leftClass = left.getClass(); + var rightClass = right.getClass(); + + return leftClass == Long.class || leftClass == Double.class + ? rightClass != Long.class && rightClass != Double.class + : leftClass != rightClass; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ExponentiationNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ExponentiationNode.java new file mode 100644 index 00000000..719d7692 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ExponentiationNode.java @@ -0,0 +1,75 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; +import org.pkl.core.runtime.VmSafeMath; + +@NodeInfo(shortName = "**") +public abstract class ExponentiationNode extends BinaryExpressionNode { + public ExponentiationNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization(guards = "y >= 0") + protected long evalPositive(long x, long y) { + return VmSafeMath.pow(x, y); + } + + @Specialization(guards = "y < 0") + protected double evalNegative(long x, long y) { + return StrictMath.pow(x, y); + } + + @Specialization + protected double eval(long x, double y) { + return StrictMath.pow(x, y); + } + + @Specialization + protected double eval(double x, long y) { + return StrictMath.pow(x, y); + } + + @Specialization + protected double eval(double x, double y) { + return StrictMath.pow(x, y); + } + + @Specialization + protected VmDuration eval(VmDuration x, long y) { + return x.pow(y); + } + + @Specialization + protected VmDuration eval(VmDuration x, double y) { + return x.pow(y); + } + + @Specialization + protected VmDataSize eval(VmDataSize x, long y) { + return x.pow(y); + } + + @Specialization + protected VmDataSize eval(VmDataSize x, double y) { + return x.pow(y); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanNode.java new file mode 100644 index 00000000..8fd26f43 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanNode.java @@ -0,0 +1,66 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; + +@NodeInfo(shortName = ">") +public abstract class GreaterThanNode extends ComparatorNode { + protected GreaterThanNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + @TruffleBoundary + protected boolean eval(String left, String right) { + return left.compareTo(right) > 0; + } + + @Specialization + protected boolean eval(long left, long right) { + return left > right; + } + + @Specialization + protected boolean eval(long left, double right) { + return left > right; + } + + @Specialization + protected boolean eval(double left, long right) { + return left > right; + } + + @Specialization + protected boolean eval(double left, double right) { + return left > right; + } + + @Specialization + protected boolean eval(VmDuration left, VmDuration right) { + return left.compareTo(right) > 0; + } + + @Specialization + protected boolean eval(VmDataSize left, VmDataSize right) { + return left.compareTo(right) > 0; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanOrEqualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanOrEqualNode.java new file mode 100644 index 00000000..adf1162c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/GreaterThanOrEqualNode.java @@ -0,0 +1,66 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; + +@NodeInfo(shortName = ">=") +public abstract class GreaterThanOrEqualNode extends BinaryExpressionNode { + protected GreaterThanOrEqualNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + @TruffleBoundary + protected boolean eval(String left, String right) { + return left.compareTo(right) >= 0; + } + + @Specialization + protected boolean eval(long left, long right) { + return left >= right; + } + + @Specialization + protected boolean eval(long left, double right) { + return left >= right; + } + + @Specialization + protected boolean eval(double left, long right) { + return left >= right; + } + + @Specialization + protected boolean eval(double left, double right) { + return left >= right; + } + + @Specialization + protected boolean eval(VmDuration left, VmDuration right) { + return left.compareTo(right) >= 0; + } + + @Specialization + protected boolean eval(VmDataSize left, VmDataSize right) { + return left.compareTo(right) >= 0; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanNode.java new file mode 100644 index 00000000..fa25c6f7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanNode.java @@ -0,0 +1,66 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; + +@NodeInfo(shortName = "<") +public abstract class LessThanNode extends ComparatorNode { + protected LessThanNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + @TruffleBoundary + protected boolean eval(String left, String right) { + return left.compareTo(right) < 0; + } + + @Specialization + protected boolean eval(long left, long right) { + return left < right; + } + + @Specialization + protected boolean eval(long left, double right) { + return left < right; + } + + @Specialization + protected boolean eval(double left, long right) { + return left < right; + } + + @Specialization + protected boolean eval(double left, double right) { + return left < right; + } + + @Specialization + protected boolean eval(VmDuration left, VmDuration right) { + return left.compareTo(right) < 0; + } + + @Specialization + protected boolean eval(VmDataSize left, VmDataSize right) { + return left.compareTo(right) < 0; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanOrEqualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanOrEqualNode.java new file mode 100644 index 00000000..7b2cef9c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LessThanOrEqualNode.java @@ -0,0 +1,68 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; + +@NodeInfo(shortName = "<=") +public abstract class LessThanOrEqualNode extends BinaryExpressionNode { + protected LessThanOrEqualNode(SourceSection sourceSection) { + super(sourceSection); + } + + public abstract boolean executeWith(Object left, Object right); + + @Specialization + @TruffleBoundary + protected boolean eval(String left, String right) { + return left.compareTo(right) <= 0; + } + + @Specialization + protected boolean eval(long left, long right) { + return left <= right; + } + + @Specialization + protected boolean eval(long left, double right) { + return left <= right; + } + + @Specialization + protected boolean eval(double left, long right) { + return left <= right; + } + + @Specialization + protected boolean eval(double left, double right) { + return left <= right; + } + + @Specialization + protected boolean eval(VmDuration left, VmDuration right) { + return left.compareTo(right) <= 0; + } + + @Specialization + protected boolean eval(VmDataSize left, VmDataSize right) { + return left.compareTo(right) <= 0; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LetExprNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LetExprNode.java new file mode 100644 index 00000000..8b6dccce --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LetExprNode.java @@ -0,0 +1,76 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; +import org.pkl.core.ast.member.FunctionNode; +import org.pkl.core.ast.member.UnresolvedFunctionNode; +import org.pkl.core.runtime.VmFunction; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.LateInit; + +public final class LetExprNode extends ExpressionNode { + private @Child UnresolvedFunctionNode unresolvedFunctionNode; + private @Child ExpressionNode valueNode; + private final boolean isCustomThisScope; + + @CompilationFinal @LateInit private FunctionNode functionNode; + @Child @LateInit private DirectCallNode callNode; + @CompilationFinal private int customThisSlot = -1; + + public LetExprNode( + SourceSection sourceSection, + UnresolvedFunctionNode functionNode, + ExpressionNode valueNode, + boolean isCustomThisScope) { + + super(sourceSection); + this.unresolvedFunctionNode = functionNode; + this.valueNode = valueNode; + this.isCustomThisScope = isCustomThisScope; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (functionNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + functionNode = unresolvedFunctionNode.execute(frame); + callNode = insert(DirectCallNode.create(functionNode.getCallTarget())); + if (isCustomThisScope) { + // deferred until execution time s.t. nodes of inlined type aliases get the right frame slot + customThisSlot = VmUtils.findAuxiliarySlot(frame, CustomThisScope.FRAME_SLOT_ID); + } + } + + var function = + new VmFunction( + frame.materialize(), + isCustomThisScope ? frame.getAuxiliarySlot(customThisSlot) : VmUtils.getReceiver(frame), + 1, + functionNode, + null); + + var value = valueNode.executeGeneric(frame); + + return callNode.call(function.getThisValue(), function, value); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LogicalAndNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LogicalAndNode.java new file mode 100644 index 00000000..4b732da4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LogicalAndNode.java @@ -0,0 +1,39 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; + +@NodeInfo(shortName = "&&") +public abstract class LogicalAndNode extends ShortCircuitingExpressionNode { + protected LogicalAndNode(SourceSection sourceSection, ExpressionNode rightNode) { + super(sourceSection, rightNode); + } + + @Specialization + protected boolean eval(VirtualFrame frame, boolean left) { + try { + return left && rightNode.executeBoolean(frame); + } catch (UnexpectedResultException e) { + throw operatorNotDefined(true, e.getResult()); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LogicalOrNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LogicalOrNode.java new file mode 100644 index 00000000..b97280fb --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LogicalOrNode.java @@ -0,0 +1,39 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; + +@NodeInfo(shortName = "||") +public abstract class LogicalOrNode extends ShortCircuitingExpressionNode { + protected LogicalOrNode(SourceSection sourceSection, ExpressionNode rightNode) { + super(sourceSection, rightNode); + } + + @Specialization + protected boolean eval(VirtualFrame frame, boolean left) { + try { + return left || rightNode.executeBoolean(frame); + } catch (UnexpectedResultException e) { + throw operatorNotDefined(false, e.getResult()); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/MultiplicationNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/MultiplicationNode.java new file mode 100644 index 00000000..4b0da9e9 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/MultiplicationNode.java @@ -0,0 +1,94 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; + +@NodeInfo(shortName = "*") +public abstract class MultiplicationNode extends BinaryExpressionNode { + protected MultiplicationNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected long eval(long left, long right) { + try { + return StrictMath.multiplyExact(left, right); + } catch (VmException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("integerOverflow").build(); + } + } + + @Specialization + protected double eval(long left, double right) { + return left * right; + } + + @Specialization + protected double eval(double left, long right) { + return left * right; + } + + @Specialization + protected double eval(double left, double right) { + return left * right; + } + + @Specialization + protected VmDuration eval(VmDuration left, long right) { + return left.multiply(right); + } + + @Specialization + protected VmDuration eval(VmDuration left, double right) { + return left.multiply(right); + } + + @Specialization + protected VmDuration eval(long left, VmDuration right) { + return right.multiply(left); + } + + @Specialization + protected VmDuration eval(double left, VmDuration right) { + return right.multiply(left); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, long right) { + return left.multiply(right); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, double right) { + return left.multiply(right); + } + + @Specialization + protected VmDataSize eval(long left, VmDataSize right) { + return right.multiply(left); + } + + @Specialization + protected VmDataSize eval(double left, VmDataSize right) { + return right.multiply(left); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/NotEqualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/NotEqualNode.java new file mode 100644 index 00000000..62e693e4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/NotEqualNode.java @@ -0,0 +1,111 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmValue; +import org.pkl.core.util.Nullable; + +@NodeInfo(shortName = "!=") +@NodeChild(value = "leftNode", type = ExpressionNode.class) +@NodeChild(value = "rightNode", type = ExpressionNode.class) +// not extending BinaryExpressionNode because we don't want the latter's fallback +public abstract class NotEqualNode extends ExpressionNode { + protected NotEqualNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected boolean eval(String left, String right) { + return !left.equals(right); + } + + @Specialization + protected boolean eval(long left, long right) { + return left != right; + } + + @Specialization + protected boolean eval(long left, double right) { + return left != right; + } + + @Specialization + protected boolean eval(double left, long right) { + return left != right; + } + + @Specialization + protected boolean eval(double left, double right) { + return left != right; + } + + @Specialization + protected boolean eval(boolean left, boolean right) { + return left != right; + } + + /** + * This method effectively covers `VmValue left, VmValue right` but is implemented in a more + * efficient way. See: + * https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces + */ + @Specialization( + guards = {"left.getClass() == leftJavaClass", "right.getClass() == leftJavaClass"}, + limit = "99") + protected boolean eval( + Object left, + Object right, + @SuppressWarnings("unused") @Cached("getVmValueJavaClassOrNull(left)") + Class leftJavaClass) { + return !equals(left, right); + } + + // TODO: Putting the equals call behind a boundary make the above optimization moot. + // Without the boundary, native-image 22.0 complains that Object.equals is reachable for + // runtime compilation, but with the above optimization, this isn't actually a problem. + @TruffleBoundary + private boolean equals(Object left, Object right) { + return left.equals(right); + } + + protected static @Nullable Class getVmValueJavaClassOrNull(Object value) { + // OK to perform slow cast here (not a guard) + return value instanceof VmValue ? ((VmValue) value).getClass() : null; + } + + // covers all remaining cases (else it's a bug) + @Specialization(guards = "isIncompatibleTypes(left, right)") + protected boolean eval( + @SuppressWarnings("unused") Object left, @SuppressWarnings("unused") Object right) { + return true; + } + + protected static boolean isIncompatibleTypes(Object left, Object right) { + var leftClass = left.getClass(); + var rightClass = right.getClass(); + + return leftClass == Long.class || leftClass == Double.class + ? rightClass != Long.class && rightClass != Double.class + : leftClass != rightClass; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/NullCoalescingNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/NullCoalescingNode.java new file mode 100644 index 00000000..1c9c13dd --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/NullCoalescingNode.java @@ -0,0 +1,43 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmNull; + +@NodeInfo(shortName = "??") +public abstract class NullCoalescingNode extends ShortCircuitingExpressionNode { + protected NullCoalescingNode(SourceSection sourceSection, ExpressionNode rightNode) { + super(sourceSection, rightNode); + } + + @Specialization + @SuppressWarnings("UnusedParameters") + protected Object eval(VirtualFrame frame, VmNull left) { + return rightNode.executeGeneric(frame); + } + + @Fallback + @Override + protected Object fallback(Object left) { + return left; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/PipeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/PipeNode.java new file mode 100644 index 00000000..15b282c5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/PipeNode.java @@ -0,0 +1,36 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.lambda.ApplyVmFunction1Node; +import org.pkl.core.runtime.VmFunction; + +@NodeInfo(shortName = "|>") +public abstract class PipeNode extends BinaryExpressionNode { + @Child private ApplyVmFunction1Node applyFunctionNode = ApplyVmFunction1Node.create(); + + protected PipeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected Object eval(Object left, VmFunction right) { + return applyFunctionNode.execute(right, left); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/RemainderNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/RemainderNode.java new file mode 100644 index 00000000..c9b8f1cb --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/RemainderNode.java @@ -0,0 +1,71 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; +import org.pkl.core.runtime.VmSafeMath; + +@NodeInfo(shortName = "%") +@SuppressWarnings("SuspiciousNameCombination") +public abstract class RemainderNode extends BinaryExpressionNode { + protected RemainderNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected long eval(long left, long right) { + return VmSafeMath.remainder(left, right); + } + + @Specialization + protected double eval(long left, double right) { + return VmSafeMath.remainder(left, right); + } + + @Specialization + protected double eval(double left, long right) { + return VmSafeMath.remainder(left, right); + } + + @Specialization + protected double eval(double left, double right) { + return VmSafeMath.remainder(left, right); + } + + @Specialization + protected VmDuration eval(VmDuration left, long right) { + return left.remainder(right); + } + + @Specialization + protected VmDuration eval(VmDuration left, double right) { + return left.remainder(right); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, long right) { + return left.remainder(right); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, double right) { + return left.remainder(right); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ShortCircuitingExpressionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ShortCircuitingExpressionNode.java new file mode 100644 index 00000000..41912184 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/ShortCircuitingExpressionNode.java @@ -0,0 +1,64 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmException; +import org.pkl.core.runtime.VmUtils; + +/** + * A binary expression whose right operand may be short-circuited. Does not inherit from + * BinaryExpressionNode for technical reasons. + */ +@NodeChild(value = "leftNode", type = ExpressionNode.class) +public abstract class ShortCircuitingExpressionNode extends ExpressionNode { + @Child protected ExpressionNode rightNode; + + protected abstract ExpressionNode getLeftNode(); + + protected ShortCircuitingExpressionNode(SourceSection sourceSection, ExpressionNode rightNode) { + super(sourceSection); + this.rightNode = rightNode; + } + + @Fallback + @TruffleBoundary + protected Object fallback(Object left) { + throw operatorNotDefined(left); + } + + @TruffleBoundary + protected VmException operatorNotDefined(Object left) { + return exceptionBuilder() + .evalError("operatorNotDefinedLeft", getShortName(), VmUtils.getClass(left)) + .withProgramValue("Left operand", left) + .build(); + } + + @TruffleBoundary + protected VmException operatorNotDefined(Object left, Object right) { + return exceptionBuilder() + .evalError( + "operatorNotDefined2", getShortName(), VmUtils.getClass(left), VmUtils.getClass(right)) + .withProgramValue("Left operand", left) + .withProgramValue("Right operand", right) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java new file mode 100644 index 00000000..5bdf70f1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java @@ -0,0 +1,107 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; + +@NodeInfo(shortName = "[]") +public abstract class SubscriptNode extends BinaryExpressionNode { + protected SubscriptNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + @TruffleBoundary + protected String eval(String receiver, long index) { + var charIndex = VmUtils.codePointOffsetToCharOffset(receiver, index); + if (charIndex == -1 || charIndex == receiver.length()) { + throw exceptionBuilder() + .evalError( + "charIndexOutOfRange", index, 0, receiver.codePointCount(0, receiver.length()) - 1) + .withSourceSection(getRightNode().getSourceSection()) + .withProgramValue("String", receiver) + .build(); + } + + if (Character.isHighSurrogate(receiver.charAt(charIndex))) { + return receiver.substring(charIndex, charIndex + 2); + } + return receiver.substring(charIndex, charIndex + 1); + } + + @Specialization + protected Object eval(VmList receiver, long index) { + if (index < 0 || index >= receiver.getLength()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("elementIndexOutOfRange", index, 0, receiver.getLength() - 1) + .withProgramValue("Collection", receiver) + .build(); + } + return receiver.get(index); + } + + @Specialization + protected Object eval(VmMap receiver, Object key) { + var result = receiver.getOrNull(key); + if (result != null) return result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().cannotFindKey(receiver, key).build(); + } + + @Specialization + protected Object eval( + VmListing listing, long index, @Cached("create()") IndirectCallNode callNode) { + + var result = VmUtils.readMemberOrNull(listing, index, callNode); + if (result != null) return result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("elementIndexOutOfRange", index, 0, listing.getLength() - 1) + .build(); + } + + @Specialization + protected Object eval( + VmMapping mapping, Object key, @Cached("create()") IndirectCallNode callNode) { + + return readMember(mapping, key, callNode); + } + + @Specialization + protected Object eval( + VmDynamic dynamic, Object key, @Cached("create()") IndirectCallNode callNode) { + + return readMember(dynamic, key, callNode); + } + + private Object readMember(VmObject object, Object key, IndirectCallNode callNode) { + var result = VmUtils.readMemberOrNull(object, key, callNode); + if (result != null) return result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().cannotFindMember(object, key).build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubtractionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubtractionNode.java new file mode 100644 index 00000000..e18b869b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubtractionNode.java @@ -0,0 +1,64 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; + +@NodeInfo(shortName = "-") +public abstract class SubtractionNode extends BinaryExpressionNode { + protected SubtractionNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected long eval(long left, long right) { + try { + return StrictMath.subtractExact(left, right); + } catch (ArithmeticException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("integerOverflow").build(); + } + } + + @Specialization + protected double eval(long left, double right) { + return left - right; + } + + @Specialization + protected double eval(double left, long right) { + return left - right; + } + + @Specialization + protected double eval(double left, double right) { + return left - right; + } + + @Specialization + protected VmDuration eval(VmDuration left, VmDuration right) { + return left.subtract(right); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, VmDataSize right) { + return left.subtract(right); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/TruncatingDivisionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/TruncatingDivisionNode.java new file mode 100644 index 00000000..08ce96de --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/TruncatingDivisionNode.java @@ -0,0 +1,124 @@ +/** + * 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.ast.expression.binary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import java.math.RoundingMode; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.util.MathUtils; + +@NodeInfo(shortName = "~/") +@SuppressWarnings("SuspiciousNameCombination") +public abstract class TruncatingDivisionNode extends BinaryExpressionNode { + protected TruncatingDivisionNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected long eval(long left, long right) { + if (right == 0) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("divisionByZero").build(); + } + + var result = left / right; + + // use same check as com.oracle.truffle.sl.nodes.expression.SLDivNode + if ((left & right & result) < 0) { + CompilerDirectives.transferToInterpreter(); + assert left == Long.MIN_VALUE && right == -1; + throw exceptionBuilder().evalError("integerOverflow").build(); + } + return result; + } + + @Specialization + protected long eval(long left, double right) { + return doTruncatingDivide(left, right); + } + + @Specialization + protected long eval(double left, long right) { + return doTruncatingDivide(left, right); + } + + @Specialization + protected long eval(double left, double right) { + return doTruncatingDivide(left, right); + } + + @Specialization + protected VmDuration eval(VmDuration left, long right) { + var newValue = doTruncatingDivide(left.getValue(), right); + return new VmDuration(newValue, left.getUnit()); + } + + @Specialization + protected VmDuration eval(VmDuration left, double right) { + var newValue = doTruncatingDivide(left.getValue(), right); + return new VmDuration(newValue, left.getUnit()); + } + + @Specialization + protected long eval(VmDuration left, VmDuration right) { + // use same conversion strategy as add/subtract + if (left.getUnit().ordinal() <= right.getUnit().ordinal()) { + return doTruncatingDivide(left.getValue(right.getUnit()), right.getValue()); + } + return doTruncatingDivide(left.getValue(), right.getValue(left.getUnit())); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, long right) { + var value = doTruncatingDivide(left.getValue(), right); + return new VmDataSize(value, left.getUnit()); + } + + @Specialization + protected VmDataSize eval(VmDataSize left, double right) { + var newValue = doTruncatingDivide(left.getValue(), right); + return new VmDataSize(newValue, left.getUnit()); + } + + @Specialization + protected long eval(VmDataSize left, VmDataSize right) { + // use same conversion strategy as add/subtract + if (left.getUnit().ordinal() <= right.getUnit().ordinal()) { + var leftValue = left.convertTo(right.getUnit()).getValue(); + return doTruncatingDivide(leftValue, right.getValue()); + } + var rightValue = right.convertTo(left.getUnit()).getValue(); + return doTruncatingDivide(left.getValue(), rightValue); + } + + private long doTruncatingDivide(double x, double y) { + try { + return MathUtils.roundToLong(x / y, RoundingMode.DOWN); + } catch (ArithmeticException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError( + Double.isFinite(x) ? "cannotConvertLargeFloat" : "cannotConvertNonFiniteFloat", + new ProgramValue("Float", x)) + .build(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/package-info.java new file mode 100644 index 00000000..0b1079d8 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.expression.binary; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorElementNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorElementNode.java new file mode 100644 index 00000000..ee6fcae7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorElementNode.java @@ -0,0 +1,75 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmDynamic; +import org.pkl.core.runtime.VmListing; +import org.pkl.core.util.EconomicMaps; + +@ImportStatic(BaseModule.class) +public abstract class GeneratorElementNode extends GeneratorMemberNode { + private final ObjectMember element; + + protected GeneratorElementNode(ObjectMember element) { + super(element.getSourceSection()); + this.element = element; + } + + @Specialization + @SuppressWarnings("unused") + protected void evalDynamic(VmDynamic parent, ObjectData data) { + addElement(data); + } + + @Specialization + @SuppressWarnings("unused") + protected void evalListing(VmListing parent, ObjectData data) { + addElement(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "parent == getDynamicClass()") + protected void evalDynamicClass(VmClass parent, ObjectData data) { + addElement(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "parent == getListingClass()") + protected void evalListingClass(VmClass parent, ObjectData data) { + addElement(data); + } + + @Fallback + @SuppressWarnings("unused") + void fallback(Object parent, ObjectData data) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("objectCannotHaveElement", parent).build(); + } + + private void addElement(ObjectData data) { + long index = data.length; + EconomicMaps.put(data.members, index, element); + data.length += 1; + data.persistForBindings(index); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorEntryNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorEntryNode.java new file mode 100644 index 00000000..3e015545 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorEntryNode.java @@ -0,0 +1,122 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.util.EconomicMaps; + +@ImportStatic(BaseModule.class) +public abstract class GeneratorEntryNode extends GeneratorMemberNode { + @Child private ExpressionNode keyNode; + private final ObjectMember member; + + protected GeneratorEntryNode(ExpressionNode keyNode, ObjectMember member) { + super(member.getSourceSection()); + this.keyNode = keyNode; + this.member = member; + } + + @Specialization + @SuppressWarnings("unused") + protected void evalDynamic(VirtualFrame frame, VmDynamic parent, ObjectData data) { + addRegularEntry(frame, data); + } + + @Specialization + @SuppressWarnings("unused") + protected void evalMapping(VirtualFrame frame, VmMapping parent, ObjectData data) { + addRegularEntry(frame, data); + } + + @Specialization + @SuppressWarnings("unused") + protected void evalListing(VirtualFrame frame, VmListing parent, ObjectData data) { + addListingEntry(frame, data, parent.getLength()); + } + + @SuppressWarnings("unused") + @Specialization(guards = "parent == getDynamicClass()") + protected void evalDynamicClass(VirtualFrame frame, VmClass parent, ObjectData data) { + addRegularEntry(frame, data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "parent == getMappingClass()") + protected void evalMappingClass(VirtualFrame frame, VmClass parent, ObjectData data) { + addRegularEntry(frame, data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "parent == getListingClass()") + protected void evalListingClass(VirtualFrame frame, VmClass parent, ObjectData data) { + // always throws + addListingEntry(frame, data, 0); + } + + @Fallback + @SuppressWarnings("unused") + void fallback(Object parent, ObjectData data) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("objectCannotHaveEntry", parent).build(); + } + + private void addRegularEntry(VirtualFrame frame, ObjectData data) { + var key = keyNode.executeGeneric(frame); + doAdd(key, data); + } + + private void addListingEntry(VirtualFrame frame, ObjectData data, int parentLength) { + long index; + try { + index = keyNode.executeInt(frame); + } catch (UnexpectedResultException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("wrongListingKeyType", new ProgramValue("", VmUtils.getClass(e.getResult()))) + .withLocation(keyNode) + .build(); + } + + // use same error messages as in checkIsValidListingAmendment and checkMaxListingMemberIndex + if (index < 0 || index >= parentLength) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("elementIndexOutOfRange", index, 0, parentLength - 1) + .withLocation(keyNode) + .build(); + } + + doAdd(index, data); + } + + private void doAdd(Object key, ObjectData data) { + if (EconomicMaps.put(data.members, key, member) != null) { + CompilerDirectives.transferToInterpreter(); + throw duplicateDefinition(key, member); + } + + data.persistForBindings(key); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java new file mode 100644 index 00000000..2887a7db --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java @@ -0,0 +1,230 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import java.util.*; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.ast.type.VmTypeMismatchException; +import org.pkl.core.runtime.*; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; + +public abstract class GeneratorForNode extends GeneratorMemberNode { + private final int keySlot; + private final int valueSlot; + @Child private ExpressionNode iterableNode; + @Child private @Nullable UnresolvedTypeNode unresolvedKeyTypeNode; + @Child private @Nullable UnresolvedTypeNode unresolvedValueTypeNode; + @Children private final GeneratorMemberNode[] childNodes; + @Child private @Nullable TypeNode keyTypeNode; + @Child @LateInit private TypeNode valueTypeNode; + + public GeneratorForNode( + SourceSection sourceSection, + int keySlot, + int valueSlot, + ExpressionNode iterableNode, + @Nullable UnresolvedTypeNode unresolvedKeyTypeNode, + @Nullable UnresolvedTypeNode unresolvedValueTypeNode, + GeneratorMemberNode[] childNodes, + boolean hasKeyIdentifier, + boolean hasValueIdentifier) { + + super(sourceSection); + this.keySlot = keySlot; + this.valueSlot = valueSlot; + this.iterableNode = iterableNode; + this.unresolvedKeyTypeNode = unresolvedKeyTypeNode; + this.unresolvedValueTypeNode = unresolvedValueTypeNode; + this.childNodes = childNodes; + + // initialize now if possible to save later insert() + if (unresolvedKeyTypeNode == null && hasKeyIdentifier) { + keyTypeNode = + new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection()) + .initWriteSlotNode(keySlot); + } + if (unresolvedValueTypeNode == null && hasValueIdentifier) { + valueTypeNode = + new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection()) + .initWriteSlotNode(valueSlot); + } + } + + protected abstract void executeWithIterable( + VirtualFrame frame, Object parent, ObjectData data, Object iterable); + + @Override + public final void execute(VirtualFrame frame, Object parent, ObjectData data) { + executeWithIterable(frame, parent, data, iterableNode.executeGeneric(frame)); + } + + @Specialization + protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmListing iterable) { + doEvalObject(frame, iterable, parent, data); + } + + @Specialization + protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmMapping iterable) { + doEvalObject(frame, iterable, parent, data); + } + + @Specialization + protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmDynamic iterable) { + doEvalObject(frame, iterable, parent, data); + } + + @Specialization + protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmList iterable) { + initTypeNodes(frame); + long idx = 0; + for (Object element : iterable) { + executeIteration(frame, parent, data, idx++, element); + } + resetFrameSlots(frame); + } + + @Specialization + protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmMap iterable) { + initTypeNodes(frame); + for (var entry : iterable) { + executeIteration(frame, parent, data, VmUtils.getKey(entry), VmUtils.getValue(entry)); + } + resetFrameSlots(frame); + } + + @Specialization + protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmSet iterable) { + initTypeNodes(frame); + long idx = 0; + for (var element : iterable) { + executeIteration(frame, parent, data, idx++, element); + } + resetFrameSlots(frame); + } + + @Specialization + protected void eval(VirtualFrame frame, Object parent, ObjectData data, VmIntSeq iterable) { + initTypeNodes(frame); + var length = iterable.getLength(); + for (long key = 0, value = iterable.start; key < length; key++, value += iterable.step) { + executeIteration(frame, parent, data, key, value); + } + resetFrameSlots(frame); + } + + @Fallback + @SuppressWarnings("unused") + protected void fallback(VirtualFrame frame, Object parent, ObjectData data, Object iterable) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("cannotIterateOverThisValue", VmUtils.getClass(iterable)) + .withLocation(iterableNode) + .withProgramValue("Value", iterable) + .build(); + } + + @SuppressWarnings("ForLoopReplaceableByForEach") + private void doEvalObject(VirtualFrame frame, VmObject iterable, Object parent, ObjectData data) { + initTypeNodes(frame); + var members = evaluateMembers(iterable); + for (int i = 0; i < members.size(); i++) { + var member = members.get(i); + executeIteration(frame, parent, data, member.first, member.second); + } + resetFrameSlots(frame); + } + + private void resetFrameSlots(VirtualFrame frame) { + if (keySlot != -1) { + frame.clear(keySlot); + } + if (valueSlot != -1) { + frame.clear(valueSlot); + } + } + + private void initTypeNodes(VirtualFrame frame) { + if (unresolvedKeyTypeNode != null) { + CompilerDirectives.transferToInterpreter(); + keyTypeNode = insert(unresolvedKeyTypeNode.execute(frame)).initWriteSlotNode(keySlot); + unresolvedKeyTypeNode = null; + } + + if (unresolvedValueTypeNode != null) { + CompilerDirectives.transferToInterpreter(); + valueTypeNode = insert(unresolvedValueTypeNode.execute(frame)).initWriteSlotNode(valueSlot); + unresolvedValueTypeNode = null; + } + } + + /** + * Evaluate members upfront to make sure that `childNode.execute()` is not behind a Truffle + * boundary. + */ + @TruffleBoundary + private List> evaluateMembers(VmObject object) { + var members = new ArrayList>(); + object.forceAndIterateMemberValues( + (key, member, value) -> { + members.add(Pair.of(member.isProp() ? key.toString() : key, value)); + return true; + }); + return members; + } + + @ExplodeLoop + private void executeIteration( + VirtualFrame frame, Object parent, ObjectData data, Object key, Object value) { + + try { + if (keyTypeNode != null) { + keyTypeNode.executeAndSet(frame, key); + } + if (valueTypeNode != null) { + valueTypeNode.executeAndSet(frame, value); + } + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw e.toVmException(); + } + + Object[] prevBindings = null; + if (keyTypeNode != null && valueTypeNode != null) { + prevBindings = data.addForBinding(key, value); + } else if (valueTypeNode != null) { + prevBindings = data.addForBinding(value); + } else if (keyTypeNode != null) { + prevBindings = data.addForBinding(key); + } + + for (var childNode : childNodes) { + childNode.execute(frame, parent, data); + } + + data.resetForBindings(prevBindings); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorMemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorMemberNode.java new file mode 100644 index 00000000..bc0cd992 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorMemberNode.java @@ -0,0 +1,129 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.CompilerDirectives.ValueType; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import java.util.Arrays; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.ast.PklNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmException; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; + +public abstract class GeneratorMemberNode extends PklNode { + protected GeneratorMemberNode(SourceSection sourceSection) { + super(sourceSection); + } + + public abstract void execute(VirtualFrame frame, Object parent, ObjectData data); + + protected VmException duplicateDefinition(Object key, ObjectMember member) { + return exceptionBuilder() + .evalError( + "duplicateDefinition", key instanceof Identifier ? key : new ProgramValue("", key)) + .withSourceSection(member.getHeaderSection()) + .build(); + } + + protected static boolean isTypedObjectClass(VmClass clazz) { + assert clazz.isInstantiable(); + return !(clazz.isListingClass() || clazz.isMappingClass() || clazz.isDynamicClass()); + } + + protected boolean checkIsValidTypedProperty(VmClass clazz, ObjectMember member) { + if (member.isLocal() || clazz.hasProperty(member.getName())) return true; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .cannotFindProperty(clazz.getPrototype(), member.getName(), false, false) + .withSourceSection(member.getHeaderSection()) + .build(); + } + + /** + * + * x = new Mapping { for (i in IntSeq(1, 3)) for (key, value in Map(4, "Pigeon", 6, "Barn Owl")) [i * + * key] = value.reverse() } + * + * + *

The above code results in - 1 MemberNode for `value.reverse()` - 1 ObjectMember for `[i * + * key] = value.reverse()` - 1 ObjectData.members map with 6 identical ObjectMember values keyed + * by `i * key` - 1 ObjectData.forBindings map with 6 distinct arrays keyed by `i * key` Each + * array contains three elements, namely the current values for `i`, `key`, and `value`. - 1 + * VmMapping whose `members` field holds `ObjectData.members` and whose `extraStorage` field holds + * `ObjectData.forBindings`. - 3 `FrameSlot`s for `i`, `key`, and `value` + */ + @ValueType + static final class ObjectData { + // member count is exact iff every for/when body has exactly one member + ObjectData(int minMemberCount, int length) { + this.members = EconomicMaps.create(minMemberCount); + this.length = length; + } + + final EconomicMap members; + + // For-bindings keyed by object member key. + // (There is only one ObjectMember instance per lexical member definition, + // hence can't store a member's for-bindings there.) + final EconomicMap forBindings = EconomicMap.create(); + + int length; + + private Object @Nullable [] currentForBindings; + + @TruffleBoundary + Object @Nullable [] addForBinding(Object value) { + var result = currentForBindings; + if (currentForBindings == null) { + currentForBindings = new Object[] {value}; + } else { + currentForBindings = Arrays.copyOf(currentForBindings, currentForBindings.length + 1); + currentForBindings[currentForBindings.length - 1] = value; + } + return result; + } + + @TruffleBoundary + Object @Nullable [] addForBinding(Object key, Object value) { + var result = currentForBindings; + if (currentForBindings == null) { + currentForBindings = new Object[] {key, value}; + } else { + currentForBindings = Arrays.copyOf(currentForBindings, currentForBindings.length + 2); + currentForBindings[currentForBindings.length - 2] = key; + currentForBindings[currentForBindings.length - 1] = value; + } + return result; + } + + void persistForBindings(Object key) { + EconomicMaps.put(forBindings, key, currentForBindings); + } + + void resetForBindings(Object @Nullable [] bindings) { + currentForBindings = bindings; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorObjectLiteralNode.java new file mode 100644 index 00000000..385daa95 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorObjectLiteralNode.java @@ -0,0 +1,205 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.expression.generator.GeneratorMemberNode.ObjectData; +import org.pkl.core.ast.expression.literal.AmendFunctionNode; +import org.pkl.core.ast.expression.literal.ObjectLiteralNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +/** An object literal node that contains at least one for- or when-expression. */ +@ImportStatic(BaseModule.class) +public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode { + @Children private final GeneratorMemberNode[] memberNodes; + + public GeneratorObjectLiteralNode( + SourceSection sourceSection, + VmLanguage language, + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes, + GeneratorMemberNode[] memberNodes) { + + super( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + parametersDescriptor, + parameterTypes); + this.memberNodes = memberNodes; + } + + protected GeneratorObjectLiteralNode copy(ExpressionNode newParentNode) { + //noinspection ConstantConditions + return GeneratorObjectLiteralNodeGen.create( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + null, // copied node no longer has parameters + new UnresolvedTypeNode[0], // ditto + memberNodes, + newParentNode); + } + + @Specialization(guards = "checkObjectCannotHaveParameters()") + protected VmDynamic evalDynamic(VirtualFrame frame, VmDynamic parent) { + var data = createData(frame, parent, parent.getLength()); + var result = new VmDynamic(frame.materialize(), parent, data.members, data.length); + result.setExtraStorage(data.forBindings); + return result; + } + + @Specialization(guards = "checkObjectCannotHaveParameters()") + protected VmTyped evalTyped(VirtualFrame frame, VmTyped parent) { + VmUtils.checkIsInstantiable(parent.getVmClass(), getParentNode()); + var data = createData(frame, parent, 0); + assert data.forBindings.isEmpty(); + return new VmTyped(frame.materialize(), parent, parent.getVmClass(), data.members); + } + + @Specialization(guards = "checkListingCannotHaveParameters()") + protected VmListing evalListing(VirtualFrame frame, VmListing parent) { + var data = createData(frame, parent, parent.getLength()); + var result = new VmListing(frame.materialize(), parent, data.members, data.length); + result.setExtraStorage(data.forBindings); + return result; + } + + @Specialization(guards = "checkMappingCannotHaveParameters()") + protected VmMapping evalMapping(VirtualFrame frame, VmMapping parent) { + var data = createData(frame, parent, 0); + var result = new VmMapping(frame.materialize(), parent, data.members); + result.setExtraStorage(data.forBindings); + return result; + } + + @Specialization(guards = "checkObjectCannotHaveParameters()") + protected Object evalNull(VirtualFrame frame, VmNull parent) { + // assumes that Graal PE can handle recursive call to same node + return executeWithParent(frame, parent.getDefaultValue()); + } + + @Specialization(guards = "checkIsValidFunctionAmendment(parent)") + protected Object evalFunction( + VirtualFrame frame, + VmFunction parent, + @Cached("createAmendFunctionNode(frame)") AmendFunctionNode amendFunctionNode) { + + return amendFunctionNode.execute(frame, parent); + } + + @Specialization(guards = {"parent == getDynamicClass()", "checkObjectCannotHaveParameters()"}) + protected VmDynamic evalDynamicClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + var data = createData(frame, parent, 0); + var result = + new VmDynamic(frame.materialize(), parent.getPrototype(), data.members, data.length); + result.setExtraStorage(data.forBindings); + return result; + } + + @Specialization(guards = {"parent == getMappingClass()", "checkMappingCannotHaveParameters()"}) + protected VmMapping evalMappingClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + var data = createData(frame, parent, 0); + var result = new VmMapping(frame.materialize(), parent.getPrototype(), data.members); + result.setExtraStorage(data.forBindings); + return result; + } + + @Specialization(guards = {"parent == getListingClass()", "checkListingCannotHaveParameters()"}) + protected VmListing evalListingClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + var data = createData(frame, parent, 0); + var result = + new VmListing(frame.materialize(), parent.getPrototype(), data.members, data.length); + result.setExtraStorage(data.forBindings); + return result; + } + + @Specialization(guards = {"isTypedObjectClass(parent)", "checkObjectCannotHaveParameters()"}) + protected VmTyped evalTypedObjectClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + VmUtils.checkIsInstantiable(parent, getParentNode()); + var data = createData(frame, parent, 0); + assert data.forBindings.isEmpty(); + return new VmTyped(frame.materialize(), parent.getPrototype(), parent, data.members); + } + + @Fallback + @TruffleBoundary + protected void fallback(Object parent) { + VmUtils.checkIsInstantiable( + parent instanceof VmClass ? (VmClass) parent : VmUtils.getClass(parent), getParentNode()); + + throw exceptionBuilder().unreachableCode().build(); + } + + protected boolean checkObjectCannotHaveParameters() { + if (parametersDescriptor == null) return true; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("objectAmendmentCannotHaveParameters") + .withLocation(parameterTypes[0]) + .build(); + } + + protected boolean checkListingCannotHaveParameters() { + if (parametersDescriptor == null) return true; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("listingAmendmentCannotHaveParameters") + .withLocation(parameterTypes[0]) + .build(); + } + + protected boolean checkMappingCannotHaveParameters() { + if (parametersDescriptor == null) return true; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("mappingAmendmentCannotHaveParameters") + .withLocation(parameterTypes[0]) + .build(); + } + + @ExplodeLoop + private ObjectData createData(VirtualFrame frame, Object parent, int parentLength) { + var data = new ObjectData(memberNodes.length, parentLength); + for (GeneratorMemberNode memberNode : memberNodes) { + memberNode.execute(frame, parent, data); + } + return data; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPredicateMemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPredicateMemberNode.java new file mode 100644 index 00000000..5a3cb871 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPredicateMemberNode.java @@ -0,0 +1,146 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.*; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.EconomicSets; + +public abstract class GeneratorPredicateMemberNode extends GeneratorMemberNode { + @Child private ExpressionNode predicateNode; + private final ObjectMember member; + + @CompilationFinal private int customThisSlot = -1; + + protected GeneratorPredicateMemberNode(ExpressionNode predicateNode, ObjectMember member) { + super(member.getSourceSection()); + this.predicateNode = predicateNode; + this.member = member; + } + + @Specialization + @SuppressWarnings("unused") + protected void evalDynamic(VirtualFrame frame, VmDynamic parent, ObjectData data) { + addMembers(frame, parent, data); + } + + @Specialization + @SuppressWarnings("unused") + protected void evalMapping(VirtualFrame frame, VmMapping parent, ObjectData data) { + addMembers(frame, parent, data); + } + + @Specialization + @SuppressWarnings("unused") + protected void evalListing(VirtualFrame frame, VmListing parent, ObjectData data) { + addMembers(frame, parent, data); + } + + @Fallback + @SuppressWarnings("unused") + void fallback(Object parent, ObjectData data) { + if (parent == BaseModule.getDynamicClass() + || parent == BaseModule.getMappingClass() + || parent == BaseModule.getListingClass()) { + // nothing to do (parent is guaranteed to have zero elements/entries) + return; + } + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError( + "objectCannotHavePredicateMember", + parent instanceof VmClass ? parent : VmUtils.getClass(parent)) + .withLocation(predicateNode) + .build(); + } + + private void addMembers(VirtualFrame frame, VmObject parent, ObjectData data) { + initThisSlot(frame); + + var previousValue = frame.getAuxiliarySlot(customThisSlot); + var visitedKeys = EconomicSets.create(); + + // do our own traversal instead of relying on `VmAbstractObject.force/iterateMemberValues` + // (more efficent and we don't want to execute `predicateNode` behind Truffle boundary) + for (var owner = parent; owner != null; owner = owner.getParent()) { + var entries = EconomicMaps.getEntries(owner.getMembers()); + while (entries.advance()) { + var key = entries.getKey(); + if (!EconomicSets.add(visitedKeys, key)) continue; + + var member = entries.getValue(); + if (member.isProp() || member.isLocal()) continue; + + var value = owner.getCachedValue(key); + if (value == null) { + var constantValue = member.getConstantValue(); + if (constantValue != null) { + value = constantValue; + } else { + var callTarget = member.getCallTarget(); + value = callTarget.call(parent, owner, key); + } + owner.setCachedValue(key, value); + } + + frame.setAuxiliarySlot(customThisSlot, value); + + try { + var isApplicable = predicateNode.executeBoolean(frame); + if (isApplicable) doAdd(key, data); + } catch (UnexpectedResultException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .typeMismatch(e.getResult(), BaseModule.getBooleanClass()) + .withLocation(predicateNode) + .build(); + } + } + } + + // restore previous value + // handles the (pathetic) case of a predicate containing an object with another predicate + frame.setAuxiliarySlot(customThisSlot, previousValue); + } + + private void initThisSlot(VirtualFrame frame) { + if (customThisSlot == -1) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + // deferred until execution time s.t. nodes of inlined type aliases get the right frame slot + customThisSlot = + frame.getFrameDescriptor().findOrAddAuxiliarySlot(CustomThisScope.FRAME_SLOT_ID); + } + } + + private void doAdd(Object key, ObjectData data) { + if (EconomicMaps.put(data.members, key, member) != null) { + CompilerDirectives.transferToInterpreter(); + throw duplicateDefinition(key, member); + } + + data.persistForBindings(key); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPropertyNode.java new file mode 100644 index 00000000..4536ae14 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorPropertyNode.java @@ -0,0 +1,123 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.*; +import org.pkl.core.util.EconomicMaps; + +@ImportStatic({BaseModule.class, GeneratorObjectLiteralNode.class}) +public abstract class GeneratorPropertyNode extends GeneratorMemberNode { + protected final ObjectMember member; + + protected GeneratorPropertyNode(ObjectMember member) { + super(member.getSourceSection()); + this.member = member; + assert member.isProp(); + } + + @Specialization + @SuppressWarnings("unused") + protected void evalDynamic(VmDynamic parent, ObjectData data) { + addProperty(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "checkIsValidTypedProperty(parent.getVmClass(), member)") + protected void evalTyped(VmTyped parent, ObjectData data) { + addProperty(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "checkIsValidMappingProperty()") + protected void evalMapping(VmMapping parent, ObjectData data) { + addProperty(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "checkIsValidListingProperty()") + protected void evalListing(VmListing parent, ObjectData data) { + addProperty(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = "parent == getDynamicClass()") + protected void evalDynamicClass(VmClass parent, ObjectData data) { + addProperty(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = {"parent == getMappingClass()", "checkIsValidMappingProperty()"}) + protected void evalMappingClass(VmClass parent, ObjectData data) { + addProperty(data); + } + + @SuppressWarnings("unused") + @Specialization(guards = {"parent == getListingClass()", "checkIsValidListingProperty()"}) + protected void evalListingClass(VmClass parent, ObjectData data) { + addProperty(data); + } + + @SuppressWarnings("unused") + @Specialization( + guards = {"isTypedObjectClass(parent)", "checkIsValidTypedProperty(parent, member)"}) + protected void evalTypedObjectClass(VmClass parent, ObjectData data) { + addProperty(data); + } + + @Fallback + @SuppressWarnings("unused") + void fallback(Object parent, ObjectData data) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError( + "objectCannotHaveProperty", + parent instanceof VmClass ? parent : VmUtils.getClass(parent)) + .withSourceSection(member.getHeaderSection()) + .build(); + } + + protected boolean checkIsValidListingProperty() { + if (member.isLocal() || member.getName() == Identifier.DEFAULT) return true; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("objectCannotHaveProperty", BaseModule.getListingClass()) + .withSourceSection(member.getHeaderSection()) + .build(); + } + + protected boolean checkIsValidMappingProperty() { + if (member.isLocal() || member.getName() == Identifier.DEFAULT) return true; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("objectCannotHaveProperty", BaseModule.getMappingClass()) + .withSourceSection(member.getHeaderSection()) + .build(); + } + + private void addProperty(ObjectData data) { + if (EconomicMaps.put(data.members, member.getName(), member) == null) return; + + CompilerDirectives.transferToInterpreter(); + throw duplicateDefinition(member.getName(), member); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java new file mode 100644 index 00000000..bd102c22 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorSpreadNode.java @@ -0,0 +1,361 @@ +/** + * 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.ast.expression.generator; + +import static org.pkl.core.runtime.BaseModule.getListingClass; +import static org.pkl.core.runtime.BaseModule.getMappingClass; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmCollection; +import org.pkl.core.runtime.VmDynamic; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.runtime.VmIntSeq; +import org.pkl.core.runtime.VmListing; +import org.pkl.core.runtime.VmMap; +import org.pkl.core.runtime.VmMapping; +import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmObject; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.MutableLong; + +@ImportStatic(BaseModule.class) +public abstract class GeneratorSpreadNode extends GeneratorMemberNode { + @Child private ExpressionNode iterableNode; + private final boolean nullable; + + public GeneratorSpreadNode( + SourceSection sourceSection, ExpressionNode iterableNode, boolean nullable) { + super(sourceSection); + this.iterableNode = iterableNode; + this.nullable = nullable; + } + + protected abstract void executeWithIterable( + VirtualFrame frame, Object parent, ObjectData data, Object iterable); + + @Override + public final void execute(VirtualFrame frame, Object parent, ObjectData data) { + executeWithIterable(frame, parent, data, iterableNode.executeGeneric(frame)); + } + + @Specialization + @SuppressWarnings("unused") + protected void eval(VmObject parent, ObjectData data, VmNull iterable) { + if (nullable) { + return; + } + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("cannotIterateOverThisValue", BaseModule.getNullClass()) + .withLocation(iterableNode) + .withHint( + "To guard against a nullable value, use `...?` instead of `...`.\n" + + "Try: `...?" + + iterableNode.getSourceSection().getCharacters() + + "`") + .build(); + } + + @Specialization(guards = "!iterable.isTyped()") + @SuppressWarnings("unused") + protected void eval(VmDynamic parent, ObjectData data, VmObject iterable) { + doEvalDynamic(data, iterable); + } + + @Specialization(guards = "!iterable.isTyped()") + @SuppressWarnings("unused") + protected void eval(VmListing parent, ObjectData data, VmObject iterable) { + doEvalListing(data, iterable); + } + + @Specialization(guards = "!iterable.isTyped()") + @SuppressWarnings("unused") + protected void eval(VmMapping parent, ObjectData data, VmObject iterable) { + doEvalMapping(data, iterable); + } + + @Specialization(guards = {"parent == getDynamicClass()", "!iterable.isTyped()"}) + @SuppressWarnings("unused") + protected void evalDynamicClass(VmClass parent, ObjectData data, VmObject iterable) { + doEvalDynamic(data, iterable); + } + + @Specialization(guards = {"parent == getListingClass()", "!iterable.isTyped()"}) + @SuppressWarnings("unused") + protected void evalListingClass(VmClass parent, ObjectData data, VmObject iterable) { + doEvalListing(data, iterable); + } + + @Specialization(guards = {"parent == getMappingClass()", "!iterable.isTyped()"}) + @SuppressWarnings("unused") + protected void evalMappingClass(VmClass parent, ObjectData data, VmObject iterable) { + doEvalMapping(data, iterable); + } + + @Specialization(guards = {"isTypedObjectClass(parent)", "!iterable.isTyped()"}) + protected void evalTypedClass(VmClass parent, ObjectData data, VmObject iterable) { + doEvalTyped(parent, data, iterable); + } + + @Specialization(guards = {"!iterable.isTyped()"}) + protected void eval(VmTyped parent, ObjectData data, VmObject iterable) { + doEvalTyped(parent.getVmClass(), data, iterable); + } + + @Specialization + protected void eval(VmObject parent, ObjectData data, VmMap iterable) { + doEvalMap(parent.getVmClass(), data, iterable); + } + + @Specialization + protected void eval(VmClass parent, ObjectData data, VmMap iterable) { + doEvalMap(parent, data, iterable); + } + + @Specialization + protected void eval(VmObject parent, ObjectData data, VmCollection iterable) { + doEvalCollection(parent.getVmClass(), data, iterable); + } + + @Specialization + protected void eval(VmClass parent, ObjectData data, VmCollection iterable) { + doEvalCollection(parent, data, iterable); + } + + @Specialization + protected void eval(VmObject parent, ObjectData data, VmIntSeq iterable) { + doEvalIntSeq(parent.getVmClass(), data, iterable); + } + + @Specialization + protected void eval(VmClass parent, ObjectData data, VmIntSeq iterable) { + doEvalIntSeq(parent, data, iterable); + } + + @Fallback + @SuppressWarnings("unused") + protected void fallback(VirtualFrame frame, Object parent, ObjectData data, Object iterable) { + CompilerDirectives.transferToInterpreter(); + var builder = + exceptionBuilder() + .evalError("cannotIterateOverThisValue", VmUtils.getClass(iterable)) + .withLocation(iterableNode) + .withProgramValue("Value", iterable); + if (iterable instanceof VmObject && ((VmObject) iterable).isTyped()) { + builder.withHint( + "`Typed` values are not iterable. If you mean to spread its members, convert it to `Dynamic` using `toDynamic()`."); + } + throw builder.build(); + } + + protected void doEvalDynamic(ObjectData data, VmObject iterable) { + var length = new MutableLong(data.length); + iterable.forceAndIterateMemberValues( + (key, member, value) -> { + if (member.isElement()) { + EconomicMaps.put(data.members, length.getAndIncrement(), createMember(member, value)); + } else { + if (EconomicMaps.put(data.members, key, createMember(member, value)) != null) { + duplicateMember(key, member); + } + } + return true; + }); + data.length = (int) length.get(); + } + + private void doEvalMapping(ObjectData data, VmObject iterable) { + iterable.forceAndIterateMemberValues( + (key, member, value) -> { + if (member.isElement() || member.isProp()) { + cannotHaveMember(BaseModule.getMappingClass(), member); + } + if (EconomicMaps.put(data.members, key, createMember(member, value)) != null) { + duplicateMember(key, member); + } + return true; + }); + } + + private void doEvalListing(ObjectData data, VmObject iterable) { + var length = new MutableLong(data.length); + iterable.forceAndIterateMemberValues( + (key, member, value) -> { + if (member.isEntry() || member.isProp()) { + cannotHaveMember(getListingClass(), member); + } + EconomicMaps.put(data.members, length.getAndIncrement(), createMember(member, value)); + return true; + }); + data.length = (int) length.get(); + } + + private void doEvalTyped(VmClass clazz, ObjectData data, VmObject iterable) { + iterable.forceAndIterateMemberValues( + (key, member, value) -> { + if (member.isElement() || member.isEntry()) { + cannotHaveMember(clazz, member); + } + checkTypedProperty(clazz, member); + if (EconomicMaps.put(data.members, key, createMember(member, value)) != null) { + duplicateMember(key, member); + } + return true; + }); + } + + // handles both `List` and `Set` + private void doEvalCollection(VmClass parent, ObjectData data, VmCollection iterable) { + if (isTypedObjectClass(parent) || parent == getMappingClass()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("cannotSpreadObject", iterable.getVmClass(), parent) + .withHint( + "`List` and `Set` can only be spread into objects of type `Dynamic` and `Listing`.") + .withProgramValue("Value", iterable) + .build(); + } + spreadIterable(data, iterable); + } + + private void doEvalMap(VmClass parent, ObjectData data, VmMap iterable) { + if (isTypedObjectClass(parent) || parent == getListingClass()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("cannotSpreadObject", iterable.getVmClass(), parent) + .withHint("`Map` can only be spread into objects of type `Dynamic` and `Mapping`.") + .withProgramValue("Value", iterable) + .build(); + } + for (var entry : iterable) { + var member = VmUtils.createSyntheticObjectEntry("", VmUtils.getValue(entry)); + if (EconomicMaps.put(data.members, VmUtils.getKey(entry), member) != null) { + duplicateMember(VmUtils.getKey(entry), member); + } + } + } + + private void doEvalIntSeq(VmClass parent, ObjectData data, VmIntSeq iterable) { + if (isTypedObjectClass(parent) || parent == getMappingClass()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("cannotSpreadObject", iterable.getVmClass(), parent) + .withHint("`IntSeq` can only be spread into objects of type `Dynamic` and `Listing`.") + .withProgramValue("Value", iterable) + .build(); + } + spreadIterable(data, iterable); + } + + private void cannotHaveMember(VmClass clazz, ObjectMember member) { + CompilerDirectives.transferToInterpreter(); + var builder = exceptionBuilder(); + if (member.isEntry()) { + builder.evalError("objectCannotHaveSpreadEntry", clazz); + } else if (member.isElement()) { + builder.evalError("objectCannotHaveSpreadElement", clazz); + } else { + builder.evalError("objectCannotHaveSpreadProperty", clazz); + } + var exception = builder.build(); + // add the member's source section to the stack trace if it exists. + if (member.getHeaderSection().isAvailable()) { + exception + .getInsertedStackFrames() + .put( + getRootNode().getCallTarget(), + VmUtils.createStackFrame(member.getHeaderSection(), member.getQualifiedName())); + } + throw exception; + } + + private void duplicateMember(Object key, ObjectMember member) { + CompilerDirectives.transferToInterpreter(); + var exception = + exceptionBuilder() + .evalError( + member.isProp() ? "objectSpreadDuplicateProperty" : "objectSpreadDuplicateEntry", + key instanceof Identifier ? key : new ProgramValue("", key)) + .build(); + if (member.getHeaderSection().isAvailable()) { + exception + .getInsertedStackFrames() + .put( + getRootNode().getCallTarget(), + VmUtils.createStackFrame(member.getHeaderSection(), member.getQualifiedName())); + } + throw exception; + } + + private ObjectMember createMember(ObjectMember prototype, Object value) { + // If there is a constant value that is equal to the target value, we can use as-is. + // Otherwise, initialize a new member and initialize a constant value for it + // (effectively cached). + if (prototype.getConstantValue() == value) { + return prototype; + } + var result = + new ObjectMember( + prototype.getSourceSection(), + prototype.getHeaderSection(), + prototype.getModifiers(), + prototype.getNameOrNull(), + prototype.getQualifiedName()); + result.initConstantValue(value); + return result; + } + + @TruffleBoundary + private void spreadIterable(ObjectData data, Iterable iterable) { + var length = data.length; + for (var elem : iterable) { + var index = length++; + var member = VmUtils.createSyntheticObjectElement(String.valueOf(index), elem); + EconomicMaps.put(data.members, (long) index, member); + } + data.length = length; + } + + protected void checkTypedProperty(VmClass clazz, ObjectMember member) { + if (member.isLocal() || clazz.hasProperty(member.getName())) return; + CompilerDirectives.transferToInterpreter(); + var exception = + exceptionBuilder() + .cannotFindProperty(clazz.getPrototype(), member.getName(), false, false) + .build(); + if (member.getHeaderSection().isAvailable()) { + exception + .getInsertedStackFrames() + .put( + getRootNode().getCallTarget(), + VmUtils.createStackFrame(member.getHeaderSection(), member.getQualifiedName())); + } + throw exception; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorWhenNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorWhenNode.java new file mode 100644 index 00000000..8d4aa29e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorWhenNode.java @@ -0,0 +1,60 @@ +/** + * 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.ast.expression.generator; + +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.UnexpectedResultException; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.BaseModule; + +public final class GeneratorWhenNode extends GeneratorMemberNode { + @Child private ExpressionNode conditionNode; + @Children private final GeneratorMemberNode[] thenNodes; + @Children private final GeneratorMemberNode[] elseNodes; + + public GeneratorWhenNode( + SourceSection sourceSection, + ExpressionNode conditionNode, + GeneratorMemberNode[] thenNodes, + GeneratorMemberNode[] elseNodes) { + + super(sourceSection); + this.conditionNode = conditionNode; + this.thenNodes = thenNodes; + this.elseNodes = elseNodes; + } + + @Override + @ExplodeLoop + public void execute(VirtualFrame frame, Object parent, ObjectData data) { + boolean condition; + try { + condition = conditionNode.executeBoolean(frame); + } catch (UnexpectedResultException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .typeMismatch(e.getResult(), BaseModule.getBooleanClass()) + .withSourceSection(conditionNode.getSourceSection()) + .build(); + } + for (var node : condition ? thenNodes : elseNodes) { + node.execute(frame, parent, data); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/WriteForVariablesNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/WriteForVariablesNode.java new file mode 100644 index 00000000..6d1174db --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/WriteForVariablesNode.java @@ -0,0 +1,52 @@ +/** + * 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.ast.expression.generator; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.EconomicMaps; + +public final class WriteForVariablesNode extends ExpressionNode { + private final int[] auxiliarySlots; + @Child private ExpressionNode childNode; + + public WriteForVariablesNode(int[] auxiliarySlots, ExpressionNode childNode) { + this.auxiliarySlots = auxiliarySlots; + this.childNode = childNode; + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + var extraStorage = VmUtils.getOwner(frame).getExtraStorage(); + assert extraStorage instanceof UnmodifiableEconomicMap; + + @SuppressWarnings("unchecked") + var forBindings = (UnmodifiableEconomicMap) extraStorage; + var bindings = EconomicMaps.get(forBindings, VmUtils.getMemberKey(frame)); + assert bindings != null; + assert bindings.length == auxiliarySlots.length; + + for (var i = 0; i < auxiliarySlots.length; i++) { + frame.setAuxiliarySlot(auxiliarySlots[i], bindings[i]); + } + + return childNode.executeGeneric(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/package-info.java new file mode 100644 index 00000000..2e07dcad --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.expression.generator; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendFunctionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendFunctionNode.java new file mode 100644 index 00000000..8546dbdc --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendFunctionNode.java @@ -0,0 +1,215 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.ValueType; +import com.oracle.truffle.api.frame.*; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.ast.PklRootNode; +import org.pkl.core.ast.SimpleRootNode; +import org.pkl.core.ast.builder.SymbolTable; +import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; +import org.pkl.core.ast.frame.ReadFrameSlotNodeGen; +import org.pkl.core.ast.member.FunctionNode; +import org.pkl.core.ast.member.Lambda; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class AmendFunctionNode extends PklNode { + private final boolean isCustomThisScope; + private final PklRootNode initialFunctionRootNode; + @CompilationFinal private int customThisSlot = -1; + + public AmendFunctionNode( + ObjectLiteralNode hostNode, + TypeNode[] parameterTypeNodes, + FrameDescriptor hostFrameDesecriptor) { + super(hostNode.getSourceSection()); + + isCustomThisScope = hostNode.isCustomThisScope; + + var builder = FrameDescriptor.newBuilder(); + var hostDescriptor = hostNode.parametersDescriptor; + int[] parameterSlots; + if (hostDescriptor != null) { + parameterSlots = new int[hostDescriptor.getNumberOfSlots()]; + for (int i = 0; i < hostDescriptor.getNumberOfSlots(); i++) { + var slotKind = hostDescriptor.getSlotKind(i); + var slot = builder.addSlot(slotKind, hostDescriptor.getSlotName(i), null); + parameterSlots[i] = slot; + } + } else { + parameterSlots = new int[0]; + } + for (var i = 0; i < hostFrameDesecriptor.getNumberOfSlots(); i++) { + var slotInfo = hostFrameDesecriptor.getSlotInfo(i); + // Copy for-generator variables from the outer frame descriptor into inner lambda. + // + // We need to do this because at parse time within AstBuilder, we inject for-generator + // variables into the frame descriptor of the containing root node. + // The expectation is that when GeneratorForNode executes, it writes for-generator variables + // into these slots. + // + // In the case of an amend function node, AstBuilder can't determine out that there is another + // frame (e.g. with `new Mixin { ... }` syntax), so it injects for-generator vars into the + // wrong frame. + // + // As a remedy, we simply copy outer for-generator variables into this frame. + if (slotInfo != null && slotInfo.equals(SymbolTable.FOR_GENERATOR_VARIABLE)) { + builder.addSlot( + hostFrameDesecriptor.getSlotKind(i), hostFrameDesecriptor.getSlotName(i), null); + } + } + var objectToAmendSlot = builder.addSlot(FrameSlotKind.Object, new Object(), null); + var frameDescriptor = builder.build(); + + var subsequentFunctionRootNode = + new SimpleRootNode( + hostNode.language, + frameDescriptor, + sourceSection, + hostNode.qualifiedScopeName + ".", + new AmendFunctionBodyNode( + sourceSection, + hostNode.copy( + ReadFrameSlotNodeGen.create( + hostNode.getParentNode().getSourceSection(), objectToAmendSlot)), + parameterSlots, + objectToAmendSlot, + null)); + + if (parameterSlots.length > 0) { + var parameterCount = hostNode.parameterTypes.length; + initialFunctionRootNode = + new FunctionNode( + hostNode.language, + frameDescriptor, + new Lambda(sourceSection, hostNode.qualifiedScopeName + "."), + parameterCount, + parameterTypeNodes, + null, + true, + new AmendFunctionBodyNode( + sourceSection, + hostNode.copy( + ReadFrameSlotNodeGen.create( + hostNode.getParentNode().getSourceSection(), objectToAmendSlot)), + parameterSlots, + objectToAmendSlot, + subsequentFunctionRootNode)); + } else { + initialFunctionRootNode = subsequentFunctionRootNode; + } + } + + public VmFunction execute(VirtualFrame frame, VmFunction functionToAmend) { + if (isCustomThisScope && customThisSlot == -1) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + customThisSlot = VmUtils.findAuxiliarySlot(frame, CustomThisScope.FRAME_SLOT_ID); + } + return new VmFunction( + frame.materialize(), + isCustomThisScope ? frame.getAuxiliarySlot(customThisSlot) : VmUtils.getReceiver(frame), + functionToAmend.getParameterCount(), + initialFunctionRootNode, + new Context(functionToAmend, null)); + } + + private static class AmendFunctionBodyNode extends ExpressionNode { + @Child private ExpressionNode amendObjectNode; + private final int[] parameterSlots; + private final int valueToAmendSlot; + private final @Nullable PklRootNode nextFunctionRootNode; + + @Child private IndirectCallNode callNode = IndirectCallNode.create(); + + public AmendFunctionBodyNode( + SourceSection sourceSection, + ExpressionNode amendObjectNode, + int[] parameterSlots, + int valueToAmendSlot, + @Nullable PklRootNode nextFunctionRootNode) { + + super(sourceSection); + this.amendObjectNode = amendObjectNode; + this.parameterSlots = parameterSlots; + this.valueToAmendSlot = valueToAmendSlot; + this.nextFunctionRootNode = nextFunctionRootNode; + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + var frameArguments = frame.getArguments(); + var currentFunction = (VmFunction) VmUtils.getOwner(frame); + var context = (Context) currentFunction.getExtraStorage(); + if (nextFunctionRootNode != null) { + context = context.setFrame(frame.materialize()); + } + var functionToAmend = context.function; + + var arguments = new Object[frameArguments.length]; + arguments[0] = functionToAmend.getThisValue(); + arguments[1] = functionToAmend; + System.arraycopy(frameArguments, 2, arguments, 2, frameArguments.length - 2); + + var valueToAmend = callNode.call(functionToAmend.getCallTarget(), arguments); + if (!(valueToAmend instanceof VmFunction)) { + var materializedFrame = context.frame; + if (materializedFrame != null) { + for (var slot : parameterSlots) { + // could use WriteFrameSlotNode and Read(Other)FrameSlotNode to specialize + frame.setObject(slot, materializedFrame.getValue(slot)); + } + } + frame.setObject(valueToAmendSlot, valueToAmend); + return amendObjectNode.executeGeneric(frame); + } + + var newFunctionToAmend = (VmFunction) valueToAmend; + return currentFunction.copy( + newFunctionToAmend.getParameterCount(), + nextFunctionRootNode, + context.setFunction(newFunctionToAmend)); + } + } + + @ValueType + private static class Context { + public final VmFunction function; + public final @Nullable MaterializedFrame frame; + + public Context(VmFunction function, @Nullable MaterializedFrame frame) { + this.function = function; + this.frame = frame; + } + + public Context setFunction(VmFunction newFunction) { + return new Context(newFunction, frame); + } + + public Context setFrame(MaterializedFrame newFrame) { + return new Context(function, newFrame); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java new file mode 100644 index 00000000..8d3aed7f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java @@ -0,0 +1,77 @@ +/** + * 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.ast.expression.literal; + +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.source.SourceSection; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.ModuleInfo; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; + +public abstract class AmendModuleNode extends SpecializedObjectLiteralNode { + @Children private final ExpressionNode[] annotationNodes; + private final ModuleInfo moduleInfo; + + public AmendModuleNode( + SourceSection sourceSection, + VmLanguage language, + ExpressionNode[] annotationNodes, + EconomicMap properties, + ModuleInfo moduleInfo) { + + super(sourceSection, language, "", false, null, new UnresolvedTypeNode[0], properties); + this.annotationNodes = annotationNodes; + this.moduleInfo = moduleInfo; + } + + @Override + @TruffleBoundary + protected AmendModuleNode copy(ExpressionNode newParentNode) { + throw exceptionBuilder().unreachableCode().build(); + } + + @Specialization + protected VmTyped eval(VirtualFrame frame, VmTyped supermodule) { + // receiver is empty module object + var module = VmUtils.getTypedObjectReceiver(frame); + + if (module == supermodule) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("moduleCannotAmendSelf", moduleInfo.getModuleName()) + .build(); + } + + checkIsValidTypedAmendment(supermodule); + + module.lateInitVmClass(supermodule.getVmClass()); + module.lateInitParent(supermodule); + module.addProperties(members); + + module.setExtraStorage(moduleInfo); + moduleInfo.initAnnotations(VmUtils.evaluateAnnotations(frame, annotationNodes)); + + return module; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/CheckIsAnnotationClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/CheckIsAnnotationClassNode.java new file mode 100644 index 00000000..5a8e27e6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/CheckIsAnnotationClassNode.java @@ -0,0 +1,51 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class CheckIsAnnotationClassNode extends ExpressionNode { + @Child private UnresolvedTypeNode unresolvedTypeNode; + @Child private @Nullable TypeNode typeNode; + + public CheckIsAnnotationClassNode(UnresolvedTypeNode unresolvedTypeNode) { + super(unresolvedTypeNode.getSourceSection()); + this.unresolvedTypeNode = unresolvedTypeNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (typeNode == null) { + // invalidation is done by insert() + CompilerDirectives.transferToInterpreter(); + typeNode = insert(unresolvedTypeNode.execute(frame)); + unresolvedTypeNode = null; + } + var clazz = typeNode.getVmClass(); + if (clazz != null && clazz.isSubclassOf(BaseModule.getAnnotationClass())) { + return typeNode.getVmClass(); + } + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("expectedAnnotationClass").build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java new file mode 100644 index 00000000..ec4b052d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ConstantEntriesLiteralNode.java @@ -0,0 +1,131 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.*; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +/** + * Object literal that contains entries (and possibly properties) but not elements. Additionally, + * all entry keys are constants. Example: `new foo { ["one"] = 1 }` + */ +// IDEA: don't materialize frames if all members have constant values +@ImportStatic(BaseModule.class) +public abstract class ConstantEntriesLiteralNode extends SpecializedObjectLiteralNode { + public ConstantEntriesLiteralNode( + SourceSection sourceSection, + VmLanguage language, + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes, + UnmodifiableEconomicMap members) { + + super( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + parametersDescriptor, + parameterTypes, + members); + } + + @Override + public ConstantEntriesLiteralNode copy(ExpressionNode newParentNode) { + //noinspection ConstantConditions + return ConstantEntriesLiteralNodeGen.create( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + null, // copied node no longer has parameters + new UnresolvedTypeNode[0], // ditto + members, + newParentNode); + } + + @Specialization(guards = "checkIsValidMappingAmendment()") + protected VmMapping evalMapping(VirtualFrame frame, VmMapping parent) { + return new VmMapping(frame.materialize(), parent, members); + } + + @Specialization + protected VmDynamic evalDynamic(VirtualFrame frame, VmDynamic parent) { + return new VmDynamic(frame.materialize(), parent, members, parent.getLength()); + } + + @Specialization(guards = "checkIsValidListingAmendment()") + protected VmListing evalListing(VirtualFrame frame, VmListing parent) { + checkMaxListingMemberIndex(parent.getLength()); + return new VmListing(frame.materialize(), parent, members, parent.getLength()); + } + + @Specialization + protected Object evalNull(VirtualFrame frame, VmNull parent) { + // assumes that Graal PE can handle recursive call to same node + return executeWithParent(frame, parent.getDefaultValue()); + } + + @Specialization(guards = "checkIsValidFunctionAmendment(parent)") + protected VmFunction evalFunction( + VirtualFrame frame, + VmFunction parent, + @Cached("createAmendFunctionNode(frame)") AmendFunctionNode amendFunctionNode) { + + return amendFunctionNode.execute(frame, parent); + } + + @Specialization(guards = {"parent == getMappingClass()", "checkIsValidMappingAmendment()"}) + protected VmMapping evalMappingClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + return new VmMapping(frame.materialize(), BaseModule.getMappingClass().getPrototype(), members); + } + + @Specialization(guards = "parent == getDynamicClass()") + protected VmDynamic evalDynamicClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + return new VmDynamic( + frame.materialize(), BaseModule.getDynamicClass().getPrototype(), members, 0); + } + + @Specialization( + guards = { + "parent == getListingClass()", + "checkIsValidListingAmendment()", + "checkMaxListingMemberIndex(0)" + }) + protected void evalListingClass(@SuppressWarnings("unused") VmClass parent) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().unreachableCode().build(); + } + + @Fallback + @TruffleBoundary + protected void fallback(Object parent) { + elementsEntriesFallback(parent, findFirstNonProperty(members), false); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java new file mode 100644 index 00000000..5b467257 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java @@ -0,0 +1,167 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; + +/** + * Object literal that contains both elements and entries (and possibly properties). Example: `new + * foo { "pigeon", [3] = "barn owl" }` + */ +@ImportStatic(BaseModule.class) +public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLiteralNode { + private final ObjectMember[] elements; + @Children private final ExpressionNode[] keyNodes; + private final ObjectMember[] values; + + public ElementsEntriesLiteralNode( + SourceSection sourceSection, + VmLanguage language, + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes, + UnmodifiableEconomicMap properties, + ObjectMember[] elements, + ExpressionNode[] keyNodes, + ObjectMember[] values) { + + super( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + parametersDescriptor, + parameterTypes, + properties); + this.elements = elements; + this.keyNodes = keyNodes; + this.values = values; + + assert elements.length > 0; + } + + @Override + protected ElementsEntriesLiteralNode copy(ExpressionNode newParentNode) { + //noinspection ConstantConditions + return ElementsEntriesLiteralNodeGen.create( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + null, // copied node no longer has parameters + new UnresolvedTypeNode[0], // ditto + members, + elements, + keyNodes, + values, + newParentNode); + } + + @Specialization(guards = "checkIsValidListingAmendment()") + protected VmListing evalListing(VirtualFrame frame, VmListing parent) { + return new VmListing( + frame.materialize(), + parent, + createMembers(frame, parent.getLength()), + parent.getLength() + elements.length); + } + + @Specialization + protected VmDynamic evalDynamic(VirtualFrame frame, VmDynamic parent) { + return new VmDynamic( + frame.materialize(), + parent, + createMembers(frame, parent.getLength()), + parent.getLength() + elements.length); + } + + @Specialization + protected Object evalNull(VirtualFrame frame, VmNull parent) { + // assumes that Graal PE can handle recursive call to same node + return executeWithParent(frame, parent.getDefaultValue()); + } + + @Specialization(guards = "checkIsValidFunctionAmendment(parent)") + protected VmFunction evalFunction( + VirtualFrame frame, + VmFunction parent, + @Cached("createAmendFunctionNode(frame)") AmendFunctionNode amendFunctionNode) { + + return amendFunctionNode.execute(frame, parent); + } + + @Specialization(guards = {"parent == getListingClass()", "checkIsValidListingAmendment()"}) + protected VmListing evalListingClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + + return new VmListing( + frame.materialize(), + BaseModule.getListingClass().getPrototype(), + createMembers(frame, 0), + elements.length); + } + + @Specialization(guards = "parent == getDynamicClass()") + protected VmDynamic evalDynamicClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + + return new VmDynamic( + frame.materialize(), + BaseModule.getDynamicClass().getPrototype(), + createMembers(frame, 0), + elements.length); + } + + @Fallback + @TruffleBoundary + protected void fallback(Object parent) { + elementsEntriesFallback(parent, elements[0], true); + } + + @ExplodeLoop + protected UnmodifiableEconomicMap createMembers( + VirtualFrame frame, int parentLength) { + var result = + EconomicMaps.create( + EconomicMaps.size(members) + keyNodes.length + elements.length); + + EconomicMaps.putAll(result, members); + + addListEntries(frame, parentLength, result, keyNodes, values); + + for (var i = 0; i < elements.length; i++) { + EconomicMaps.put(result, (long) (parentLength + i), elements[i]); + } + + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsLiteralNode.java new file mode 100644 index 00000000..235ea70e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsLiteralNode.java @@ -0,0 +1,178 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.*; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; + +/** + * Object literal that contains elements (and possibly properties) but not entries. Example: `new + * foo { "pigeon" }` + */ +@ImportStatic(BaseModule.class) +public abstract class ElementsLiteralNode extends SpecializedObjectLiteralNode { + private final ObjectMember[] elements; + + public ElementsLiteralNode( + SourceSection sourceSection, + VmLanguage language, + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes, + UnmodifiableEconomicMap properties, + ObjectMember[] elements) { + + super( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + parametersDescriptor, + parameterTypes, + properties); + this.elements = elements; + + assert elements.length > 0; + } + + @Override + protected ElementsLiteralNode copy(ExpressionNode newParentNode) { + //noinspection ConstantConditions + return ElementsLiteralNodeGen.create( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + null, // copied node no longer has parameters + new UnresolvedTypeNode[0], // ditto + members, + elements, + newParentNode); + } + + @Specialization(guards = "parent.getLength() == parentLength") + protected VmDynamic evalDynamicCached( + VirtualFrame frame, + VmDynamic parent, + @Cached("parent.getLength()") int parentLength, + @Cached("createMembers(parentLength)") + UnmodifiableEconomicMap members) { + + return new VmDynamic(frame.materialize(), parent, members, parentLength + elements.length); + } + + @Specialization + protected VmDynamic evalDynamicUncached(VirtualFrame frame, VmDynamic parent) { + return new VmDynamic( + frame.materialize(), + parent, + createMembers(parent.getLength()), + parent.getLength() + elements.length); + } + + @Specialization + protected Object evalNull(VirtualFrame frame, VmNull parent) { + // assumes that Graal PE can handle recursive call to same node + return executeWithParent(frame, parent.getDefaultValue()); + } + + @Specialization(guards = "checkIsValidFunctionAmendment(parent)") + protected VmFunction evalFunction( + VirtualFrame frame, + VmFunction parent, + @Cached("createAmendFunctionNode(frame)") AmendFunctionNode amendFunctionNode) { + + return amendFunctionNode.execute(frame, parent); + } + + @Specialization( + guards = { + "parent == getListingClass()", + "checkIsValidListingAmendment()", + "checkMaxListingMemberIndex(0)" + }) + protected VmListing evalListingClass( + VirtualFrame frame, + @SuppressWarnings("unused") VmClass parent, + @Cached("createMembers(0)") UnmodifiableEconomicMap members) { + + return new VmListing( + frame.materialize(), BaseModule.getListingClass().getPrototype(), members, elements.length); + } + + @Specialization(guards = "parent == getDynamicClass()") + protected VmDynamic evalDynamicClass( + VirtualFrame frame, + @SuppressWarnings("unused") VmClass parent, + @Cached("createMembers(0)") UnmodifiableEconomicMap members) { + return new VmDynamic( + frame.materialize(), BaseModule.getDynamicClass().getPrototype(), members, elements.length); + } + + @Specialization( + guards = { + "checkIsValidListingAmendment()", + "parent.getLength() == parentLength", + "checkMaxListingMemberIndex(parentLength)" + }) + protected VmListing evalListingCached( + VirtualFrame frame, + VmListing parent, + @Cached("parent.getLength()") int parentLength, + @Cached("createMembers(parentLength)") + UnmodifiableEconomicMap properties) { + + return new VmListing(frame.materialize(), parent, properties, parentLength + elements.length); + } + + @Specialization(guards = "checkIsValidListingAmendment()") + protected VmListing evalListingUncached(VirtualFrame frame, VmListing parent) { + checkMaxListingMemberIndex(parent.getLength()); + return new VmListing( + frame.materialize(), + parent, + createMembers(parent.getLength()), + parent.getLength() + elements.length); + } + + @Fallback + @TruffleBoundary + protected void fallback(Object parent) { + elementsEntriesFallback(parent, elements[0], true); + } + + // offset element keys according to parentLength + protected UnmodifiableEconomicMap createMembers(int parentLength) { + var result = + EconomicMaps.create(EconomicMaps.size(members) + elements.length); + EconomicMaps.putAll(result, members); + for (var i = 0; i < elements.length; i++) { + EconomicMaps.put(result, (long) (parentLength + i), elements[i]); + } + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EmptyObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EmptyObjectLiteralNode.java new file mode 100644 index 00000000..0fed0201 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EmptyObjectLiteralNode.java @@ -0,0 +1,72 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; + +/** Object literal node with empty body. Example: RHS of `x = foo {}` */ +@ImportStatic(BaseModule.class) +@NodeChild(value = "parentNode", type = ExpressionNode.class) +public abstract class EmptyObjectLiteralNode extends ExpressionNode { + protected EmptyObjectLiteralNode(SourceSection sourceSection) { + super(sourceSection); + } + + protected abstract ExpressionNode getParentNode(); + + @Specialization + protected Object eval(VmClass parent) { + if (parent.isListingClass()) { + return VmListing.empty(); + } + + if (parent.isMappingClass()) { + return VmMapping.empty(); + } + + if (parent.isDynamicClass()) { + return VmDynamic.empty(); + } + + VmUtils.checkIsInstantiable(parent, getParentNode()); + return parent.getPrototype(); + } + + @Specialization + protected Object eval(VmObjectLike parent) { + return parent; + } + + @Specialization + protected Object eval(VmNull parent) { + return parent.getDefaultValue(); + } + + @Fallback + @TruffleBoundary + protected void fallback(Object parent) { + assert !(parent instanceof VmClass); + VmUtils.checkIsInstantiable(VmUtils.getClass(parent), getParentNode()); + throw exceptionBuilder().unreachableCode().build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java new file mode 100644 index 00000000..b5ea7219 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java @@ -0,0 +1,186 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.*; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; + +/** + * Object literal that contains entries (and possibly properties) but not elements. Additionally, at + * least one entry key is not a constant. (If all keys are constants, ConstantEntriesLiteralNode is + * used.) Example: `foo { ["on" + "e"] = 1 }` + */ +// IDEA: don't materialize frames if all members have constant values +@ImportStatic(BaseModule.class) +public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode { + @Children private final ExpressionNode[] keyNodes; + private final ObjectMember[] values; + + public EntriesLiteralNode( + SourceSection sourceSection, + VmLanguage language, + // contains local properties and default property (if present) + // does *not* contain entries with constant keys to maintain definition order of entries + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes, + UnmodifiableEconomicMap members, + ExpressionNode[] keyNodes, + ObjectMember[] values) { + + super( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + parametersDescriptor, + parameterTypes, + members); + this.keyNodes = keyNodes; + this.values = values; + + assert keyNodes.length > 0; + assert keyNodes.length == values.length; + } + + @Override + public EntriesLiteralNode copy(ExpressionNode newParentNode) { + //noinspection ConstantConditions + return EntriesLiteralNodeGen.create( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + null, // copied node no longer has parameters + new UnresolvedTypeNode[0], // ditto + members, + keyNodes, + values, + newParentNode); + } + + @Specialization(guards = "checkIsValidMappingAmendment()") + protected VmMapping evalMapping(VirtualFrame frame, VmMapping parent) { + return new VmMapping(frame.materialize(), parent, createMapMembers(frame)); + } + + @Specialization + protected VmDynamic evalDynamic(VirtualFrame frame, VmDynamic parent) { + return new VmDynamic(frame.materialize(), parent, createMapMembers(frame), parent.getLength()); + } + + @Specialization(guards = "checkIsValidListingAmendment()") + protected VmListing evalListing(VirtualFrame frame, VmListing parent) { + return new VmListing( + frame.materialize(), + parent, + createListMembers(frame, parent.getLength()), + parent.getLength() + keyNodes.length); + } + + @Specialization + protected Object evalNull(VirtualFrame frame, VmNull parent) { + // assumes that Graal PE can handle recursive call to same node + return executeWithParent(frame, parent.getDefaultValue()); + } + + @Specialization(guards = "checkIsValidFunctionAmendment(parent)") + protected VmFunction evalFunction( + VirtualFrame frame, + VmFunction parent, + @Cached("createAmendFunctionNode(frame)") AmendFunctionNode amendFunctionNode) { + + return amendFunctionNode.execute(frame, parent); + } + + @Specialization(guards = {"parent == getMappingClass()", "checkIsValidMappingAmendment()"}) + protected VmMapping evalMappingClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + return new VmMapping( + frame.materialize(), BaseModule.getMappingClass().getPrototype(), createMapMembers(frame)); + } + + @Specialization(guards = "parent == getDynamicClass()") + protected VmDynamic evalDynamicClass( + VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) { + return new VmDynamic( + frame.materialize(), + BaseModule.getDynamicClass().getPrototype(), + createMapMembers(frame), + 0); + } + + @Specialization(guards = {"parent == getListingClass()", "checkIsValidListingAmendment()"}) + protected void evalListingClass(@SuppressWarnings("unused") VmClass parent) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("cannotAddElementWithEntrySyntax") + .withSourceSection(keyNodes[0].getSourceSection()) + .build(); + } + + @Fallback + @TruffleBoundary + protected void fallback(Object parent) { + elementsEntriesFallback(parent, values[0], false); + } + + @ExplodeLoop + protected EconomicMap createMapMembers(VirtualFrame frame) { + var result = + EconomicMaps.create(EconomicMaps.size(members) + keyNodes.length); + EconomicMaps.putAll(result, members); + + for (var i = 0; i < keyNodes.length; i++) { + var key = keyNodes[i].executeGeneric(frame); + var value = values[i]; + var previousValue = EconomicMaps.put(result, key, value); + if (previousValue != null) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("duplicateDefinition", new ProgramValue("", key)) + .withSourceSection(value.getHeaderSection()) + .build(); + } + } + + return result; + } + + protected UnmodifiableEconomicMap createListMembers( + VirtualFrame frame, int parentLength) { + var result = + EconomicMaps.create(EconomicMaps.size(members) + keyNodes.length); + EconomicMaps.putAll(result, members); + addListEntries(frame, parentLength, result, keyNodes, values); + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FalseLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FalseLiteralNode.java new file mode 100644 index 00000000..0d38077f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FalseLiteralNode.java @@ -0,0 +1,44 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ConstantNode; +import org.pkl.core.ast.ExpressionNode; + +@NodeInfo(shortName = "false") +public final class FalseLiteralNode extends ExpressionNode implements ConstantNode { + public FalseLiteralNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public Boolean executeGeneric(VirtualFrame frame) { + return Boolean.FALSE; + } + + @Override + public boolean executeBoolean(VirtualFrame frame) { + return false; + } + + @Override + public Boolean getValue() { + return Boolean.FALSE; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FloatLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FloatLiteralNode.java new file mode 100644 index 00000000..2f420a4d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FloatLiteralNode.java @@ -0,0 +1,47 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ConstantNode; +import org.pkl.core.ast.ExpressionNode; + +@NodeInfo(shortName = "float") +public final class FloatLiteralNode extends ExpressionNode implements ConstantNode { + private final double value; + + public FloatLiteralNode(SourceSection sourceSection, double value) { + super(sourceSection); + this.value = value; + } + + @Override + public Double executeGeneric(VirtualFrame frame) { + return value; + } + + @Override + public double executeFloat(VirtualFrame frame) { + return value; + } + + @Override + public Double getValue() { + return value; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FunctionLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FunctionLiteralNode.java new file mode 100644 index 00000000..451a4878 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/FunctionLiteralNode.java @@ -0,0 +1,62 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; +import org.pkl.core.ast.member.FunctionNode; +import org.pkl.core.ast.member.UnresolvedFunctionNode; +import org.pkl.core.runtime.VmFunction; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; + +public final class FunctionLiteralNode extends ExpressionNode { + private @Child UnresolvedFunctionNode unresolvedFunctionNode; + private final boolean isCustomThisScope; + + @CompilationFinal private @Nullable FunctionNode functionNode; + @CompilationFinal private int customThisSlot = -1; + + public FunctionLiteralNode( + SourceSection sourceSection, UnresolvedFunctionNode functionNode, boolean isCustomThisScope) { + + super(sourceSection); + this.unresolvedFunctionNode = functionNode; + this.isCustomThisScope = isCustomThisScope; + } + + @Override + public VmFunction executeGeneric(VirtualFrame frame) { + if (functionNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + functionNode = unresolvedFunctionNode.execute(frame); + if (isCustomThisScope) { + customThisSlot = VmUtils.findAuxiliarySlot(frame, CustomThisScope.FRAME_SLOT_ID); + } + } + + return new VmFunction( + frame.materialize(), + isCustomThisScope ? frame.getAuxiliarySlot(customThisSlot) : VmUtils.getReceiver(frame), + functionNode.getParameterCount(), + functionNode, + null); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/IntLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/IntLiteralNode.java new file mode 100644 index 00000000..9a598d22 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/IntLiteralNode.java @@ -0,0 +1,47 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ConstantNode; +import org.pkl.core.ast.ExpressionNode; + +@NodeInfo(shortName = "int") +public final class IntLiteralNode extends ExpressionNode implements ConstantNode { + private final long value; + + public IntLiteralNode(SourceSection sourceSection, long value) { + super(sourceSection); + this.value = value; + } + + @Override + public Long executeGeneric(VirtualFrame frame) { + return value; + } + + @Override + public long executeInt(VirtualFrame frame) { + return value; + } + + @Override + public Long getValue() { + return value; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/InterpolatedStringLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/InterpolatedStringLiteralNode.java new file mode 100644 index 00000000..0e102f4b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/InterpolatedStringLiteralNode.java @@ -0,0 +1,43 @@ +/** + * 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.ast.expression.literal; + +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.runtime.VmUtils; + +@NodeInfo(shortName = "istr") +public final class InterpolatedStringLiteralNode extends ExpressionNode { + @Children private final ExpressionNode[] parts; + + public InterpolatedStringLiteralNode(SourceSection sourceSection, ExpressionNode[] parts) { + super(sourceSection); + this.parts = parts; + } + + @Override + @ExplodeLoop + public String executeGeneric(VirtualFrame frame) { + var builder = VmUtils.createBuilder(); + for (var part : parts) { + VmUtils.appendToBuilder(builder, (String) part.executeGeneric(frame)); + } + return VmUtils.builderToString(builder); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ListLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ListLiteralNode.java new file mode 100644 index 00000000..1cf21f27 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ListLiteralNode.java @@ -0,0 +1,44 @@ +/** + * 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.ast.expression.literal; + +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.runtime.VmList; + +@NodeInfo(shortName = "List()") +public final class ListLiteralNode extends ExpressionNode { + @Children private final ExpressionNode[] elements; + + public ListLiteralNode(SourceSection sourceSection, ExpressionNode[] elements) { + super(sourceSection); + this.elements = elements; + } + + @Override + @ExplodeLoop + public VmList executeGeneric(VirtualFrame frame) { + var builder = VmList.EMPTY.builder(); + for (var element : elements) { + builder.add(element.executeGeneric(frame)); + } + + return builder.build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/MapLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/MapLiteralNode.java new file mode 100644 index 00000000..6566a5f0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/MapLiteralNode.java @@ -0,0 +1,48 @@ +/** + * 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.ast.expression.literal; + +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.runtime.VmMap; + +@NodeInfo(shortName = "Map()") +public final class MapLiteralNode extends ExpressionNode { + @Children private final ExpressionNode[] keysAndValues; + + public MapLiteralNode(SourceSection sourceSection, ExpressionNode[] keysAndValues) { + super(sourceSection); + this.keysAndValues = keysAndValues; + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + assert keysAndValues.length % 2 == 0; + + var builder = VmMap.builder(); + for (var i = 0; i < keysAndValues.length; i += 2) { + var key = keysAndValues[i].executeGeneric(frame); + var value = keysAndValues[i + 1].executeGeneric(frame); + builder.add(key, value); + } + + return builder.build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java new file mode 100644 index 00000000..ae67ed12 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ObjectLiteralNode.java @@ -0,0 +1,86 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +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.UnresolvedTypeNode; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmFunction; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; + +// IDEA: don't materialize frames when all members are constants +@NodeChild(value = "parentNode", type = ExpressionNode.class) +public abstract class ObjectLiteralNode extends ExpressionNode { + protected final VmLanguage language; + protected final String qualifiedScopeName; + protected final boolean isCustomThisScope; + protected final @Nullable FrameDescriptor parametersDescriptor; + @Children protected final UnresolvedTypeNode[] parameterTypes; + + public ObjectLiteralNode( + SourceSection sourceSection, + VmLanguage language, + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes) { + + super(sourceSection); + this.language = language; + this.qualifiedScopeName = qualifiedScopeName; + this.isCustomThisScope = isCustomThisScope; + this.parametersDescriptor = parametersDescriptor; + this.parameterTypes = parameterTypes; + } + + protected abstract ExpressionNode getParentNode(); + + protected abstract Object executeWithParent(VirtualFrame frame, Object parent); + + protected abstract ObjectLiteralNode copy(ExpressionNode newParentNode); + + protected final AmendFunctionNode createAmendFunctionNode(VirtualFrame frame) { + var resolvedParameterTypes = + parametersDescriptor == null + ? new TypeNode[0] + : VmUtils.resolveParameterTypes(frame, parametersDescriptor, parameterTypes); + return new AmendFunctionNode(this, resolvedParameterTypes, frame.getFrameDescriptor()); + } + + protected static boolean isTypedObjectClass(VmClass clazz) { + return !(clazz.isListingClass() || clazz.isMappingClass() || clazz.isDynamicClass()); + } + + protected final boolean checkIsValidFunctionAmendment(VmFunction parent) { + var length = parameterTypes.length; + if (length > 0 && length != parent.getParameterCount()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("wrongFunctionAmendmentParameterCount", length, parent.getParameterCount()) + .withSourceSection(getParentNode().getSourceSection()) + .build(); + } + return true; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/PropertiesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/PropertiesLiteralNode.java new file mode 100644 index 00000000..1b2c209d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/PropertiesLiteralNode.java @@ -0,0 +1,206 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +/** Object literal that contains properties but not elements or entries. */ +// IDEA: don't materialize frame when all members are constants +public abstract class PropertiesLiteralNode extends SpecializedObjectLiteralNode { + public PropertiesLiteralNode( + SourceSection sourceSection, + VmLanguage language, + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes, + UnmodifiableEconomicMap properties) { + + super( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + parametersDescriptor, + parameterTypes, + properties); + } + + @Override + public PropertiesLiteralNode copy(ExpressionNode newParentNode) { + //noinspection ConstantConditions + return PropertiesLiteralNodeGen.create( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + null, // copied node no longer has parameters + new UnresolvedTypeNode[0], // ditto + members, + newParentNode); + } + + @Specialization( + guards = {"parentClass == parent.getVmClass()", "checkIsValidTypedAmendment(parentClass)"}) + protected Object evalTypedObjectCached( + VirtualFrame frame, VmTyped parent, @Cached("parent.getVmClass()") VmClass parentClass) { + + assert isTypedObjectClass(parentClass); + + return new VmTyped(frame.materialize(), parent, parentClass, members); + } + + @Specialization(guards = {"checkIsValidTypedAmendment(parent)"}) + protected Object evalTypedObjectUncached(VirtualFrame frame, VmTyped parent) { + assert isTypedObjectClass(parent.getVmClass()); + + return new VmTyped(frame.materialize(), parent, parent.getVmClass(), members); + } + + @Specialization + protected Object evalDynamic(VirtualFrame frame, VmDynamic parent) { + return new VmDynamic(frame.materialize(), parent, members, parent.getLength()); + } + + @Specialization(guards = "checkIsValidListingAmendment()") + protected Object evalListing(VirtualFrame frame, VmListing parent) { + return new VmListing(frame.materialize(), parent, members, parent.getLength()); + } + + @ExplodeLoop + @Specialization(guards = "checkIsValidMappingAmendment()") + protected Object evalMapping(VirtualFrame frame, VmMapping parent) { + return new VmMapping(frame.materialize(), parent, members); + } + + @Specialization + protected Object evalNull(VirtualFrame frame, VmNull parent) { + // assumes that Graal PE can handle recursive call to same node + return executeWithParent(frame, parent.getDefaultValue()); + } + + // Ultimately, this lambda or a lambda returned from it will call one of the other + // specializations, + // which will perform the "isValidXYZ" guard check. + // That said, to flag non-sensical amendments early, this specialization could have a guard to + // check + // that this amendment is at least one of a valid listing, mapping, or object amendment (modulo + // parameters). + @Specialization(guards = "checkIsValidFunctionAmendment(parent)") + protected VmFunction evalFunction( + VirtualFrame frame, + VmFunction parent, + @Cached("createAmendFunctionNode(frame)") AmendFunctionNode amendFunctionNode) { + + return amendFunctionNode.execute(frame, parent); + } + + @Specialization( + guards = { + "parent == cachedParent", + "isTypedObjectClass(cachedParent)", + "checkIsValidTypedAmendment(cachedParent)" + }) + protected VmTyped evalTypedObjectClassCached( + VirtualFrame frame, + VmClass parent, + @Cached("parent") @SuppressWarnings("unused") VmClass cachedParent) { + return new VmTyped(frame.materialize(), parent.getPrototype(), parent, members); + } + + @Specialization( + guards = { + "parent == cachedParent", + "cachedParent.isListingClass()", + "checkIsValidListingAmendment()" + }) + protected VmListing evalListingClass( + VirtualFrame frame, + @SuppressWarnings("unused") VmClass parent, + @Cached("parent") @SuppressWarnings("unused") VmClass cachedParent) { + + return new VmListing( + frame.materialize(), BaseModule.getListingClass().getPrototype(), members, 0); + } + + @Specialization( + guards = { + "parent == cachedParent", + "cachedParent.isMappingClass()", + "checkIsValidMappingAmendment()" + }) + protected VmMapping evalMappingClass( + VirtualFrame frame, + @SuppressWarnings("unused") VmClass parent, + @Cached("parent") @SuppressWarnings("unused") VmClass cachedParent) { + + return new VmMapping(frame.materialize(), BaseModule.getMappingClass().getPrototype(), members); + } + + @Specialization(guards = {"parent == cachedParent", "cachedParent.isDynamicClass()"}) + protected VmDynamic evalDynamicClass( + VirtualFrame frame, + @SuppressWarnings("unused") VmClass parent, + @Cached("parent") @SuppressWarnings("unused") VmClass cachedParent) { + + return new VmDynamic( + frame.materialize(), BaseModule.getDynamicClass().getPrototype(), members, 0); + } + + // slow but very unlikely to occur in practice + @Specialization + protected Object evalClassUncached(VirtualFrame frame, VmClass parent) { + if (parent.isListingClass()) { + checkIsValidListingAmendment(); + checkMaxListingMemberIndex(0); + return evalListingClass(frame, parent, parent); + } + + if (parent.isMappingClass()) { + checkIsValidMappingAmendment(); + return evalMappingClass(frame, parent, parent); + } + + if (parent.isDynamicClass()) { + return new VmDynamic( + frame.materialize(), BaseModule.getDynamicClass().getPrototype(), members, 0); + } + + checkIsValidTypedAmendment(parent); + return new VmTyped(frame.materialize(), parent.getPrototype(), parent, members); + } + + @Specialization + @TruffleBoundary + protected void fallback(Object parent) { + // should always throw + checkIsValidTypedAmendment(parent); + + throw exceptionBuilder().unreachableCode().build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SetLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SetLiteralNode.java new file mode 100644 index 00000000..cf4cfc56 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SetLiteralNode.java @@ -0,0 +1,44 @@ +/** + * 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.ast.expression.literal; + +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.runtime.VmSet; + +@NodeInfo(shortName = "Set()") +public final class SetLiteralNode extends ExpressionNode { + @Children private final ExpressionNode[] elements; + + public SetLiteralNode(SourceSection sourceSection, ExpressionNode[] elements) { + super(sourceSection); + this.elements = elements; + } + + @Override + @ExplodeLoop + public VmSet executeGeneric(VirtualFrame frame) { + var builder = VmSet.EMPTY.builder(); + for (var element : elements) { + builder.add(element.executeGeneric(frame)); + } + + return builder.build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java new file mode 100644 index 00000000..3577c14c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java @@ -0,0 +1,309 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.source.SourceSection; +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; + +/** + * Base class for object literal nodes specialized for a certain mix of property/entry/element + * definitions. The motivation for this specialization is to do as much object construction and + * validation work as possible only once or a few times (via `@Cached`) per object literal, instead + * of repeating it for every object instantiation. + */ +public abstract class SpecializedObjectLiteralNode extends ObjectLiteralNode { + public SpecializedObjectLiteralNode( + SourceSection sourceSection, + VmLanguage language, + String qualifiedScopeName, + boolean isCustomThisScope, + @Nullable FrameDescriptor parametersDescriptor, + UnresolvedTypeNode[] parameterTypes, + UnmodifiableEconomicMap members) { + + super( + sourceSection, + language, + qualifiedScopeName, + isCustomThisScope, + parametersDescriptor, + parameterTypes); + this.members = members; + } + + protected final UnmodifiableEconomicMap members; + + @CompilationFinal protected long maxListingMemberIndex = Long.MIN_VALUE; + @CompilationFinal private boolean checkedIsValidMappingAmendment; + + // only runs once per VmClass (which often means once per PropertiesLiteralNode) + // unless an XYZUncached specialization is active + @TruffleBoundary + protected boolean checkIsValidTypedAmendment(Object parent) { + var parentClass = parent instanceof VmClass ? (VmClass) parent : VmUtils.getClass(parent); + VmUtils.checkIsInstantiable(parentClass, getParentNode()); + + for (var member : EconomicMaps.getValues(members)) { + if (member.isLocal()) continue; + + var memberName = member.getName(); + if (!parentClass.hasProperty(memberName)) { + throw exceptionBuilder() + .cannotFindProperty(parentClass.getPrototype(), memberName, false, false) + .withSourceSection(member.getHeaderSection()) + .build(); + } + var classProperty = parentClass.getProperty(memberName); + if (classProperty != null && classProperty.isConstOrFixed()) { + // tailor error message based on whether an amends declaration is used or not + // (i.e. `friends {}` vs. `friends = new {}`) + // an amends declaration's body section includes the header section, whereas normal property + // assignment's body section is the section after the equal sign. + var isAmendsDeclaration = + member.getHeaderSection().getCharIndex() == member.getBodySection().getCharIndex(); + var errMsg = isAmendsDeclaration ? "cannotAmendFixedProperty" : "cannotAssignFixedProperty"; + if (classProperty.isConst()) { + errMsg = isAmendsDeclaration ? "cannotAmendConstProperty" : "cannotAssignConstProperty"; + } + throw exceptionBuilder() + .evalError(errMsg, memberName) + .withSourceSection(member.getHeaderSection()) + .build(); + } + } + + if (parametersDescriptor != null) { + throw exceptionBuilder() + .evalError("objectAmendmentCannotHaveParameters") + .withLocation(getParentNode()) + .build(); + } + + return true; + } + + @TruffleBoundary + protected final boolean checkIsValidListingAmendment() { + if (maxListingMemberIndex != Long.MIN_VALUE) return true; + + var cursor = EconomicMaps.getEntries(members); + long maxIndex = -1; + + while (cursor.advance()) { + var member = cursor.getValue(); + if (member.isLocal()) continue; + + var memberName = member.getNameOrNull(); + if (memberName == null) { + var key = cursor.getKey(); + + if (!(key instanceof Long)) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("wrongListingKeyType", new ProgramValue("", VmUtils.getClass(key))) + .withSourceSection(member.getHeaderSection()) + .build(); + } + + long index = (long) key; + if (index < 0) { + // defer handling of negative index to checkMaxListingMemberIndex() (gives more uniform + // error messages) + maxIndex = Long.MAX_VALUE; + break; + } else if (index > maxIndex) { + maxIndex = index; + } + } else if (memberName != Identifier.DEFAULT) { + throw exceptionBuilder() + .evalError("objectCannotHaveProperty", BaseModule.getListingClass()) + .withSourceSection(member.getHeaderSection()) + .build(); + } + } + + if (parametersDescriptor != null) { + throw exceptionBuilder() + .evalError("listingAmendmentCannotHaveParameters") + .withSourceSection(getParentNode().getSourceSection()) + .build(); + } + + maxListingMemberIndex = maxIndex; + return true; + } + + @TruffleBoundary + protected final boolean checkIsValidMappingAmendment() { + if (checkedIsValidMappingAmendment) return true; + + for (var member : EconomicMaps.getValues(members)) { + if (member.isLocal()) continue; + + var memberName = member.getNameOrNull(); + if (memberName == null) continue; + + if (memberName != Identifier.DEFAULT) { + throw exceptionBuilder() + .evalError("objectCannotHaveProperty", BaseModule.getMappingClass()) + .withSourceSection(member.getHeaderSection()) + .build(); + } + } + + if (parametersDescriptor != null) { + throw exceptionBuilder() + .evalError("mappingAmendmentCannotHaveParameters") + .withSourceSection(getParentNode().getSourceSection()) + .build(); + } + + checkedIsValidMappingAmendment = true; + return true; + } + + protected final boolean checkMaxListingMemberIndex(int parentLength) { + assert maxListingMemberIndex != Long.MIN_VALUE; + if (maxListingMemberIndex < parentLength) return true; + + CompilerDirectives.transferToInterpreter(); + var cursor = EconomicMaps.getEntries(members); + while (cursor.advance()) { + var key = cursor.getKey(); + if (!(key instanceof Long)) continue; + + var index = (long) key; + if (index < 0 || index >= parentLength) { + throw exceptionBuilder() + .evalError("elementIndexOutOfRange", index, 0, parentLength - 1) + .withSourceSection(cursor.getValue().getHeaderSection()) + .build(); + } + } + + throw exceptionBuilder().unreachableCode().build(); + } + + @ExplodeLoop + protected void addListEntries( + VirtualFrame frame, + int parentLength, + EconomicMap result, + ExpressionNode[] keyNodes, + ObjectMember[] values) { + + for (var i = 0; i < keyNodes.length; i++) { + long index; + try { + index = keyNodes[i].executeInt(frame); + } catch (UnexpectedResultException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("wrongListingKeyType", new ProgramValue("", VmUtils.getClass(e.getResult()))) + .withSourceSection(keyNodes[i].getSourceSection()) + .build(); + } + + var value = values[i]; + + // use same error messages as in checkIsValidListingAmendment and checkMaxListingMemberIndex + if (index < 0 || index >= parentLength) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("elementIndexOutOfRange", index, 0, parentLength - 1) + .withSourceSection(value.getHeaderSection()) + .build(); + } + + if (EconomicMaps.put(result, index, value) != null) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("duplicateDefinition", new ProgramValue("", index)) + .withSourceSection(value.getHeaderSection()) + .build(); + } + } + } + + @TruffleBoundary + protected @Nullable ObjectMember findFirstNonProperty( + UnmodifiableEconomicMap members) { + var cursor = EconomicMaps.getEntries(members); + while (cursor.advance()) { + var member = cursor.getValue(); + if (member.getNameOrNull() == null) return member; + } + return null; + } + + @TruffleBoundary + protected @Nullable ObjectMember findFirstNonDefaultProperty( + UnmodifiableEconomicMap members) { + var cursor = EconomicMaps.getEntries(members); + while (cursor.advance()) { + var member = cursor.getValue(); + if (member.getNameOrNull() != null && member.getNameOrNull() != Identifier.DEFAULT) + return member; + } + return null; + } + + @TruffleBoundary + protected void elementsEntriesFallback( + Object parent, @Nullable ObjectMember firstElemOrEntry, boolean isElementsOnly) { + var parentIsClass = parent instanceof VmClass; + var parentClass = parentIsClass ? (VmClass) parent : VmUtils.getClass(parent); + VmUtils.checkIsInstantiable(parentClass, getParentNode()); + + var property = findFirstNonDefaultProperty(members); + if (property != null + && (parentClass == BaseModule.getListingClass() + || parentClass == BaseModule.getMappingClass())) { + throw exceptionBuilder() + .evalError("objectCannotHaveProperty", parentClass) + .withSourceSection(property.getHeaderSection()) + .build(); + } + + assert firstElemOrEntry != null; + + if (isTypedObjectClass(parentClass) + || (isElementsOnly && parentClass == BaseModule.getMappingClass())) { + throw exceptionBuilder() + .evalError( + isElementsOnly ? "objectCannotHaveElement" : "objectCannotHaveEntry", parentClass) + .withSourceSection(firstElemOrEntry.getHeaderSection()) + .build(); + } + + throw exceptionBuilder().unreachableCode().build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/TrueLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/TrueLiteralNode.java new file mode 100644 index 00000000..574c86b3 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/TrueLiteralNode.java @@ -0,0 +1,44 @@ +/** + * 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.ast.expression.literal; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ConstantNode; +import org.pkl.core.ast.ExpressionNode; + +@NodeInfo(shortName = "true") +public final class TrueLiteralNode extends ExpressionNode implements ConstantNode { + public TrueLiteralNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public Boolean executeGeneric(VirtualFrame frame) { + return Boolean.TRUE; + } + + @Override + public boolean executeBoolean(VirtualFrame frame) { + return true; + } + + @Override + public Boolean getValue() { + return Boolean.TRUE; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/package-info.java new file mode 100644 index 00000000..e41d1c19 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.expression.literal; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinMethodNode.java new file mode 100644 index 00000000..98618d14 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinMethodNode.java @@ -0,0 +1,79 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.type.TypeNode.UnknownTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.LateInit; + +/** Infers the parent to amend in `function createPerson(): Person = new { ... }`. */ +public final class InferParentWithinMethodNode extends ExpressionNode { + private final VmLanguage language; + private final Identifier methodName; + @Child private ExpressionNode ownerNode; + @CompilationFinal @LateInit private Object inferredParent; + + public InferParentWithinMethodNode( + SourceSection sourceSection, + VmLanguage language, + Identifier methodName, + ExpressionNode ownerNode) { + + super(sourceSection); + this.language = language; + this.methodName = methodName; + this.ownerNode = ownerNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (inferredParent != null) return inferredParent; + + // remaining code only runs first time this node is executed + // (assuming evaluation isn't continued despite errors) + + CompilerDirectives.transferToInterpreter(); + + var owner = (VmObjectLike) ownerNode.executeGeneric(frame); + assert owner.isPrototype(); + + var method = owner.getVmClass().getDeclaredMethod(methodName); + assert method != null; + + var returnTypeNode = method.getReturnTypeNode(); + if (returnTypeNode == null || returnTypeNode instanceof UnknownTypeNode) { + inferredParent = VmDynamic.empty(); + ownerNode = null; + return inferredParent; + } + + var returnTypeDefaultValue = + returnTypeNode.createDefaultValue( + language, method.getHeaderSection(), method.getQualifiedName()); + if (returnTypeDefaultValue != null) { + inferredParent = returnTypeDefaultValue; + ownerNode = null; + return inferredParent; + } + + throw exceptionBuilder().evalError("cannotInferParent").build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinObjectMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinObjectMethodNode.java new file mode 100644 index 00000000..c789a66b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinObjectMethodNode.java @@ -0,0 +1,87 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMethodNode; +import org.pkl.core.ast.type.TypeNode.UnknownTypeNode; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmDynamic; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmObjectLike; +import org.pkl.core.util.LateInit; + +/** Infers the parent to amend in `obj { local function createPerson(): Person = new { ... } }`. */ +public final class InferParentWithinObjectMethodNode extends ExpressionNode { + private final VmLanguage language; + private final Identifier localMethodName; + @Child private ExpressionNode ownerNode; + @CompilationFinal @LateInit private Object inferredParent; + + public InferParentWithinObjectMethodNode( + SourceSection sourceSection, + VmLanguage language, + Identifier localMethodName, + ExpressionNode ownerNode) { + + super(sourceSection); + this.language = language; + this.localMethodName = localMethodName; + this.ownerNode = ownerNode; + + assert localMethodName.isLocalMethod(); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (inferredParent != null) return inferredParent; + + // remaining code only runs first time this node is executed + // (assuming evaluation isn't continued despite errors) + + CompilerDirectives.transferToInterpreter(); + + var owner = (VmObjectLike) ownerNode.executeGeneric(frame); + + var member = owner.getMember(localMethodName); + assert member != null; + + var methodNode = (ObjectMethodNode) member.getMemberNode(); + assert methodNode != null; + + var returnTypeNode = methodNode.getReturnTypeNode(); + if (returnTypeNode == null || returnTypeNode instanceof UnknownTypeNode) { + inferredParent = VmDynamic.empty(); + ownerNode = null; + return inferredParent; + } + + Object defaultReturnTypeValue = + returnTypeNode.createDefaultValue( + language, member.getHeaderSection(), member.getQualifiedName()); + if (defaultReturnTypeValue != null) { + inferredParent = defaultReturnTypeValue; + ownerNode = null; + return inferredParent; + } + + throw exceptionBuilder().evalError("cannotInferParent").build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinPropertyNode.java new file mode 100644 index 00000000..5c38c26e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinPropertyNode.java @@ -0,0 +1,123 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; + +/** Infers the parent to amend in `x = new { ... }`. */ +@NodeChild(value = "ownerNode", type = ExpressionNode.class) +public abstract class InferParentWithinPropertyNode extends ExpressionNode { + private final Identifier ownPropertyName; + private final boolean isLocalProperty; + + protected InferParentWithinPropertyNode(SourceSection sourceSection, Identifier ownPropertyName) { + super(sourceSection); + this.ownPropertyName = ownPropertyName; + isLocalProperty = ownPropertyName.isLocalProp(); + } + + @Specialization(guards = "!owner.isPrototype()") + protected Object evalTypedObject(VmTyped owner) { + if (isLocalProperty) { + return getLocalPropertyDefaultValue(owner); + } + + try { + var result = VmUtils.readMemberOrNull(owner.getPrototype(), ownPropertyName, false); + assert result != null : "every property has a default"; + return result; + } catch (VmUndefinedValueException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("cannotInferParent").build(); + } + } + + @Specialization(guards = "owner.isPrototype()") + protected Object evalPrototype(VmTyped owner) { + var property = + isLocalProperty + ? + // only getDeclaredProperty() returns local properties + owner.getVmClass().getDeclaredProperty(ownPropertyName) + : + // only getProperty() returns aggregated type information if property type is specified + // in a superclass + owner.getVmClass().getProperty(ownPropertyName); + assert property != null; + + var typeNode = property.getTypeNode(); + if (typeNode == null || typeNode.isUnknownType()) return VmDynamic.empty(); + + var result = typeNode.getDefaultValue(); + if (result != null) return result; + + // no default exists for this property type + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("cannotInferParent").build(); + } + + @Specialization + protected Object eval(@SuppressWarnings("unused") VmDynamic owner) { + if (isLocalProperty) { + return getLocalPropertyDefaultValue(owner); + } + + return VmDynamic.empty(); + } + + @Specialization + protected Object eval(@SuppressWarnings("unused") VmListing owner) { + if (isLocalProperty) { + return getLocalPropertyDefaultValue(owner); + } + + assert ownPropertyName == Identifier.DEFAULT; + // return `VmListing.default` + return VmUtils.readMember(BaseModule.getListingClass().getPrototype(), ownPropertyName); + } + + @Specialization + protected Object eval(@SuppressWarnings("unused") VmMapping owner) { + if (isLocalProperty) { + return getLocalPropertyDefaultValue(owner); + } + + assert ownPropertyName == Identifier.DEFAULT; + // return `VmMapping.default` + return VmUtils.readMember(BaseModule.getMappingClass().getPrototype(), ownPropertyName); + } + + private Object getLocalPropertyDefaultValue(VmObjectLike owner) { + assert isLocalProperty; + + var member = owner.getMember(ownPropertyName); + assert member != null; + + var defaultValue = member.getLocalPropertyDefaultValue(); + if (defaultValue != null) return defaultValue; + + // no default exists for this property type + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("cannotInferParent").build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodDirectNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodDirectNode.java new file mode 100644 index 00000000..951195b3 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodDirectNode.java @@ -0,0 +1,60 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ClassMethod; +import org.pkl.core.runtime.VmObjectLike; + +/** A non-virtual ("direct") method call. */ +public final class InvokeMethodDirectNode extends ExpressionNode { + private final VmObjectLike owner; + @Child private ExpressionNode receiverNode; + @Children private final ExpressionNode[] argumentNodes; + + @Child private DirectCallNode callNode; + + public InvokeMethodDirectNode( + SourceSection sourceSection, + ClassMethod method, + ExpressionNode receiverNode, + ExpressionNode[] argumentNodes) { + + super(sourceSection); + this.owner = method.getOwner(); + this.receiverNode = receiverNode; + this.argumentNodes = argumentNodes; + + callNode = DirectCallNode.create(method.getCallTarget(sourceSection)); + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + var args = new Object[2 + argumentNodes.length]; + args[0] = receiverNode.executeGeneric(frame); + args[1] = owner; + for (var i = 0; i < argumentNodes.length; i++) { + args[2 + i] = argumentNodes[i].executeGeneric(frame); + } + + return callNode.call(args); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodLexicalNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodLexicalNode.java new file mode 100644 index 00000000..e549bea7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodLexicalNode.java @@ -0,0 +1,75 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.frame.Frame; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +/** + * A non-virtual method call whose call target is in the lexical scope of the call site. Mainly used + * for calling `local` methods. + */ +public final class InvokeMethodLexicalNode extends ExpressionNode { + @Children private final ExpressionNode[] argumentNodes; + private final int levelsUp; + + @Child private DirectCallNode callNode; + + InvokeMethodLexicalNode( + SourceSection sourceSection, + CallTarget callTarget, + int levelsUp, + ExpressionNode[] argumentNodes) { + + super(sourceSection); + this.levelsUp = levelsUp; + this.argumentNodes = argumentNodes; + + callNode = DirectCallNode.create(callTarget); + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + var args = new Object[2 + argumentNodes.length]; + var enclosingFrame = getEnclosingFrame(frame); + args[0] = VmUtils.getReceiver(enclosingFrame); + args[1] = VmUtils.getOwner(enclosingFrame); + for (var i = 0; i < argumentNodes.length; i++) { + args[2 + i] = argumentNodes[i].executeGeneric(frame); + } + + return callNode.call(args); + } + + @ExplodeLoop + private Frame getEnclosingFrame(VirtualFrame frame) { + if (levelsUp == 0) return frame; + + var owner = VmUtils.getOwner(frame); + for (var i = 1; i < levelsUp; i++) { + owner = owner.getEnclosingOwner(); + assert owner != null; + } + return owner.getEnclosingFrame(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java new file mode 100644 index 00000000..2160f8c6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java @@ -0,0 +1,190 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.ImportStatic; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberLookupMode; +import org.pkl.core.ast.internal.GetClassNode; +import org.pkl.core.ast.member.ClassMethod; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmFunction; + +/** A virtual method call. */ +@ImportStatic(Identifier.class) +@NodeChild(value = "receiverNode", type = ExpressionNode.class) +@NodeChild(value = "receiverClassNode", type = GetClassNode.class, executeWith = "receiverNode") +public abstract class InvokeMethodVirtualNode extends ExpressionNode { + protected final Identifier methodName; + @Children private final ExpressionNode[] argumentNodes; + private final MemberLookupMode lookupMode; + private final boolean needsConst; + @CompilationFinal private boolean isConstChecked; + + protected InvokeMethodVirtualNode( + SourceSection sourceSection, + Identifier methodName, + ExpressionNode[] argumentNodes, + MemberLookupMode lookupMode, + boolean needsConst) { + + super(sourceSection); + this.methodName = methodName; + this.argumentNodes = argumentNodes; + this.lookupMode = lookupMode; + this.needsConst = needsConst; + } + + protected InvokeMethodVirtualNode( + SourceSection sourceSection, + Identifier methodName, + ExpressionNode[] argumentNodes, + MemberLookupMode lookupMode) { + this(sourceSection, methodName, argumentNodes, lookupMode, false); + } + + /** + * When only using this execute method, pass `null` for `receiverNode` and `receiverClassNode` to + * {@link InvokeMethodVirtualNodeGen#create}. + */ + public abstract Object executeWith(VirtualFrame frame, Object value, VmClass clazz); + + /** Intrinsifies `FunctionN.apply()` calls. */ + @ExplodeLoop + @Specialization(guards = {"methodName == APPLY", "receiver.getCallTarget() == cachedCallTarget"}) + protected Object evalFunctionCached( + VirtualFrame frame, + VmFunction receiver, + @SuppressWarnings("unused") VmClass receiverClass, + @Cached("receiver.getCallTarget()") @SuppressWarnings("unused") + RootCallTarget cachedCallTarget, + @Cached("create(cachedCallTarget)") DirectCallNode callNode) { + + var args = new Object[2 + argumentNodes.length]; + args[0] = receiver.getThisValue(); + args[1] = receiver; + for (var i = 0; i < argumentNodes.length; i++) { + args[2 + i] = argumentNodes[i].executeGeneric(frame); + } + + return callNode.call(args); + } + + /** Intrinsifies `FunctionN.apply()` calls. */ + @ExplodeLoop + @Specialization(guards = "methodName == APPLY", replaces = "evalFunctionCached") + protected Object evalFunction( + VirtualFrame frame, + VmFunction receiver, + VmClass receiverClass, + @Cached("create()") IndirectCallNode callNode) { + + checkConst(receiverClass); + var args = new Object[2 + argumentNodes.length]; + args[0] = receiver.getThisValue(); + args[1] = receiver; + for (var i = 0; i < argumentNodes.length; i++) { + args[2 + i] = argumentNodes[i].executeGeneric(frame); + } + + return callNode.call(receiver.getCallTarget(), args); + } + + @ExplodeLoop + @Specialization(guards = "receiverClass == cachedReceiverClass") + protected Object evalCached( + VirtualFrame frame, + Object receiver, + @SuppressWarnings("unused") VmClass receiverClass, + @Cached("receiverClass") @SuppressWarnings("unused") VmClass cachedReceiverClass, + @Cached("resolveMethod(receiverClass)") ClassMethod method, + @Cached("create(method.getCallTarget(sourceSection))") DirectCallNode callNode) { + + checkConst(method); + var args = new Object[2 + argumentNodes.length]; + args[0] = receiver; + args[1] = method.getOwner(); + for (var i = 0; i < argumentNodes.length; i++) { + args[2 + i] = argumentNodes[i].executeGeneric(frame); + } + + return callNode.call(args); + } + + @ExplodeLoop + @Specialization(replaces = "evalCached") + protected Object eval( + VirtualFrame frame, + Object receiver, + VmClass receiverClass, + @Cached("create()") IndirectCallNode callNode) { + + var method = resolveMethod(receiverClass); + checkConst(method); + + var args = new Object[2 + argumentNodes.length]; + args[0] = receiver; + args[1] = method.getOwner(); + for (var i = 0; i < argumentNodes.length; i++) { + args[2 + i] = argumentNodes[i].executeGeneric(frame); + } + + // Deprecation should not report here (getCallTarget(sourceSection)), as this happens for each + // and every call. + return callNode.call(method.getCallTarget(), args); + } + + protected ClassMethod resolveMethod(VmClass receiverClass) { + var method = receiverClass.getMethod(methodName); + if (method != null) return method; + + CompilerDirectives.transferToInterpreter(); + + throw exceptionBuilder() + .cannotFindMethod( + receiverClass.getPrototype(), + methodName, + argumentNodes.length, + lookupMode != MemberLookupMode.EXPLICIT_RECEIVER) + .build(); + } + + private void checkConst(VmClass receiverClass) { + checkConst(resolveMethod(receiverClass)); + } + + private void checkConst(ClassMethod method) { + if (needsConst && !isConstChecked) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + if (!method.isConst()) { + throw exceptionBuilder().evalError("methodMustBeConst", methodName.toString()).build(); + } + isConstChecked = true; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeSuperMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeSuperMethodNode.java new file mode 100644 index 00000000..278ee312 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeSuperMethodNode.java @@ -0,0 +1,89 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ClassMethod; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmUtils; + +public abstract class InvokeSuperMethodNode extends ExpressionNode { + private final Identifier methodName; + @Children private final ExpressionNode[] argumentNodes; + private final boolean needsConst; + + protected InvokeSuperMethodNode( + SourceSection sourceSection, + Identifier methodName, + ExpressionNode[] argumentNodes, + boolean needsConst) { + + super(sourceSection); + this.needsConst = needsConst; + + assert !methodName.isLocalMethod(); + + this.methodName = methodName; + this.argumentNodes = argumentNodes; + } + + @ExplodeLoop + @Specialization + protected Object eval( + VirtualFrame frame, + @Cached("findSupermethod(frame)") ClassMethod supermethod, + @Cached("create(supermethod.getCallTarget(sourceSection))") DirectCallNode callNode) { + + if (needsConst && !supermethod.isConst()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("methodMustBeConst", methodName.toString()).build(); + } + var args = new Object[2 + argumentNodes.length]; + args[0] = VmUtils.getReceiverOrNull(frame); + args[1] = supermethod.getOwner(); + for (int i = 0; i < argumentNodes.length; i++) { + args[2 + i] = argumentNodes[i].executeGeneric(frame); + } + + return callNode.call(args); + } + + protected ClassMethod findSupermethod(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + assert owner.isPrototype(); + + var superclass = owner.getVmClass().getSuperclass(); + assert superclass != null; + + // note the use of getMethod() rather than getDeclaredMethod() + var supermethod = superclass.getMethod(methodName); + if (supermethod != null) return supermethod; + + CompilerDirectives.transferToInterpreter(); + var parent = owner.getParent(); + assert parent != null; + throw exceptionBuilder() + .cannotFindMethod(parent, methodName, argumentNodes.length, false) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java new file mode 100644 index 00000000..fd962ca0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java @@ -0,0 +1,79 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.VmObjectLike; +import org.pkl.core.runtime.VmUtils; + +/** Reads a local non-constant property that is known to exist in the lexical scope of this node. */ +public final class ReadLocalPropertyNode extends ExpressionNode { + private final ObjectMember property; + private final int levelsUp; + @Child private DirectCallNode callNode; + + public ReadLocalPropertyNode(SourceSection sourceSection, ObjectMember property, int levelsUp) { + + super(sourceSection); + CompilerAsserts.neverPartOfCompilation(); + + this.property = property; + this.levelsUp = levelsUp; + + assert property.getNameOrNull() != null; + assert property.getConstantValue() == null : "Use a ConstantNode instead."; + + callNode = DirectCallNode.create(property.getCallTarget()); + } + + @Override + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + Object receiver; + + if (levelsUp == 0) { + receiver = VmUtils.getReceiver(frame); + } else { + for (int i = 1; i < levelsUp; i++) { + owner = owner.getEnclosingOwner(); + assert owner != null; + } + + receiver = owner.getEnclosingReceiver(); + owner = owner.getEnclosingOwner(); + } + + assert receiver instanceof VmObjectLike + : "Assumption: This node isn't used in Truffle ASTs of `external` pkl.base classes whose values aren't VmObject's."; + + var objReceiver = (VmObjectLike) receiver; + var result = objReceiver.getCachedValue(property); + + if (result == null) { + result = callNode.call(objReceiver, owner, property.getName()); + objReceiver.setCachedValue(property, result); + } + + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java new file mode 100644 index 00000000..f5333a86 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java @@ -0,0 +1,135 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.*; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +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.MemberLookupMode; +import org.pkl.core.ast.member.ClassProperty; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +@NodeInfo(shortName = ".") +@ImportStatic(BaseModule.class) +@NodeChild(value = "receiverNode", type = ExpressionNode.class) +public abstract class ReadPropertyNode extends ExpressionNode { + protected final Identifier propertyName; + private final MemberLookupMode lookupMode; + private final boolean needsConst; + @CompilationFinal private boolean isConstChecked; + + protected ReadPropertyNode( + SourceSection sourceSection, + Identifier propertyName, + MemberLookupMode lookupMode, + boolean needsConst) { + + super(sourceSection); + this.propertyName = propertyName; + this.lookupMode = lookupMode; + this.needsConst = needsConst; + + assert !propertyName.isLocalProp() : "Must use ReadLocalPropertyNode for local properties."; + } + + protected ReadPropertyNode( + SourceSection sourceSection, Identifier propertyName, boolean needsConst) { + this(sourceSection, propertyName, MemberLookupMode.EXPLICIT_RECEIVER, needsConst); + } + + protected ReadPropertyNode(SourceSection sourceSection, Identifier propertyName) { + this(sourceSection, propertyName, MemberLookupMode.EXPLICIT_RECEIVER, false); + } + + // This method effectively covers `VmObject receiver` but is implemented in a more + // efficient way. See: + // https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces + @Specialization(guards = "receiver.getClass() == cachedClass", limit = "99") + protected Object evalObject( + Object receiver, + @Cached("getVmObjectSubclassOrNull(receiver)") Class cachedClass, + @Cached("create()") IndirectCallNode callNode) { + + var object = cachedClass.cast(receiver); + checkConst(object); + var result = VmUtils.readMemberOrNull(object, propertyName, true, callNode); + if (result != null) return result; + + CompilerDirectives.transferToInterpreter(); + throw cannotFindProperty(object); + } + + // specializations for all other types + @Specialization(guards = "receiver.getClass() == cachedClass", limit = "99") + protected Object evalOther( + Object receiver, + @Cached("receiver.getClass()") @SuppressWarnings("unused") Class cachedClass, + @Cached("resolveProperty(receiver)") ClassProperty resolvedProperty, + @Cached("createCallNode(resolvedProperty)") DirectCallNode callNode) { + + return callNode.call(receiver, resolvedProperty.getOwner(), resolvedProperty.getName()); + } + + protected static @Nullable Class getVmObjectSubclassOrNull(Object value) { + // OK to perform slow cast here (not a guard) + return value instanceof VmObjectLike ? ((VmObjectLike) value).getClass() : null; + } + + protected ClassProperty resolveProperty(Object value) { + var clazz = VmUtils.getClass(value); + var propertyDef = clazz.getProperty(propertyName); + if (propertyDef != null) return propertyDef; + + CompilerDirectives.transferToInterpreter(); + throw cannotFindProperty(clazz.getPrototype()); + } + + // This method should only be used for standard library properties implemented as nodes. + protected static DirectCallNode createCallNode(ClassProperty resolvedProperty) { + var callTarget = resolvedProperty.getInitializer().getCallTarget(); + return DirectCallNode.create(callTarget); + } + + @TruffleBoundary + private VmException cannotFindProperty(VmObjectLike receiver) { + return exceptionBuilder() + .cannotFindProperty( + receiver, propertyName, true, lookupMode != MemberLookupMode.EXPLICIT_RECEIVER) + .build(); + } + + private void checkConst(VmObjectLike receiver) { + if (needsConst && !isConstChecked) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + var property = receiver.getVmClass().getProperty(propertyName); + if (property == null) { + // fall through; `cannotFindProperty` gets thrown when we attempt to read the property. + return; + } + if (!property.isConst()) { + throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build(); + } + isConstChecked = true; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadSuperEntryNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadSuperEntryNode.java new file mode 100644 index 00000000..96b2b0a6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadSuperEntryNode.java @@ -0,0 +1,74 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; + +/** + * An expression of the form `super[key]`. + * + *

Note: Reading an entry (`object[key]`) is the subscript operator (SubscriptNode). + */ +public class ReadSuperEntryNode extends ExpressionNode { + @Child private ExpressionNode keyNode; + @Child private IndirectCallNode callNode = IndirectCallNode.create(); + + public ReadSuperEntryNode(SourceSection sourceSection, ExpressionNode keyNode) { + super(sourceSection); + this.keyNode = keyNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + var key = keyNode.executeGeneric(frame); + + var receiver = VmUtils.getObjectReceiver(frame); + + var initialOwner = VmUtils.getOwner(frame); + while (initialOwner instanceof VmFunction) { + initialOwner = initialOwner.getEnclosingOwner(); + } + assert initialOwner != null : "VmFunction always has a parent"; + initialOwner = initialOwner.getParent(); + + for (var owner = initialOwner; owner != null; owner = owner.getParent()) { + var member = owner.getMember(key); + if (member == null) continue; + + var constantValue = member.getConstantValue(); + if (constantValue != null) return constantValue; // TODO: type check + + // caching the result of a super call is tricky (function of both receiver and owner) + return callNode.call( + member.getCallTarget(), + // TODO: should the marker only turn off constraint checking, not overall type checking? + receiver, + owner, + key, + VmUtils.SKIP_TYPECHECK_MARKER); + } + + // not found -> apply lambda contained in `default` property + var defaultFunction = + (VmFunction) VmUtils.readMemberOrNull(receiver, Identifier.DEFAULT, callNode); + assert defaultFunction != null; + return defaultFunction.apply(key); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadSuperPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadSuperPropertyNode.java new file mode 100644 index 00000000..3dc7667d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadSuperPropertyNode.java @@ -0,0 +1,79 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; + +public final class ReadSuperPropertyNode extends ExpressionNode { + + private final Identifier propertyName; + private final boolean needsConst; + + @Child private IndirectCallNode callNode = IndirectCallNode.create(); + + public ReadSuperPropertyNode( + SourceSection sourceSection, Identifier propertyName, boolean needsConst) { + super(sourceSection); + this.propertyName = propertyName; + this.needsConst = needsConst; + } + + // TODO: how can this be optimized? + // (result not cached and expensive lookups on every execution) + public Object executeGeneric(VirtualFrame frame) { + var receiver = VmUtils.getObjectReceiver(frame); + + // start from the parent of the owner of the `super.` expression + // skip any function object owners (same as when resolving `this`) + // `receiver` must be passed on unchanged to make sure that overridden properties still take + // effect + var initialOwner = VmUtils.getOwner(frame); + while (initialOwner instanceof VmFunction) { + initialOwner = initialOwner.getEnclosingOwner(); + } + assert initialOwner != null : "VmFunction always has a parent"; + initialOwner = initialOwner.getParent(); + + for (var owner = initialOwner; owner != null; owner = owner.getParent()) { + var property = owner.getMember(propertyName); + if (property == null) continue; + if (needsConst && !property.isConst()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build(); + } + + var constantValue = property.getConstantValue(); + if (constantValue != null) return constantValue; // TODO: type check + + // caching the result of a super call is tricky (function of both receiver and owner) + return callNode.call( + property.getCallTarget(), + // TODO: should the marker only turn off constraint checking, not overall type checking? + receiver, + owner, + propertyName, + VmUtils.SKIP_TYPECHECK_MARKER); + } + + // TODO: refine when to return VmDynamic.empty() and when to fail + return VmDynamic.empty(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ResolveMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ResolveMethodNode.java new file mode 100644 index 00000000..8533f6c4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ResolveMethodNode.java @@ -0,0 +1,184 @@ +/** + * 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.ast.expression.member; + +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ConstantValueNode; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberLookupMode; +import org.pkl.core.ast.builder.ConstLevel; +import org.pkl.core.ast.expression.primary.*; +import org.pkl.core.ast.internal.GetClassNodeGen; +import org.pkl.core.ast.member.Member; +import org.pkl.core.runtime.*; + +/** + * Resolves a method name in a method call with implicit receiver, for example `bar` in `x = bar()` + * (but not `foo.bar()`). + * + *

A method name can refer to any of the following: - a (potentially `local`) method in the + * lexical scope - a base module method - a method accessible through `this` + * + *

This node's task is to make a one-time decision between these alternatives for the call site + * it represents. + */ +// TODO: Consider doing this at parse time (cf. ResolveVariableNode). +@NodeInfo(shortName = "resolveMethod") +public final class ResolveMethodNode extends ExpressionNode { + private final Identifier methodName; + private final ExpressionNode[] argumentNodes; + // Tells if the call site is inside the base module. + private final boolean isBaseModule; + // Tells if the call site is inside a [CustomThisScope]. + private final boolean isCustomThisScope; + private final ConstLevel constLevel; + private final int constDepth; + + public ResolveMethodNode( + SourceSection sourceSection, + Identifier methodName, + ExpressionNode[] argumentNodes, + boolean isBaseModule, + boolean isCustomThisScope, + ConstLevel constLevel, + int constDepth) { + + super(sourceSection); + + this.methodName = methodName; + this.argumentNodes = argumentNodes; + this.isBaseModule = isBaseModule; + this.isCustomThisScope = isCustomThisScope; + this.constLevel = constLevel; + this.constDepth = constDepth; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return replace(doResolve(VmUtils.getOwner(frame))).executeGeneric(frame); + } + + @TruffleBoundary + private ExpressionNode doResolve(VmObjectLike initialOwner) { + var levelsUp = 0; + Identifier localMethodName = methodName.toLocalMethod(); + + // Search lexical scope. + for (var currOwner = initialOwner; + currOwner != null; + currOwner = currOwner.getEnclosingOwner()) { + + if (currOwner.isPrototype()) { + var localMethod = currOwner.getVmClass().getDeclaredMethod(localMethodName); + if (localMethod != null) { + assert localMethod.isLocal(); + checkConst(currOwner, localMethod, levelsUp); + return new InvokeMethodLexicalNode( + sourceSection, localMethod.getCallTarget(sourceSection), levelsUp, argumentNodes); + } + var method = currOwner.getVmClass().getDeclaredMethod(methodName); + if (method != null) { + assert !method.isLocal(); + checkConst(currOwner, method, levelsUp); + if (method.getDeclaringClass().isClosed()) { + return new InvokeMethodLexicalNode( + sourceSection, method.getCallTarget(sourceSection), levelsUp, argumentNodes); + } + + //noinspection ConstantConditions + return InvokeMethodVirtualNodeGen.create( + sourceSection, + methodName, + argumentNodes, + MemberLookupMode.IMPLICIT_LEXICAL, + levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp), + GetClassNodeGen.create(null)); + } + } else { + var localMethod = currOwner.getMember(localMethodName); + if (localMethod != null) { + assert localMethod.isLocal(); + checkConst(currOwner, localMethod, levelsUp); + var methodCallTarget = + // TODO: is it OK to pass owner as receiver here? + // (calls LocalMethodNode, which only resolves types) + (CallTarget) localMethod.getCallTarget().call(currOwner, currOwner); + + return new InvokeMethodLexicalNode( + sourceSection, methodCallTarget, levelsUp, argumentNodes); + } + } + + levelsUp += 1; + } + + // Search base module (unless call site is itself inside base module). + if (!isBaseModule) { + var baseModule = BaseModule.getModule(); + // use `getDeclaredMethod()` so as not to resolve to anything declared in class + // pkl.base#Module + var method = baseModule.getVmClass().getDeclaredMethod(methodName); + if (method != null) { + assert !method.isLocal(); + return new InvokeMethodDirectNode( + sourceSection, method, new ConstantValueNode(baseModule), argumentNodes); + } + } + + // Assuming this method exists at all, it must be a method accessible through `this`. + // + // Calling a method off implicit `this` needs a const check if the node is not in a const scope + // (see ResolveVariableNode for an explanation) + // + // Edge case: always allow method calls for custom `this` scopes (member predicates, type + // constraints) + // because they do not refer to a lexical `this`. + boolean needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThisScope; + //noinspection ConstantConditions + return InvokeMethodVirtualNodeGen.create( + sourceSection, + methodName, + argumentNodes, + MemberLookupMode.IMPLICIT_THIS, + needsConst, + VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThisScope), + GetClassNodeGen.create(null)); + } + + @SuppressWarnings("DuplicatedCode") + private void checkConst(VmObjectLike currOwner, Member method, int levelsUp) { + if (!constLevel.isConst()) { + return; + } + var memberIsOutsideConstScope = levelsUp > constDepth; + var invalid = false; + switch (constLevel) { + case ALL: + invalid = memberIsOutsideConstScope && !method.isConst(); + break; + case MODULE: + invalid = currOwner.isModuleObject() && !method.isConst(); + break; + } + if (invalid) { + throw exceptionBuilder().evalError("methodMustBeConst", methodName.toString()).build(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/package-info.java new file mode 100644 index 00000000..caae5599 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.expression.member; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/package-info.java new file mode 100644 index 00000000..7a057cef --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.expression; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/CustomThisNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/CustomThisNode.java new file mode 100644 index 00000000..508b90d7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/CustomThisNode.java @@ -0,0 +1,45 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +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.builder.SymbolTable.CustomThisScope; +import org.pkl.core.runtime.VmUtils; + +/** `this` inside `CustomThisScope` (type constraint, object member predicate). */ +@NodeInfo(shortName = "this") +public final class CustomThisNode extends ExpressionNode { + @CompilationFinal private int customThisSlot = -1; + + public CustomThisNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (customThisSlot == -1) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + // deferred until execution time s.t. nodes of inlined type aliases get the right frame slot + customThisSlot = VmUtils.findAuxiliarySlot(frame, CustomThisScope.FRAME_SLOT_ID); + } + return frame.getAuxiliarySlot(customThisSlot); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingOwnerNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingOwnerNode.java new file mode 100644 index 00000000..33062dfa --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingOwnerNode.java @@ -0,0 +1,43 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +public final class GetEnclosingOwnerNode extends ExpressionNode { + private final int levelsUp; + + public GetEnclosingOwnerNode(int levelsUp) { + this.levelsUp = levelsUp; + + assert levelsUp > 0 : "shouldn't be using GetEnclosingOwnerNode for levelsUp == 0"; + } + + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + for (var i = 1; i < levelsUp; i++) { + owner = owner.getEnclosingOwner(); + assert owner != null; + } + var result = owner.getEnclosingOwner(); + assert result != null; + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingReceiverNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingReceiverNode.java new file mode 100644 index 00000000..be440a40 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingReceiverNode.java @@ -0,0 +1,43 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +public final class GetEnclosingReceiverNode extends ExpressionNode { + private final int levelsUp; + + public GetEnclosingReceiverNode(int levelsUp) { + this.levelsUp = levelsUp; + + assert levelsUp > 0 : "shouldn't be using GetEnclosingReceiverNode for levelsUp == 0"; + } + + @ExplodeLoop + public Object executeGeneric(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + for (var i = 1; i < levelsUp; i++) { + owner = owner.getEnclosingOwner(); + assert owner != null; + } + var result = owner.getEnclosingReceiver(); + assert result != null; + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetMemberKeyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetMemberKeyNode.java new file mode 100644 index 00000000..1f0bcc97 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetMemberKeyNode.java @@ -0,0 +1,28 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +/** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */ +public final class GetMemberKeyNode extends ExpressionNode { + @Override + public Object executeGeneric(VirtualFrame frame) { + return VmUtils.getMemberKey(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetModuleNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetModuleNode.java new file mode 100644 index 00000000..1d6335ce --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetModuleNode.java @@ -0,0 +1,44 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +@NodeInfo(shortName = "module") +public final class GetModuleNode extends ExpressionNode { + public GetModuleNode(SourceSection sourceSection) { + super(sourceSection); + } + + public Object executeGeneric(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var levelsUp = 0; + for (var current = VmUtils.getOwner(frame).getEnclosingOwner(); + current != null; + current = current.getEnclosingOwner()) { + levelsUp += 1; + } + + return replace(levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp)) + .executeGeneric(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetOwnerNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetOwnerNode.java new file mode 100644 index 00000000..26fdd72d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetOwnerNode.java @@ -0,0 +1,29 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +@NodeInfo(shortName = "owner") +public final class GetOwnerNode extends ExpressionNode { + @Override + public Object executeGeneric(VirtualFrame frame) { + return VmUtils.getOwner(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetReceiverNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetReceiverNode.java new file mode 100644 index 00000000..b0341c02 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetReceiverNode.java @@ -0,0 +1,29 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +@NodeInfo(shortName = "receiver") +public final class GetReceiverNode extends ExpressionNode { + @Override + public Object executeGeneric(VirtualFrame frame) { + return VmUtils.getReceiver(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/OuterNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/OuterNode.java new file mode 100644 index 00000000..40f4806d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/OuterNode.java @@ -0,0 +1,43 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +// TODO: should `outer.x` work like `super.x`? (start search in enclosing scope) +// TODO: if enclosing scope is lambda scope, can't use `lambda.x` to access lambda param `x` +@NodeInfo(shortName = "outer") +public final class OuterNode extends ExpressionNode { + + public OuterNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + var outer = VmUtils.getOwner(frame).getEnclosingReceiver(); + if (outer == null) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("noOuterScope").build(); + } + return outer; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java new file mode 100644 index 00000000..c0215cd0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java @@ -0,0 +1,242 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ConstantValueNode; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberLookupMode; +import org.pkl.core.ast.builder.ConstLevel; +import org.pkl.core.ast.expression.member.ReadLocalPropertyNode; +import org.pkl.core.ast.expression.member.ReadPropertyNodeGen; +import org.pkl.core.ast.frame.ReadAuxiliarySlotNode; +import org.pkl.core.ast.frame.ReadEnclosingAuxiliarySlotNode; +import org.pkl.core.ast.frame.ReadEnclosingFrameSlotNodeGen; +import org.pkl.core.ast.frame.ReadFrameSlotNodeGen; +import org.pkl.core.ast.member.Member; +import org.pkl.core.runtime.*; + +/** + * Resolves a variable name, for example `foo` in `x = foo`. + * + *

A variable name can refer to any of the following: - a (potentially `local`) property in the + * lexical scope - a method or lambda parameter in the lexical scope - a base module property - a + * property accessible through `this` + * + *

This node's task is to make a one-time decision between these alternatives for the call site + * it represents. + */ +// TODO: Move this to parse time (required for supporting local variables, more efficient) +// +// TODO: In REPL, undo replace if environment changes to make the following work. +// Perhaps instrumenting this node in REPL would be a good solution. +// x = { y = z } +// :force x // Property not found: z +// z = 1 +// :force x // should work but doesn't +public final class ResolveVariableNode extends ExpressionNode { + private final Identifier variableName; + private final boolean isBaseModule; + private final boolean isCustomThisScope; + private final ConstLevel constLevel; + private final int constDepth; + + public ResolveVariableNode( + SourceSection sourceSection, + Identifier variableName, + boolean isBaseModule, + boolean isCustomThisScope, + ConstLevel constLevel, + int constDepth) { + super(sourceSection); + this.variableName = variableName; + this.isBaseModule = isBaseModule; + this.isCustomThisScope = isCustomThisScope; + this.constLevel = constLevel; + this.constDepth = constDepth; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return replace(doResolve(frame)).executeGeneric(frame); + } + + private ExpressionNode doResolve(VirtualFrame frame) { + // don't compile this (only runs once) + // invalidation will be done by Node.replace() in the caller + CompilerDirectives.transferToInterpreter(); + + var localPropertyName = variableName.toLocalProperty(); + + // search the frame for auxiliary slots carrying this variable (placed by + // `WriteForVariablesNode`) + var variableSlot = VmUtils.findAuxiliarySlot(frame, localPropertyName); + if (variableSlot == -1) { + variableSlot = VmUtils.findAuxiliarySlot(frame, variableName); + } + if (variableSlot != -1) { + return new ReadAuxiliarySlotNode(getSourceSection(), variableSlot); + } + // search the frame for slots carrying this variable + variableSlot = VmUtils.findSlot(frame, localPropertyName); + if (variableSlot == -1) { + variableSlot = VmUtils.findSlot(frame, variableName); + } + if (variableSlot != -1) { + return ReadFrameSlotNodeGen.create(getSourceSection(), variableSlot); + } + + var currFrame = frame; + var currOwner = VmUtils.getOwner(currFrame); + var levelsUp = 0; + + // Search lexical scope for a matching method/lambda parameter, `for` generator variable, or + // object property. + do { + var parameterSlot = VmUtils.findSlot(currFrame, variableName); + if (parameterSlot == -1) { + parameterSlot = VmUtils.findSlot(currFrame, localPropertyName); + } + if (parameterSlot != -1) { + return levelsUp == 0 + ? ReadFrameSlotNodeGen.create(getSourceSection(), parameterSlot) + : ReadEnclosingFrameSlotNodeGen.create(getSourceSection(), parameterSlot, levelsUp); + } + var auxiliarySlot = VmUtils.findAuxiliarySlot(currFrame, variableName); + if (auxiliarySlot == -1) { + auxiliarySlot = VmUtils.findAuxiliarySlot(currFrame, localPropertyName); + } + if (auxiliarySlot != -1) { + return levelsUp == 0 + ? new ReadAuxiliarySlotNode(getSourceSection(), auxiliarySlot) + : new ReadEnclosingAuxiliarySlotNode(getSourceSection(), auxiliarySlot, levelsUp); + } + + var localMember = currOwner.getMember(localPropertyName); + if (localMember != null) { + assert localMember.isLocal(); + + checkConst(currOwner, localMember, levelsUp); + + var value = localMember.getConstantValue(); + if (value != null) { + // This is the only code path that resolves local constant properties. + // Since this code path doesn't use ObjectMember.getCachedValue(), + // there is no point in calling localMember.setCachedValue() either. + return new ConstantValueNode(sourceSection, value); + } + + return new ReadLocalPropertyNode(sourceSection, localMember, levelsUp); + } + + var member = currOwner.getMember(variableName); + if (member != null) { + assert !member.isLocal(); + + checkConst(currOwner, member, levelsUp); + + // Non-local properties are late-bound, which is why we can't ever return ConstantNode here. + // + // Assuming this node isn't used in Truffle ASTs of `external` pkl.base classes whose values + // aren't VmObject's, + // we only ever need VmObject-compatible specializations here. + // We don't exploit this fact here but ReadLocalPropertyNode (used above) does. + return ReadPropertyNodeGen.create( + sourceSection, + variableName, + MemberLookupMode.IMPLICIT_LEXICAL, + // we already checked for const-safety, no need to recheck + false, + levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp)); + } + + currFrame = currOwner.getEnclosingFrame(); + currOwner = VmUtils.getOwnerOrNull(currFrame); + levelsUp += 1; + } while (currOwner != null); + + // Search base module, unless call site is inside base module. + if (!isBaseModule) { + var baseModule = BaseModule.getModule(); + + var cachedValue = baseModule.getCachedValue(variableName); + if (cachedValue != null) { + return new ConstantValueNode(sourceSection, cachedValue); + } + + var member = baseModule.getMember(variableName); + + if (member != null) { + var constantValue = member.getConstantValue(); + if (constantValue != null) { + baseModule.setCachedValue(variableName, constantValue); + return new ConstantValueNode(sourceSection, constantValue); + } + + var computedValue = member.getCallTarget().call(baseModule, baseModule); + baseModule.setCachedValue(variableName, computedValue); + return new ConstantValueNode(sourceSection, computedValue); + } + } + + // Assuming this method exists at all, it must be a method accessible through `this`. + /// + // Reading a property off of implicit `this` needs a const check if this node is not in a const + // scope. + // open class A { + // a = 1 + // } + // + // class B extends A { + // const b = a // <-- implicit this lookup of `a`, which is not in a const scope. + // } + // + // A const scope exists if there is an object body, for example. + // + // class B extends A { + // const b = new { a } // <-- `new {}` creates a const scope. + // } + boolean needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThisScope; + return ReadPropertyNodeGen.create( + sourceSection, + variableName, + MemberLookupMode.IMPLICIT_THIS, + needsConst, + VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThisScope)); + } + + @SuppressWarnings("DuplicatedCode") + private void checkConst(VmObjectLike currOwner, Member member, int levelsUp) { + if (!constLevel.isConst()) { + return; + } + var memberIsOutsideConstScope = levelsUp > constDepth; + var invalid = false; + switch (constLevel) { + case ALL: + invalid = memberIsOutsideConstScope && !member.isConst(); + break; + case MODULE: + invalid = currOwner.isModuleObject() && !member.isConst(); + break; + } + if (invalid) { + throw exceptionBuilder().evalError("propertyMustBeConst", variableName.toString()).build(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ThisNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ThisNode.java new file mode 100644 index 00000000..2c9520c1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ThisNode.java @@ -0,0 +1,34 @@ +/** + * 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.ast.expression.primary; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmUtils; + +@NodeInfo(shortName = "this") +public final class ThisNode extends ExpressionNode { + public ThisNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return VmUtils.getReceiver(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/package-info.java new file mode 100644 index 00000000..36c30fd4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.expression.primary; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/ternary/IfElseNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/ternary/IfElseNode.java new file mode 100644 index 00000000..597fd1cb --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/ternary/IfElseNode.java @@ -0,0 +1,63 @@ +/** + * 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.ast.expression.ternary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.BaseModule; + +@NodeInfo(shortName = "if") +public final class IfElseNode extends ExpressionNode { + @Child private ExpressionNode conditionNode; + + @Child private ExpressionNode thenNode; + + @Child private ExpressionNode elseNode; + + public IfElseNode( + SourceSection sourceSection, + ExpressionNode conditionNode, + ExpressionNode thenNode, + ExpressionNode elseNode) { + super(sourceSection); + this.conditionNode = conditionNode; + this.thenNode = thenNode; + this.elseNode = elseNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return evaluateCondition(frame) + ? thenNode.executeGeneric(frame) + : elseNode.executeGeneric(frame); + } + + private boolean evaluateCondition(VirtualFrame frame) { + try { + return conditionNode.executeBoolean(frame); + } catch (UnexpectedResultException e) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .typeMismatch(e.getResult(), BaseModule.getBooleanClass()) + .withSourceSection(conditionNode.getSourceSection()) + .build(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/ternary/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/ternary/package-info.java new file mode 100644 index 00000000..c1aabf33 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/ternary/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.expression.ternary; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractImportNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractImportNode.java new file mode 100644 index 00000000..054382b5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractImportNode.java @@ -0,0 +1,33 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.source.SourceSection; +import java.net.URI; +import org.pkl.core.ast.ExpressionNode; + +public abstract class AbstractImportNode extends ExpressionNode { + protected final URI importUri; + + AbstractImportNode(SourceSection sourceSection, URI importUri) { + super(sourceSection); + this.importUri = importUri; + } + + public URI getImportUri() { + return importUri; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractReadNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractReadNode.java new file mode 100644 index 00000000..0e5c891d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/AbstractReadNode.java @@ -0,0 +1,82 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; + +public abstract class AbstractReadNode extends UnaryExpressionNode { + private final ModuleKey moduleKey; + + protected AbstractReadNode(SourceSection sourceSection, ModuleKey moduleKey) { + super(sourceSection); + this.moduleKey = moduleKey; + } + + @TruffleBoundary + protected @Nullable Object doRead(String resourceUri, VmContext context, Node readNode) { + var resolvedUri = resolveResource(moduleKey, resourceUri); + return context.getResourceManager().read(resolvedUri, readNode).orElse(null); + } + + private URI resolveResource(ModuleKey moduleKey, String resourceUri) { + URI parsedUri; + try { + parsedUri = IoUtils.toUri(resourceUri); + } catch (URISyntaxException e) { + throw exceptionBuilder() + .evalError("invalidResourceUri", resourceUri) + .withHint(e.getReason()) + .build(); + } + + var context = VmContext.get(this); + URI resolvedUri; + try { + resolvedUri = IoUtils.resolve(context.getSecurityManager(), moduleKey, parsedUri); + } catch (FileNotFoundException e) { + throw exceptionBuilder().evalError("cannotFindResource", resourceUri).build(); + } catch (URISyntaxException e) { + throw exceptionBuilder() + .evalError("invalidResourceUri", resourceUri) + .withHint(e.getReason()) + .build(); + } catch (IOException e) { + throw exceptionBuilder() + .evalError("ioErrorReadingResource", resourceUri) + .withHint(e.getMessage()) + .build(); + } catch (PackageLoadError | SecurityManagerException e) { + throw exceptionBuilder().withCause(e).build(); + } + + if (!resolvedUri.isAbsolute()) { + throw exceptionBuilder().evalError("cannotHaveRelativeResource", moduleKey.getUri()).build(); + } + return resolvedUri; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportGlobNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportGlobNode.java new file mode 100644 index 00000000..7f416c85 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportGlobNode.java @@ -0,0 +1,129 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.member.UntypedObjectMemberNode; +import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmMapping; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.GlobResolver; +import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; +import org.pkl.core.util.GlobResolver.ResolvedGlobElement; +import org.pkl.core.util.LateInit; + +@NodeInfo(shortName = "import*") +public class ImportGlobNode extends AbstractImportNode { + private final VmLanguage language; + + private final ResolvedModuleKey currentModule; + + private final String globPattern; + + @CompilationFinal @LateInit private VmMapping importedMapping; + + public ImportGlobNode( + VmLanguage language, + SourceSection sourceSection, + ResolvedModuleKey currentModule, + URI importUri, + String globPattern) { + super(sourceSection, importUri); + this.language = language; + this.currentModule = currentModule; + this.globPattern = globPattern; + } + + @TruffleBoundary + private EconomicMap buildMembers( + FrameDescriptor frameDescriptor, List uris) { + var members = EconomicMaps.create(); + for (var entry : uris) { + var readNode = + new ImportNode( + language, VmUtils.unavailableSourceSection(), currentModule, entry.getUri()); + var member = + new ObjectMember( + VmUtils.unavailableSourceSection(), + VmUtils.unavailableSourceSection(), + VmModifier.ENTRY, + null, + ""); + var memberNode = new UntypedObjectMemberNode(language, frameDescriptor, member, readNode); + member.initMemberNode(memberNode); + EconomicMaps.put(members, entry.getPath(), member); + } + return members; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (importedMapping == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + var context = VmContext.get(this); + try { + var moduleKey = context.getModuleResolver().resolve(importUri); + var securityManager = VmContext.get(this).getSecurityManager(); + if (!moduleKey.isGlobbable()) { + throw exceptionBuilder() + .evalError("cannotGlobUri", importUri, importUri.getScheme()) + .build(); + } + var uris = + GlobResolver.resolveGlob( + securityManager, + moduleKey, + currentModule.getOriginal(), + currentModule.getUri(), + globPattern); + var members = buildMembers(frame.getFrameDescriptor(), uris); + importedMapping = + new VmMapping( + frame.materialize(), BaseModule.getMappingClass().getPrototype(), members); + } catch (IOException e) { + throw exceptionBuilder().evalError("ioErrorResolvingGlob", importUri).withCause(e).build(); + } catch (SecurityManagerException e) { + throw exceptionBuilder().withCause(e).build(); + } catch (PackageLoadError e) { + throw exceptionBuilder().adhocEvalError(e.getMessage()).build(); + } catch (InvalidGlobPatternException e) { + throw exceptionBuilder() + .evalError("invalidGlobPattern", globPattern) + .withHint(e.getMessage()) + .build(); + } + } + return importedMapping; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportNode.java new file mode 100644 index 00000000..cffe6461 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ImportNode.java @@ -0,0 +1,70 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import java.net.URI; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.util.LateInit; + +@NodeInfo(shortName = "import") +public final class ImportNode extends AbstractImportNode { + private final VmLanguage language; + private final ResolvedModuleKey currentModule; + + @CompilationFinal @LateInit private VmTyped importedModule; + + public ImportNode( + VmLanguage language, + SourceSection sourceSection, + ResolvedModuleKey currentModule, + URI importUri) { + super(sourceSection, importUri); + this.language = language; + this.currentModule = currentModule; + + assert importUri.isAbsolute(); + } + + public URI getImportUri() { + return importUri; + } + + public Object executeGeneric(VirtualFrame frame) { + if (importedModule == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + var context = VmContext.get(this); + try { + context.getSecurityManager().checkImportModule(currentModule.getUri(), importUri); + var moduleToImport = context.getModuleResolver().resolve(importUri, this); + importedModule = language.loadModule(moduleToImport, this); + } catch (SecurityManagerException | PackageLoadError e) { + throw exceptionBuilder().withCause(e).build(); + } + } + + return importedModule; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/LogicalNotNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/LogicalNotNode.java new file mode 100644 index 00000000..cc4c5fbf --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/LogicalNotNode.java @@ -0,0 +1,32 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; + +@NodeInfo(shortName = "!") +public abstract class LogicalNotNode extends UnaryExpressionNode { + protected LogicalNotNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected boolean eval(boolean operand) { + return !operand; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/NonNullNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/NonNullNode.java new file mode 100644 index 00000000..a2cf0c33 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/NonNullNode.java @@ -0,0 +1,46 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmNull; + +@NodeInfo(shortName = "!!") +// Truffle DSL/codegen is overkill for this node, hence don't extend UnaryExpressionNode +public final class NonNullNode extends ExpressionNode { + private @Child ExpressionNode operandNode; + + public NonNullNode(SourceSection sourceSection, ExpressionNode operandNode) { + super(sourceSection); + this.operandNode = operandNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + var operand = operandNode.executeGeneric(frame); + if (!(operand instanceof VmNull)) return operand; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("expectedNonNullValue") + .withSourceSection(operandNode.getSourceSection()) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/NullPropagatingOperationNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/NullPropagatingOperationNode.java new file mode 100644 index 00000000..f78917b9 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/NullPropagatingOperationNode.java @@ -0,0 +1,42 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.NullReceiverException; +import org.pkl.core.runtime.VmNull; + +@NodeInfo(shortName = "?.") +public final class NullPropagatingOperationNode extends ExpressionNode { + @Child private ExpressionNode expressionNode; + + public NullPropagatingOperationNode(SourceSection sourceSection, ExpressionNode expressionNode) { + super(sourceSection); + this.expressionNode = expressionNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + try { + return expressionNode.executeGeneric(frame); + } catch (NullReceiverException e) { + return VmNull.withoutDefault(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/PropagateNullReceiverNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/PropagateNullReceiverNode.java new file mode 100644 index 00000000..6f4d6cd0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/PropagateNullReceiverNode.java @@ -0,0 +1,42 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.NullReceiverException; +import org.pkl.core.runtime.VmNull; + +@NodeInfo(shortName = "?.") +public abstract class PropagateNullReceiverNode extends UnaryExpressionNode { + protected PropagateNullReceiverNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + @SuppressWarnings("UnusedParameters") + protected Object eval(VmNull value) { + throw NullReceiverException.INSTANCE; + } + + @Fallback + @Override + protected Object fallback(Object notNull) { + return notNull; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobNode.java new file mode 100644 index 00000000..9161083a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadGlobNode.java @@ -0,0 +1,111 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.member.UntypedObjectMemberNode; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmMapping; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.GlobResolver.ResolvedGlobElement; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.LateInit; + +@NodeInfo(shortName = "read*") +public abstract class ReadGlobNode extends UnaryExpressionNode { + private final VmLanguage language; + private final ModuleKey currentModule; + + @CompilationFinal @LateInit VmMapping readResult; + + protected ReadGlobNode( + VmLanguage language, SourceSection sourceSection, ModuleKey currentModule) { + super(sourceSection); + this.currentModule = currentModule; + this.language = language; + } + + @TruffleBoundary + private URI doResolveUri(String globExpression) { + try { + var globUri = IoUtils.toUri(globExpression); + var tripleDotImport = IoUtils.parseTripleDotPath(globUri); + if (tripleDotImport != null) { + throw exceptionBuilder().evalError("cannotGlobTripleDots").build(); + } + return globUri; + } catch (URISyntaxException e) { + throw exceptionBuilder() + .evalError("invalidResourceUri", globExpression) + .withHint(e.getReason()) + .build(); + } + } + + @TruffleBoundary + private EconomicMap buildMembers( + FrameDescriptor frameDescriptor, List uris) { + var members = EconomicMaps.create(); + for (var entry : uris) { + var readNode = new StaticReadNode(entry.getUri()); + var member = + new ObjectMember( + VmUtils.unavailableSourceSection(), + VmUtils.unavailableSourceSection(), + VmModifier.ENTRY, + null, + ""); + var memberNode = new UntypedObjectMemberNode(language, frameDescriptor, member, readNode); + member.initMemberNode(memberNode); + EconomicMaps.put(members, entry.getPath(), member); + } + return members; + } + + @Specialization + public Object read(VirtualFrame frame, String globPattern) { + if (readResult == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + var context = VmContext.get(this); + var resolvedUri = doResolveUri(globPattern); + var uris = + context + .getResourceManager() + .resolveGlob(resolvedUri, currentModule.getUri(), currentModule, this, globPattern); + var members = buildMembers(frame.getFrameDescriptor(), uris); + readResult = + new VmMapping(frame.materialize(), BaseModule.getMappingClass().getPrototype(), members); + } + return readResult; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadNode.java new file mode 100644 index 00000000..1112ccbe --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadNode.java @@ -0,0 +1,39 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.runtime.VmContext; + +@NodeInfo(shortName = "read") +public abstract class ReadNode extends AbstractReadNode { + protected ReadNode(SourceSection sourceSection, ModuleKey moduleKey) { + super(sourceSection, moduleKey); + } + + @Specialization + public Object read(String resourceUri) { + var result = doRead(resourceUri, VmContext.get(this), this); + if (result != null) return result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("cannotFindResource", resourceUri).build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadOrNullNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadOrNullNode.java new file mode 100644 index 00000000..12886967 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadOrNullNode.java @@ -0,0 +1,36 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.runtime.VmNull; + +@NodeInfo(shortName = "read?") +public abstract class ReadOrNullNode extends AbstractReadNode { + protected ReadOrNullNode(SourceSection sourceSection, ModuleKey moduleKey) { + super(sourceSection, moduleKey); + } + + @Specialization + public Object read(String resourceUri) { + var result = doRead(resourceUri, VmContext.get(this), this); + return result != null ? result : VmNull.withoutDefault(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadOrNullStdLibNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadOrNullStdLibNode.java new file mode 100644 index 00000000..3ac4ef07 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ReadOrNullStdLibNode.java @@ -0,0 +1,40 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.runtime.VmNull; + +/** + * Stdlib nodes are shared between engines. As such they can't use a cached [VmContextReference], + * which is bound to a single engine. + */ +@NodeInfo(shortName = "read?") +public abstract class ReadOrNullStdLibNode extends AbstractReadNode { + protected ReadOrNullStdLibNode(SourceSection sourceSection, ModuleKey moduleKey) { + super(sourceSection, moduleKey); + } + + @Specialization + public Object read(String resourceUri) { + var result = doRead(resourceUri, VmContext.get(this), this); + return result != null ? result : VmNull.withoutDefault(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/StaticReadNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/StaticReadNode.java new file mode 100644 index 00000000..eb98934d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/StaticReadNode.java @@ -0,0 +1,50 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import java.net.URI; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.LateInit; + +/** Used by {@link ReadGlobNode}. */ +public class StaticReadNode extends UnaryExpressionNode { + private final URI resourceUri; + + @CompilationFinal @LateInit private Object readResult; + + public StaticReadNode(URI resourceUri) { + super(VmUtils.unavailableSourceSection()); + assert resourceUri.isAbsolute(); + this.resourceUri = resourceUri; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (readResult == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + var context = VmContext.get(this); + readResult = context.getResourceManager().read(resourceUri, this).orElse(null); + if (readResult == null) { + throw exceptionBuilder().evalError("cannotFindResource", resourceUri).build(); + } + } + return readResult; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ThrowNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ThrowNode.java new file mode 100644 index 00000000..d2ccd177 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/ThrowNode.java @@ -0,0 +1,34 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; + +@NodeInfo(shortName = "throw") +public abstract class ThrowNode extends UnaryExpressionNode { + protected ThrowNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + @TruffleBoundary + protected Void eval(String message) { + throw exceptionBuilder().adhocEvalError(message).build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/TraceNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/TraceNode.java new file mode 100644 index 00000000..17bbf076 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/unary/TraceNode.java @@ -0,0 +1,59 @@ +/** + * 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.ast.expression.unary; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; + +public final class TraceNode extends ExpressionNode { + @Child private ExpressionNode valueNode; + + private final VmValueRenderer renderer = VmValueRenderer.singleLine(1000000); + + public TraceNode(SourceSection sourceSection, ExpressionNode valueNode) { + super(sourceSection); + this.valueNode = valueNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + var value = valueNode.executeGeneric(frame); + doTrace(value, VmContext.get(this)); + return value; + } + + @TruffleBoundary + private void doTrace(Object value, VmContext context) { + if (value instanceof VmObjectLike) { + try { + ((VmObjectLike) value).force(true, true); + } catch (VmException ignored) { + } + } + + var sourceSection = valueNode.getSourceSection(); + var renderedValue = renderer.render(value); + var message = + (sourceSection.isAvailable() ? sourceSection.getCharacters() : " 0 : "should be using ReadFrameSlotNode for levelsUp == 0"; + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected long evalInt(VirtualFrame frame) throws FrameSlotTypeException { + return getCapturedFrame(frame).getLong(slot); + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected double evalFloat(VirtualFrame frame) throws FrameSlotTypeException { + return getCapturedFrame(frame).getDouble(slot); + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected boolean evalBoolean(VirtualFrame frame) throws FrameSlotTypeException { + return getCapturedFrame(frame).getBoolean(slot); + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected Object evalObject(VirtualFrame frame) throws FrameSlotTypeException { + return getCapturedFrame(frame).getObject(slot); + } + + @Specialization(replaces = {"evalInt", "evalFloat", "evalBoolean", "evalObject"}) + protected Object evalGeneric(VirtualFrame frame) { + return getCapturedFrame(frame).getValue(slot); + } + + // could be factored out into a separate node s.t. it + // won't be repeated in case of FrameSlotTypeException + @ExplodeLoop + protected final MaterializedFrame getCapturedFrame(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + for (var i = 0; i < levelsUp - 1; i++) { + owner = owner.getEnclosingOwner(); + assert owner != null; // guaranteed by AstBuilder + } + return owner.getEnclosingFrame(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadFrameSlotNode.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadFrameSlotNode.java new file mode 100644 index 00000000..a0ed8303 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadFrameSlotNode.java @@ -0,0 +1,57 @@ +/** + * 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.ast.frame; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameSlotTypeException; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; + +public abstract class ReadFrameSlotNode extends ExpressionNode { + + private final int slot; + + protected ReadFrameSlotNode(SourceSection sourceSection, int slot) { + super(sourceSection); + this.slot = slot; + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected long evalInt(VirtualFrame frame) throws FrameSlotTypeException { + return frame.getLong(slot); + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected double evalFloat(VirtualFrame frame) throws FrameSlotTypeException { + return frame.getDouble(slot); + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected boolean evalBoolean(VirtualFrame frame) throws FrameSlotTypeException { + return frame.getBoolean(slot); + } + + @Specialization(rewriteOn = FrameSlotTypeException.class) + protected Object evalObject(VirtualFrame frame) throws FrameSlotTypeException { + return frame.getObject(slot); + } + + @Specialization(replaces = {"evalInt", "evalFloat", "evalBoolean", "evalObject"}) + protected Object evalGeneric(VirtualFrame frame) { + return frame.getValue(slot); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteAuxiliarySlotNode.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteAuxiliarySlotNode.java new file mode 100644 index 00000000..50bcaee6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteAuxiliarySlotNode.java @@ -0,0 +1,31 @@ +/** + * 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.ast.frame; + +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.PklNode; + +public class WriteAuxiliarySlotNode extends PklNode { + private final int slot; + + public WriteAuxiliarySlotNode(int slot) { + this.slot = slot; + } + + public void evalGeneric(VirtualFrame frame, Object value) { + frame.setAuxiliarySlot(slot, value); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteFrameSlotNode.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteFrameSlotNode.java new file mode 100644 index 00000000..304d5828 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/WriteFrameSlotNode.java @@ -0,0 +1,81 @@ +/** + * 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.ast.frame; + +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameSlotKind; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; + +// modeled after: +// https://github.com/oracle/graal/blob/93c461734f70a37458312b1d5e6d6e5bb26dd757/truffle/src/com.oracle.truffle.sl/src/com/oracle/truffle/sl/nodes/local/SLWriteLocalVariableNode.java +@NodeChild(value = "valueNode", type = ExpressionNode.class) +public abstract class WriteFrameSlotNode extends ExpressionNode { + + private final int slot; + + public WriteFrameSlotNode(SourceSection sourceSection, int slot) { + super(sourceSection); + this.slot = slot; + } + + public abstract void executeWithValue(VirtualFrame frame, Object value); + + @Specialization(guards = "isIntOrIllegal(frame)") + protected long evalInt(VirtualFrame frame, long value) { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Long); + frame.setLong(slot, value); + return value; + } + + @Specialization(guards = "isFloatOrIllegal(frame)") + protected double evalFloat(VirtualFrame frame, double value) { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Double); + frame.setDouble(slot, value); + return value; + } + + @Specialization(guards = "isBooleanOrIllegal(frame)") + protected boolean evalBoolean(VirtualFrame frame, boolean value) { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Boolean); + frame.setBoolean(slot, value); + return value; + } + + @Specialization(replaces = {"evalInt", "evalFloat", "evalBoolean"}) + protected Object evalGeneric(VirtualFrame frame, Object value) { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Object); + frame.setObject(slot, value); + return value; + } + + protected final boolean isIntOrIllegal(VirtualFrame frame) { + var kind = frame.getFrameDescriptor().getSlotKind(slot); + return kind == FrameSlotKind.Long || kind == FrameSlotKind.Illegal; + } + + protected final boolean isFloatOrIllegal(VirtualFrame frame) { + var kind = frame.getFrameDescriptor().getSlotKind(slot); + return kind == FrameSlotKind.Double || kind == FrameSlotKind.Illegal; + } + + protected final boolean isBooleanOrIllegal(VirtualFrame frame) { + var kind = frame.getFrameDescriptor().getSlotKind(slot); + return kind == FrameSlotKind.Boolean || kind == FrameSlotKind.Illegal; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/package-info.java new file mode 100644 index 00000000..65fdd20c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.frame; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/BlackholeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/BlackholeNode.java new file mode 100644 index 00000000..7b0b4878 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/BlackholeNode.java @@ -0,0 +1,50 @@ +/** + * 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.ast.internal; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.util.Nullable; + +/** Ensures that `childNode` isn't optimized away. */ +@NodeChild(value = "childNode", type = ExpressionNode.class) +public abstract class BlackholeNode extends ExpressionNode { + @Specialization + protected @Nullable Object evalBoolean(boolean value) { + CompilerDirectives.blackhole(value); + return null; + } + + @Specialization + protected @Nullable Object evalInt(long value) { + CompilerDirectives.blackhole(value); + return null; + } + + @Specialization + protected @Nullable Object evalFloat(double value) { + CompilerDirectives.blackhole(value); + return null; + } + + @Specialization + protected @Nullable Object evalObject(Object value) { + CompilerDirectives.blackhole(value); + return null; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/GetBaseModuleClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/GetBaseModuleClassNode.java new file mode 100644 index 00000000..b4df60d1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/GetBaseModuleClassNode.java @@ -0,0 +1,45 @@ +/** + * 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.ast.internal; + +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.module.ModuleKeys; +import org.pkl.core.runtime.*; + +public final class GetBaseModuleClassNode extends ExpressionNode { + private final Identifier className; + + public GetBaseModuleClassNode(Identifier className) { + this.className = className; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + var current = VmUtils.getOwner(frame); + var parent = current.getEnclosingOwner(); + while (parent != null) { + current = parent; + parent = parent.getEnclosingOwner(); + } + + assert ModuleKeys.isBaseModule(VmUtils.getModuleInfo(current).getModuleKey()); + + var result = VmUtils.readMember(current, className); + assert result instanceof VmClass; + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/GetClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/GetClassNode.java new file mode 100644 index 00000000..d57e6773 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/GetClassNode.java @@ -0,0 +1,79 @@ +/** + * 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.ast.internal; + +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; + +// NOTE: needs to be kept in sync with VmUtils.getClass() +@NodeChild(value = "valueNode", type = ExpressionNode.class) +public abstract class GetClassNode extends ExpressionNode { + protected GetClassNode(SourceSection sourceSection) { + super(sourceSection); + } + + protected GetClassNode() { + super(); + } + + /** + * When only using this execute method, pass `null` for `valueNode` to {@link + * GetClassNodeGen#create}. + */ + public abstract VmClass executeWith(VirtualFrame frame, Object value); + + @Specialization + protected VmClass evalString(@SuppressWarnings("unused") String value) { + return BaseModule.getStringClass(); + } + + @Specialization + protected VmClass evalInt(@SuppressWarnings("unused") long value) { + return BaseModule.getIntClass(); + } + + @Specialization + protected VmClass evalFloat(@SuppressWarnings("unused") double value) { + return BaseModule.getFloatClass(); + } + + @Specialization + protected VmClass evalBoolean(@SuppressWarnings("unused") boolean value) { + return BaseModule.getBooleanClass(); + } + + // This method effectively covers `VmValue value` but is implemented in a more efficient way. + // See: + // https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces + @Specialization(guards = "value.getClass() == cachedClass", limit = "99") + protected VmClass evalVmValue( + Object value, @Cached("getValueClass(value)") Class cachedClass) { + return cachedClass.cast(value).getVmClass(); + } + + protected static Class getValueClass(Object value) { + // OK to perform slow cast here (not a guard). + // `value instanceof VmValue` is guaranteed because `evalVmValue()` + // - comes after String/long/double/boolean specializations + // - has a guard that triggers per-class respecialization + return ((VmValue) value).getClass(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/IsInstanceOfNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/IsInstanceOfNode.java new file mode 100644 index 00000000..a28e5139 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/IsInstanceOfNode.java @@ -0,0 +1,73 @@ +/** + * 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.ast.internal; + +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmValue; + +public abstract class IsInstanceOfNode extends PklNode { + public abstract boolean executeBoolean(Object value, VmClass clazz); + + @Specialization + protected boolean eval(@SuppressWarnings("unused") String left, VmClass right) { + return right == BaseModule.getStringClass() || right == BaseModule.getAnyClass(); + } + + @Specialization + protected boolean eval(@SuppressWarnings("unused") long left, VmClass right) { + return right == BaseModule.getIntClass() + || right == BaseModule.getNumberClass() + || right == BaseModule.getAnyClass(); + } + + @Specialization + protected boolean eval(@SuppressWarnings("unused") double left, VmClass right) { + return right == BaseModule.getFloatClass() + || right == BaseModule.getNumberClass() + || right == BaseModule.getAnyClass(); + } + + @Specialization + protected boolean eval(@SuppressWarnings("unused") boolean left, VmClass right) { + return right == BaseModule.getBooleanClass() || right == BaseModule.getAnyClass(); + } + + /** + * This method effectively covers `VmValue value` but is implemented in a more efficient way. See: + * https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces + */ + @Specialization(guards = "value.getClass() == valueJavaClass", limit = "99") + protected boolean evalVmValue( + Object value, + VmClass vmClass, + @Cached("getJavaClass(value)") Class valueJavaClass) { + + VmClass valueVmClass = valueJavaClass.cast(value).getVmClass(); + return vmClass.isSuperclassOf(valueVmClass); + } + + protected Class getJavaClass(Object value) { + // OK to perform slow cast here (not a guard). + // `value instanceof VmValue` is guaranteed because `evalVmValue()` + // - comes after String/long/double/boolean specializations + // - has a guard that triggers per-class respecialization + return ((VmValue) value).getClass(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/ToStringNode.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/ToStringNode.java new file mode 100644 index 00000000..6c2a4465 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/ToStringNode.java @@ -0,0 +1,85 @@ +/** + * 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.ast.internal; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberLookupMode; +import org.pkl.core.ast.expression.member.InvokeMethodVirtualNode; +import org.pkl.core.ast.expression.member.InvokeMethodVirtualNodeGen; +import org.pkl.core.ast.expression.unary.UnaryExpressionNode; +import org.pkl.core.runtime.*; + +@SuppressWarnings("unused") +public abstract class ToStringNode extends UnaryExpressionNode { + protected ToStringNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Specialization + protected String evalString(String value) { + return value; + } + + @Specialization + @TruffleBoundary + protected String evalInt(long value) { + return String.valueOf(value); + } + + @Specialization + @TruffleBoundary + protected String evalFloat(double value) { + return String.valueOf(value); + } + + @Specialization + protected String evalBoolean(boolean value) { + return String.valueOf(value); + } + + @Specialization + protected String evalTyped( + VirtualFrame frame, + VmTyped value, + @Cached("createInvokeNode()") InvokeMethodVirtualNode invokeNode) { + + return (String) invokeNode.executeWith(frame, value, value.getVmClass()); + } + + @Fallback + @Override + @TruffleBoundary + protected Object fallback(Object value) { + return value.toString(); + } + + protected InvokeMethodVirtualNode createInvokeNode() { + //noinspection ConstantConditions + return InvokeMethodVirtualNodeGen.create( + sourceSection, + Identifier.TO_STRING, + new ExpressionNode[] {}, + MemberLookupMode.EXPLICIT_RECEIVER, + null, + null); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/internal/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/internal/package-info.java new file mode 100644 index 00000000..dd580308 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/internal/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.internal; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction0Node.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction0Node.java new file mode 100644 index 00000000..d37a0e3d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction0Node.java @@ -0,0 +1,44 @@ +/** + * 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.ast.lambda; + +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.VmFunction; + +public abstract class ApplyVmFunction0Node extends PklNode { + public abstract Object execute(VmFunction function); + + @Specialization(guards = "function.getCallTarget() == cachedCallTarget") + protected Object evalDirect( + VmFunction function, + @SuppressWarnings("unused") @Cached("function.getCallTarget()") + RootCallTarget cachedCallTarget, + @Cached("create(cachedCallTarget)") DirectCallNode callNode) { + + return callNode.call(function.getThisValue(), function); + } + + @Specialization(replaces = "evalDirect") + protected Object eval(VmFunction function, @Cached("create()") IndirectCallNode callNode) { + + return callNode.call(function.getCallTarget(), function.getThisValue(), function); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction1Node.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction1Node.java new file mode 100644 index 00000000..102e3733 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction1Node.java @@ -0,0 +1,89 @@ +/** + * 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.ast.lambda; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.VmCollection; +import org.pkl.core.runtime.VmFunction; + +@NodeChild("functionNode") +@NodeChild("argumentNode") +public abstract class ApplyVmFunction1Node extends ExpressionNode { + public abstract Object execute(VmFunction function, Object arg1); + + public static ApplyVmFunction1Node create() { + //noinspection ConstantConditions + return ApplyVmFunction1NodeGen.create(null, null); + } + + public final boolean executeBoolean(VmFunction function, Object arg1) { + var result = execute(function, arg1); + if (result instanceof Boolean) return (Boolean) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getBooleanClass()).build(); + } + + public final String executeString(VmFunction function, Object arg1) { + var result = execute(function, arg1); + if (result instanceof String) return (String) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getStringClass()).build(); + } + + public final Long executeInt(VmFunction function, Object arg1) { + var result = execute(function, arg1); + if (result instanceof Long) return (Long) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getIntClass()).build(); + } + + public final VmCollection executeCollection(VmFunction function, Object arg1) { + var result = execute(function, arg1); + if (result instanceof VmCollection) return (VmCollection) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getCollectionClass()).build(); + } + + @Specialization(guards = "function.getCallTarget() == cachedCallTarget") + protected Object evalDirect( + VmFunction function, + Object arg1, + @SuppressWarnings("unused") @Cached("function.getCallTarget()") + RootCallTarget cachedCallTarget, + @Cached("create(cachedCallTarget)") DirectCallNode callNode) { + + return callNode.call(function.getThisValue(), function, arg1); + } + + @Specialization(replaces = "evalDirect") + protected Object eval( + VmFunction function, Object arg1, @Cached("create()") IndirectCallNode callNode) { + + return callNode.call(function.getCallTarget(), function.getThisValue(), function, arg1); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction2Node.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction2Node.java new file mode 100644 index 00000000..bf8222dd --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction2Node.java @@ -0,0 +1,93 @@ +/** + * 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.ast.lambda; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.*; + +public abstract class ApplyVmFunction2Node extends PklNode { + public abstract Object execute(VmFunction function, Object arg1, Object arg2); + + public final boolean executeBoolean(VmFunction function, Object arg1, Object arg2) { + var result = execute(function, arg1, arg2); + if (result instanceof Boolean) return (Boolean) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getBooleanClass()).build(); + } + + public final VmCollection executeCollection(VmFunction function, Object arg1, Object arg2) { + var result = execute(function, arg1, arg2); + if (result instanceof VmCollection) return (VmCollection) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getCollectionClass()).build(); + } + + public final VmMap executeMap(VmFunction function, Object arg1, Object arg2) { + var result = execute(function, arg1, arg2); + if (result instanceof VmMap) return (VmMap) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getMapClass()).build(); + } + + public final Long executeInt(VmFunction function, Object arg1, Object arg2) { + var result = execute(function, arg1, arg2); + if (result instanceof Long) return (Long) result; + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getIntClass()).build(); + } + + public final VmPair executePair(VmFunction function, Object arg1, Object arg2) { + var result = execute(function, arg1, arg2); + if (result instanceof VmPair) { + return (VmPair) result; + } + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().typeMismatch(result, BaseModule.getPairClass()).build(); + } + + @Specialization(guards = "function.getCallTarget() == cachedCallTarget") + protected Object evalDirect( + VmFunction function, + Object arg1, + Object arg2, + @SuppressWarnings("unused") @Cached("function.getCallTarget()") + RootCallTarget cachedCallTarget, + @Cached("create(cachedCallTarget)") DirectCallNode callNode) { + + return callNode.call(function.getThisValue(), function, arg1, arg2); + } + + @Specialization(replaces = "evalDirect") + protected Object eval( + VmFunction function, + Object arg1, + Object arg2, + @Cached("create()") IndirectCallNode callNode) { + + return callNode.call(function.getCallTarget(), function.getThisValue(), function, arg1, arg2); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction3Node.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction3Node.java new file mode 100644 index 00000000..55ebe910 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction3Node.java @@ -0,0 +1,53 @@ +/** + * 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.ast.lambda; + +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.VmFunction; + +public abstract class ApplyVmFunction3Node extends PklNode { + public abstract Object execute(VmFunction function, Object arg1, Object arg2, Object arg3); + + @Specialization(guards = "function.getCallTarget() == cachedCallTarget") + protected Object evalDirect( + VmFunction function, + Object arg1, + Object arg2, + Object arg3, + @SuppressWarnings("unused") @Cached("function.getCallTarget()") + RootCallTarget cachedCallTarget, + @Cached("create(cachedCallTarget)") DirectCallNode callNode) { + + return callNode.call(function.getThisValue(), function, arg1, arg2, arg3); + } + + @Specialization(replaces = "evalDirect") + protected Object eval( + VmFunction function, + Object arg1, + Object arg2, + Object arg3, + @Cached("create()") IndirectCallNode callNode) { + + return callNode.call( + function.getCallTarget(), function.getThisValue(), function, arg1, arg2, arg3); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction4Node.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction4Node.java new file mode 100644 index 00000000..81ea2d85 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction4Node.java @@ -0,0 +1,56 @@ +/** + * 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.ast.lambda; + +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.VmFunction; + +public abstract class ApplyVmFunction4Node extends PklNode { + public abstract Object execute( + VmFunction function, Object arg1, Object arg2, Object arg3, Object arg4); + + @Specialization(guards = "function.getCallTarget() == cachedCallTarget") + protected Object evalDirect( + VmFunction function, + Object arg1, + Object arg2, + Object arg3, + Object arg4, + @SuppressWarnings("unused") @Cached("function.getCallTarget()") + RootCallTarget cachedCallTarget, + @Cached("create(cachedCallTarget)") DirectCallNode callNode) { + + return callNode.call(function.getThisValue(), function, arg1, arg2, arg3, arg4); + } + + @Specialization(replaces = "evalDirect") + protected Object eval( + VmFunction function, + Object arg1, + Object arg2, + Object arg3, + Object arg4, + @Cached("create()") IndirectCallNode callNode) { + + return callNode.call( + function.getCallTarget(), function.getThisValue(), function, arg1, arg2, arg3, arg4); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction5Node.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction5Node.java new file mode 100644 index 00000000..cfa3fb7e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/ApplyVmFunction5Node.java @@ -0,0 +1,58 @@ +/** + * 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.ast.lambda; + +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.VmFunction; + +public abstract class ApplyVmFunction5Node extends PklNode { + public abstract Object execute( + VmFunction function, Object arg1, Object arg2, Object arg3, Object arg4, Object arg5); + + @Specialization(guards = "function.getCallTarget() == cachedCallTarget") + protected Object evalDirect( + VmFunction function, + Object arg1, + Object arg2, + Object arg3, + Object arg4, + Object arg5, + @SuppressWarnings("unused") @Cached("function.getCallTarget()") + RootCallTarget cachedCallTarget, + @Cached("create(cachedCallTarget)") DirectCallNode callNode) { + + return callNode.call(function.getThisValue(), function, arg1, arg2, arg3, arg4, arg5); + } + + @Specialization(replaces = "evalDirect") + protected Object eval( + VmFunction function, + Object arg1, + Object arg2, + Object arg3, + Object arg4, + Object arg5, + @Cached("create()") IndirectCallNode callNode) { + + return callNode.call( + function.getCallTarget(), function.getThisValue(), function, arg1, arg2, arg3, arg4, arg5); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/lambda/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/lambda/package-info.java new file mode 100644 index 00000000..f8b5d6c5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/lambda/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.lambda; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassMember.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassMember.java new file mode 100644 index 00000000..9a17ee0e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassMember.java @@ -0,0 +1,64 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.util.Nullable; + +public abstract class ClassMember extends Member { + protected final @Nullable SourceSection docComment; + protected final List annotations; + // store prototype instead of class because the former is needed much more often + private final VmTyped owner; + + public ClassMember( + SourceSection sourceSection, + SourceSection headerSection, + int modifiers, + Identifier name, + String qualifiedName, + @Nullable SourceSection docComment, + List annotations, + VmTyped owner) { + + super(sourceSection, headerSection, modifiers, name, qualifiedName); + + this.docComment = docComment; + this.annotations = annotations; + this.owner = owner; + } + + public final @Nullable SourceSection getDocComment() { + return docComment; + } + + public final List getAnnotations() { + return annotations; + } + + /** Returns the prototype of the class that declares this member. */ + public final VmTyped getOwner() { + return owner; + } + + public final VmClass getDeclaringClass() { + return owner.getVmClass(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassMethod.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassMethod.java new file mode 100644 index 00000000..d492e988 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassMethod.java @@ -0,0 +1,132 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.pkl.core.PClass; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class ClassMethod extends ClassMember { + private final List typeParameters; + + // null = not deprecated, "" = no/empty message in the @Deprecated body + private final @Nullable String deprecation; + + @CompilationFinal private FunctionNode functionNode; + + public ClassMethod( + SourceSection sourceSection, + SourceSection headerSection, + int modifiers, + Identifier name, + String qualifiedName, + @Nullable SourceSection docComment, + List annotations, + VmTyped owner, + List typeParameters, + @Nullable String deprecation) { + + super( + sourceSection, + headerSection, + modifiers, + name, + qualifiedName, + docComment, + annotations, + owner); + this.typeParameters = typeParameters; + this.deprecation = deprecation; + } + + public void initFunctionNode(FunctionNode functionNode) { + assert this.functionNode == null; + this.functionNode = functionNode; + } + + public CallTarget getCallTarget() { + return functionNode.getCallTarget(); + } + + @TruffleBoundary + private void reportDeprecation(SourceSection callSite) { + assert deprecation != null; + + var logger = VmContext.get(null).getLogger(); + logger.warn( + "Method `" + + qualifiedName + + "` is deprecated" + + (deprecation.isEmpty() ? "" : ": " + deprecation), + VmUtils.createStackFrame(callSite, null)); + } + + public CallTarget getCallTarget(SourceSection callSite) { + if (deprecation != null) { + reportDeprecation(callSite); + } + return functionNode.getCallTarget(); + } + + public int getParameterCount() { + return functionNode.getParameterCount(); + } + + public @Nullable TypeNode getReturnTypeNode() { + return functionNode.getReturnTypeNode(); + } + + @Override + public String getCallSignature() { + return functionNode.getCallSignature(); + } + + public VmTyped getMirror() { + return MirrorFactories.methodFactory.create(this); + } + + public VmSet getModifierMirrors() { + return VmModifier.getMirrors(modifiers, false); + } + + public VmList getTypeParameterMirrors() { + var builder = VmList.EMPTY.builder(); + for (var typeParameter : typeParameters) { + builder.add(MirrorFactories.typeParameterFactory.create(typeParameter)); + } + return builder.build(); + } + + public VmMap getParameterMirrors() { + return functionNode.getParameterMirrors(); + } + + public VmTyped getReturnTypeMirror() { + return functionNode.getReturnTypeMirror(); + } + + public PClass.Method export(PClass owner) { + return functionNode.export(owner, docComment, annotations, modifiers, typeParameters); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java new file mode 100644 index 00000000..817cb48a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java @@ -0,0 +1,203 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import com.oracle.truffle.api.source.SourceSection; +import java.util.ArrayList; +import java.util.List; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.PClassInfo; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +@NodeInfo(shortName = "class") +public final class ClassNode extends ExpressionNode { + private final SourceSection headerSection; + private final @Nullable SourceSection docComment; + @Children private final ExpressionNode[] annotationNodes; + private final int modifiers; + private final PClassInfo classInfo; + private final List typeParameters; + private final @Nullable ModuleInfo moduleInfo; + // null iff this class is pkl.base#Any + @Child private @Nullable UnresolvedTypeNode unresolvedSupertypeNode; + private final EconomicMap prototypeMembers; + @Children private final UnresolvedPropertyNode[] unresolvedPropertyNodes; + @Children private final UnresolvedMethodNode[] unresolvedMethodNodes; + + @CompilationFinal @LateInit private VmClass cachedClass; + + public ClassNode( + SourceSection section, + SourceSection headerSection, + @Nullable SourceSection docComment, + ExpressionNode[] annotationNodes, + int modifiers, + PClassInfo classInfo, + List typeParameters, + @Nullable ModuleInfo moduleInfo, + @Nullable UnresolvedTypeNode unresolvedSupertypeNode, + EconomicMap prototypeMembers, + UnresolvedPropertyNode[] unresolvedPropertyNodes, + UnresolvedMethodNode[] unresolvedMethodNodes) { + + super(section); + this.headerSection = headerSection; + this.docComment = docComment; + this.annotationNodes = annotationNodes; + this.modifiers = modifiers; + this.classInfo = classInfo; + this.typeParameters = typeParameters; + this.moduleInfo = moduleInfo; + this.unresolvedSupertypeNode = unresolvedSupertypeNode; + this.prototypeMembers = prototypeMembers; + this.unresolvedPropertyNodes = unresolvedPropertyNodes; + this.unresolvedMethodNodes = unresolvedMethodNodes; + } + + @Override + public VmClass executeGeneric(VirtualFrame frame) { + // Break class resolution cycles by immediately returning the + // (possibly not yet fully initialized) cached class instance on subsequent calls. + // Caching of classes also guarantees that classes are singletons and can be compared by + // identity, + // which improves efficiency and performance (for example in shape checks). + if (cachedClass != null) return cachedClass; + + CompilerDirectives.transferToInterpreter(); + + var module = VmUtils.getTypedObjectReceiver(frame); + + VmTyped prototype; + var isModuleClass = moduleInfo != null; + if (isModuleClass) { + // For module classes, the corresponding (uninitialized) module object is provided by the + // caller of this frame. + // This allows to conveniently make stdlib module objects and their members accessible to + // nodes + // via static final fields without having to fear recursive field initialization. + prototype = module; + prototype.setExtraStorage(moduleInfo); + prototype.addProperties(prototypeMembers); + } else { + prototype = + new VmTyped( + frame.materialize(), + null, // initialized later by VmClass + null, // initialized later by Vmclass + prototypeMembers); + } + + var annotations = new ArrayList(annotationNodes.length); + if (moduleInfo != null) moduleInfo.initAnnotations(annotations); + + // Cache the (not yet fully initialized) class before making any `resolveXYZ()` + // or `node.execute()` calls as those may result in recursive calls to this method. + cachedClass = + new VmClass( + sourceSection, + headerSection, + docComment, + annotations, + modifiers, + classInfo, + typeParameters, + prototype); + + if (unresolvedSupertypeNode != null) { + var supertypeNode = unresolvedSupertypeNode.execute(frame); + var superclass = supertypeNode.getVmClass(); + + checkSupertype(supertypeNode, superclass); + cachedClass.initSupertype(supertypeNode, superclass); + } + + // The superclass resolved above may not itself have completed the below initializations yet. + // That's because these initializations may have indirectly or directly triggered + // resolution of this class, in which case the `resolveSuperclass()` call above + // will have returned the partially initialized `cachedClass` of the superclass. + // As a consequence, initializations that require a fully initialized class hierarchy + // are done lazily in VmClass rather than here. + // A fully initialized class hierarchy is only required for initialization of internal caches, + // which is guaranteed to succeed (no impact on eager vs. lazy error reporting) and easy to + // defer. + + VmUtils.evaluateAnnotations(frame, annotationNodes, annotations); + + for (var node : unresolvedPropertyNodes) { + cachedClass.addProperty(node.execute(frame, cachedClass)); + } + + for (var node : unresolvedMethodNodes) { + cachedClass.addMethod(node.execute(frame, cachedClass)); + } + + cachedClass.notifyInitialized(); + + return cachedClass; + } + + private void checkSupertype(TypeNode supertypeNode, @Nullable VmClass superclass) { + if (superclass == null) { + throw exceptionBuilder() + .evalError("invalidSupertype", supertypeNode.getSourceSection().getCharacters()) + .withSourceSection(supertypeNode.getSourceSection()) + .build(); + } + if (moduleInfo != null) { + if (cachedClass == superclass) { + throw exceptionBuilder() + .evalError("moduleCannotExtendSelf", moduleInfo.getModuleName()) + .withSourceSection(supertypeNode.getSourceSection()) + .build(); + } + if (superclass.isClosed()) { + throw exceptionBuilder() + .evalError("cannotExtendFinalModule", superclass.getModuleName()) + .withSourceSection(supertypeNode.getSourceSection()) + .build(); + } + } else { + if (cachedClass == superclass) { + throw exceptionBuilder() + .evalError("classCannotExtendSelf", superclass.getDisplayName()) + .withSourceSection(supertypeNode.getSourceSection()) + .build(); + } + if (superclass.isClosed()) { + throw exceptionBuilder() + .evalError("cannotExtendFinalClass", superclass.getDisplayName()) + .withSourceSection(supertypeNode.getSourceSection()) + .build(); + } + if (superclass.isExternal() && !classInfo.isStandardLibraryClass()) { + throw new VmExceptionBuilder() + .evalError("cannotExtendExternalClass", superclass.getDisplayName()) + .withSourceSection(supertypeNode.getSourceSection()) + .build(); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java new file mode 100644 index 00000000..8ec25bef --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java @@ -0,0 +1,93 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.pkl.core.Member.SourceLocation; +import org.pkl.core.PClass; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class ClassProperty extends ClassMember { + private final @Nullable PropertyTypeNode typeNode; + private final ObjectMember initializer; + + public ClassProperty( + SourceSection sourceSection, + SourceSection headerSection, + int modifiers, + Identifier name, + String qualifiedName, + @Nullable SourceSection docComment, + List annotations, + VmTyped owner, + @Nullable PropertyTypeNode typeNode, + ObjectMember initializer) { + + super( + sourceSection, + headerSection, + modifiers, + name, + qualifiedName, + docComment, + annotations, + owner); + + this.typeNode = typeNode; + this.initializer = initializer; + } + + public @Nullable PropertyTypeNode getTypeNode() { + return typeNode; + } + + public ObjectMember getInitializer() { + return initializer; + } + + @Override + public String getCallSignature() { + assert name != null; + return name.toString(); + } + + public VmTyped getMirror() { + return MirrorFactories.propertyFactory.create(this); + } + + public VmSet getModifierMirrors() { + return VmModifier.getMirrors(modifiers, false); + } + + public VmTyped getTypeMirror() { + return PropertyTypeNode.getMirror(typeNode); + } + + public PClass.Property export(PClass owner) { + assert name != null; + return new PClass.Property( + owner, + VmUtils.exportDocComment(docComment), + new SourceLocation(getHeaderSection().getStartLine(), sourceSection.getEndLine()), + VmModifier.export(modifiers, false), + VmUtils.exportAnnotations(annotations), + name.toString(), + PropertyTypeNode.export(typeNode)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/DefaultPropertyBodyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/DefaultPropertyBodyNode.java new file mode 100644 index 00000000..24e9a49e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/DefaultPropertyBodyNode.java @@ -0,0 +1,57 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; + +/** + * Property body for properties that don't have an explicit body. Returns the default value for the + * property's type, or throws if the type doesn't have a default value. + */ +public final class DefaultPropertyBodyNode extends ExpressionNode { + private final Identifier propertyName; + private final @Nullable PropertyTypeNode typeNode; + + public DefaultPropertyBodyNode( + SourceSection sourceSection, Identifier propertyName, @Nullable PropertyTypeNode typeNode) { + super(sourceSection); + this.propertyName = propertyName; + this.typeNode = typeNode; + } + + public boolean isUndefined() { + return typeNode == null || typeNode.getDefaultValue() == null; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (typeNode != null) { + var defaultValue = typeNode.getDefaultValue(); + if (defaultValue != null) return defaultValue; + } + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .undefinedPropertyValue(propertyName, VmUtils.getReceiver(frame)) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageMapOrParentNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageMapOrParentNode.java new file mode 100644 index 00000000..1fa70a55 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageMapOrParentNode.java @@ -0,0 +1,38 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmMap; +import org.pkl.core.runtime.VmUtils; + +/** Delegates to the equally named key of the map stored in extra storage. */ +public final class DelegateToExtraStorageMapOrParentNode extends ExpressionNode { + @Override + public Object executeGeneric(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + var delegate = (VmMap) owner.getExtraStorage(); + var memberKey = VmUtils.getMemberKey(frame); + var mapKey = memberKey instanceof Identifier ? memberKey.toString() : memberKey; + var result = delegate.getOrNull(mapKey); + if (result != null) return result; + var parent = owner.getParent(); + assert parent != null; + return VmUtils.readMember(parent, memberKey); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageObjNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageObjNode.java new file mode 100644 index 00000000..2a198df5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageObjNode.java @@ -0,0 +1,32 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmObjectLike; +import org.pkl.core.runtime.VmUtils; + +/** Delegates to the equally named member of the object stored in extra storage. */ +public final class DelegateToExtraStorageObjNode extends ExpressionNode { + @Override + public Object executeGeneric(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + var delegate = (VmObjectLike) owner.getExtraStorage(); + var memberKey = VmUtils.getMemberKey(frame); + return VmUtils.readMember(delegate, memberKey); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageObjOrParentNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageObjOrParentNode.java new file mode 100644 index 00000000..58719e53 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/DelegateToExtraStorageObjOrParentNode.java @@ -0,0 +1,39 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.VmObjectLike; +import org.pkl.core.runtime.VmUtils; + +/** + * Delegates to the equally named member of the object stored in extra storage. If no such member + * exists, delegates to its own parent member. + */ +public final class DelegateToExtraStorageObjOrParentNode extends ExpressionNode { + @Override + public Object executeGeneric(VirtualFrame frame) { + var owner = VmUtils.getOwner(frame); + var delegate = (VmObjectLike) owner.getExtraStorage(); + var memberKey = VmUtils.getMemberKey(frame); + var result = VmUtils.readMemberOrNull(delegate, memberKey); + if (result != null) return result; + var parent = owner.getParent(); + assert parent != null; + return VmUtils.readMember(parent, memberKey); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java new file mode 100644 index 00000000..31c7d879 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java @@ -0,0 +1,201 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.pkl.core.Member.SourceLocation; +import org.pkl.core.PClass; +import org.pkl.core.PType; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.VmTypeMismatchException; +import org.pkl.core.runtime.*; +import org.pkl.core.util.CollectionUtils; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; + +public final class FunctionNode extends MemberNode { + // Every function (and property) call passes two implicit arguments at positions + // frame.getArguments()[0] and [1]: + // - the receiver (target) of the call, of type Object (see VmUtils.getReceiver()) + // - the owner (lexically enclosing object) of the function/property definition, of type VmTyped + // (see VmUtils.getOwner()) + // For VmObject receivers, the owner is the same as or an ancestor of the receiver. + // For other receivers, the owner is the prototype of the receiver's class. + // The chain of enclosing owners forms a function/property's lexical scope. + private static final int IMPLICIT_PARAM_COUNT = 2; + + private final int paramCount; + private final int totalParamCount; + + @Children private final TypeNode[] parameterTypeNodes; + @Child private @Nullable TypeNode checkedReturnTypeNode; + private @Nullable TypeNode returnTypeNode; + + @TruffleBoundary + public FunctionNode( + VmLanguage language, + FrameDescriptor descriptor, + Member member, + int paramCount, + TypeNode[] parameterTypeNodes, + @Nullable TypeNode returnTypeNode, + boolean isReturnTypeChecked, + ExpressionNode bodyNode) { + + super(language, descriptor, member, bodyNode); + + assert member instanceof ClassMethod + || member instanceof ObjectMember // local object method + || member instanceof Lambda; + + this.paramCount = paramCount; + this.parameterTypeNodes = parameterTypeNodes; + this.checkedReturnTypeNode = isReturnTypeChecked ? returnTypeNode : null; + this.returnTypeNode = returnTypeNode; + + totalParamCount = Math.addExact(IMPLICIT_PARAM_COUNT, paramCount); + } + + public int getParameterCount() { + return paramCount; + } + + public @Nullable TypeNode getReturnTypeNode() { + return returnTypeNode; + } + + @TruffleBoundary + public String getCallSignature() { + var sb = new StringBuilder(member.getName().toString()); + sb.append('('); + for (var i = 0; i < Math.min(getFrameDescriptor().getNumberOfSlots(), paramCount); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(getFrameDescriptor().getSlotName(i)); + } + sb.append(')'); + return sb.toString(); + } + + @Override + @ExplodeLoop + public Object execute(VirtualFrame frame) { + var totalArgCount = frame.getArguments().length; + if (totalArgCount != totalParamCount) { + CompilerDirectives.transferToInterpreter(); + throw wrongArgumentCount(totalArgCount - IMPLICIT_PARAM_COUNT); + } + + try { + for (var i = 0; i < parameterTypeNodes.length; i++) { + var argument = frame.getArguments()[IMPLICIT_PARAM_COUNT + i]; + parameterTypeNodes[i].executeAndSet(frame, argument); + } + + var result = bodyNode.executeGeneric(frame); + + if (checkedReturnTypeNode != null) { + checkedReturnTypeNode.execute(frame, result); + } + + return result; + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw e.toVmException(); + } catch (StackOverflowError e) { + CompilerDirectives.transferToInterpreter(); + throw new VmStackOverflowException(e); + } catch (Exception e) { + CompilerDirectives.transferToInterpreter(); + if (e instanceof VmException) { + throw e; + } else { + throw exceptionBuilder().bug(e.getMessage()).withCause(e).build(); + } + } + } + + public VmMap getParameterMirrors() { + var builder = VmMap.builder(); + for (var i = 0; i < paramCount; i++) { + var parameterName = getFrameDescriptor().getSlotName(i).toString(); + builder.add( + parameterName, + MirrorFactories.methodParameterFactory.create( + Pair.of(parameterName, parameterTypeNodes[i].getMirror()))); + } + return builder.build(); + } + + public VmTyped getReturnTypeMirror() { + return TypeNode.getMirror(returnTypeNode); + } + + public PClass.Method export( + PClass owner, + @Nullable SourceSection docComment, + List annotations, + int modifiers, + List typeParameters) { + + var parameters = CollectionUtils.newLinkedHashMap(paramCount); + for (var i = 0; i < paramCount; i++) { + var slotName = getFrameDescriptor().getSlotName(i); + // Ignored parameters (`_`) have no name + var paramName = slotName == null ? "_#" + i : slotName.toString(); + parameters.put(paramName, TypeNode.export(parameterTypeNodes[i])); + } + + var result = + new PClass.Method( + owner, + VmUtils.exportDocComment(docComment), + new SourceLocation(getHeaderSection().getStartLine(), getSourceSection().getEndLine()), + VmModifier.export(modifiers, false), + VmUtils.exportAnnotations(annotations), + member.getName().toString(), + typeParameters, + parameters, + TypeNode.export(returnTypeNode)); + + for (var parameter : typeParameters) { + // works because export() is called just once per FunctionNode (because PClass is cached) + parameter.initOwner(result); + } + + return result; + } + + private VmException wrongArgumentCount(int argCount) { + assert argCount != paramCount; + + return exceptionBuilder() + .evalError("wrongFunctionArgumentCount", paramCount, argCount) + .withSourceSection(member.getHeaderSection()) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/Lambda.java b/pkl-core/src/main/java/org/pkl/core/ast/member/Lambda.java new file mode 100644 index 00000000..5461bd5a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/Lambda.java @@ -0,0 +1,31 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.util.Nullable; + +public class Lambda extends Member { + public Lambda(SourceSection sourceSection, String qualifiedName) { + super(sourceSection, sourceSection, VmModifier.NONE, null, qualifiedName); + } + + @Override + public @Nullable String getCallSignature() { + return null; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/LocalTypedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/LocalTypedPropertyNode.java new file mode 100644 index 00000000..39a564e7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/LocalTypedPropertyNode.java @@ -0,0 +1,84 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.ast.type.VmTypeMismatchException; +import org.pkl.core.runtime.VmException; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +public final class LocalTypedPropertyNode extends MemberNode { + private final VmLanguage language; + @Child private UnresolvedTypeNode unresolvedTypeNode; + @Child @LateInit private TypeNode typeNode; + private @Nullable Object defaultValue; + private boolean defaultValueInitialized; + + public LocalTypedPropertyNode( + VmLanguage language, + FrameDescriptor descriptor, + ObjectMember member, + ExpressionNode bodyNode, + UnresolvedTypeNode unresolvedTypeNode) { + + super(language, descriptor, member, bodyNode); + + this.language = language; + this.unresolvedTypeNode = unresolvedTypeNode; + } + + public @Nullable Object getDefaultValue() { + if (!defaultValueInitialized) { + defaultValue = + typeNode.createDefaultValue( + language, member.getHeaderSection(), member.getQualifiedName()); + defaultValueInitialized = true; + } + return defaultValue; + } + + @Override + public Object execute(VirtualFrame frame) { + try { + if (typeNode == null) { + CompilerDirectives.transferToInterpreter(); + typeNode = insert(unresolvedTypeNode.execute(frame)); + unresolvedTypeNode = null; + } + var result = bodyNode.executeGeneric(frame); + typeNode.execute(frame, result); + return result; + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw e.toVmException(); + } catch (Exception e) { + CompilerDirectives.transferToInterpreter(); + if (e instanceof VmException) { + throw e; + } else { + throw exceptionBuilder().bug(e.getMessage()).withCause(e).build(); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java b/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java new file mode 100644 index 00000000..b64b97ac --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java @@ -0,0 +1,147 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.util.Nullable; + +public abstract class Member { + protected final SourceSection sourceSection; + + protected final SourceSection headerSection; + + protected final int modifiers; + + protected final @Nullable Identifier name; + + protected final String qualifiedName; + + public Member( + SourceSection sourceSection, + SourceSection headerSection, + int modifiers, + @Nullable Identifier name, + String qualifiedName) { + + this.sourceSection = sourceSection; + this.headerSection = headerSection; + this.modifiers = modifiers; + this.name = name; + this.qualifiedName = qualifiedName; + } + + public final SourceSection getSourceSection() { + return sourceSection; + } + + public final SourceSection getHeaderSection() { + return headerSection; + } + + public final int getModifiers() { + return modifiers; + } + + /** Null for members that don't have a name, such as listing/mapping members and lambdas. */ + public @Nullable Identifier getNameOrNull() { + return name; + } + + public Identifier getName() { + assert name != null; + return name; + } + + /** For use in user-facing messages. May contain placeholders for computed name parts. */ + public final String getQualifiedName() { + return qualifiedName; + } + + /** For use in user-facing messages. Non-null iff getName() is non-null. */ + public abstract @Nullable String getCallSignature(); + + @TruffleBoundary + public final boolean isLocal() { + return VmModifier.isLocal(modifiers); + } + + @TruffleBoundary + public final boolean isConst() { + return VmModifier.isConst(modifiers); + } + + @TruffleBoundary + public final boolean isFixed() { + return VmModifier.isFixed(modifiers); + } + + @TruffleBoundary + public final boolean isHidden() { + return VmModifier.isHidden(modifiers); + } + + @TruffleBoundary + public final boolean isExternal() { + return VmModifier.isExternal(modifiers); + } + + @TruffleBoundary + public final boolean isClass() { + return VmModifier.isClass(modifiers); + } + + @TruffleBoundary + public final boolean isTypeAlias() { + return VmModifier.isTypeAlias(modifiers); + } + + @TruffleBoundary + public final boolean isImport() { + return VmModifier.isImport(modifiers); + } + + public final boolean isGlob() { + return VmModifier.isGlob(modifiers); + } + + @TruffleBoundary + public final boolean isAbstract() { + return VmModifier.isAbstract(modifiers); + } + + @TruffleBoundary + public final boolean isType() { + return VmModifier.isType(modifiers); + } + + @TruffleBoundary + public final boolean isLocalOrExternalOrHidden() { + return VmModifier.isLocalOrExternalOrHidden(modifiers); + } + + @TruffleBoundary + public final boolean isConstOrFixed() { + return VmModifier.isConstOrFixed(modifiers); + } + + @TruffleBoundary + public final boolean isLocalOrExternalOrAbstract() { + return VmModifier.isLocalOrExternalOrAbstract(modifiers); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ModuleNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ModuleNode.java new file mode 100644 index 00000000..8d70ee9d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ModuleNode.java @@ -0,0 +1,64 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.PklRootNode; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmTyped; + +public final class ModuleNode extends PklRootNode { + private final SourceSection sourceSection; + private final String moduleName; + private @Child ExpressionNode moduleNode; + + public ModuleNode( + VmLanguage language, + SourceSection sourceSection, + String moduleName, + ExpressionNode moduleNode) { + + super(language, new FrameDescriptor()); + this.sourceSection = sourceSection; + this.moduleName = moduleName; + this.moduleNode = moduleNode; + } + + @Override + public SourceSection getSourceSection() { + return sourceSection; + } + + @Override + public String getName() { + return moduleName; + } + + @Override + public Object execute(VirtualFrame frame) { + var module = executeBody(frame, moduleNode); + if (module instanceof VmClass) { + return ((VmClass) module).getPrototype(); + } + + assert module instanceof VmTyped; + return module; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java new file mode 100644 index 00000000..19c427f2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java @@ -0,0 +1,153 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.RootCallTarget; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ConstantNode; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmDynamic; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; + +public final class ObjectMember extends Member { + @CompilationFinal private @Nullable Object constantValue; + @CompilationFinal private @Nullable MemberNode memberNode; + + public ObjectMember( + SourceSection sourceSection, + SourceSection headerSection, + int modifiers, + @Nullable Identifier name, + String qualifiedName) { + + super(sourceSection, headerSection, modifiers, name, qualifiedName); + } + + public void initConstantValue(ConstantNode node) { + initConstantValue(node.getValue()); + } + + public void initConstantValue(Object value) { + assert constantValue == null; + assert memberNode == null; + + constantValue = value; + } + + public void initMemberNode(MemberNode node) { + assert constantValue == null; + assert memberNode == null; + + memberNode = node; + } + + /** + * Tells if this member is a property. + * + *

Not named `isProperty()` to work around https://bugs.openjdk.java.net/browse/JDK-8185424 + * (which is apparently triggered by `-Xdoclint:none`). + */ + public boolean isProp() { + return name != null; + } + + /** Tells if this member is an element. */ + public boolean isElement() { + return VmModifier.isElement(modifiers); + } + + /** + * Tells if this member is an entry. Note that this returns true if an existing element is + * overridden with entry syntax (e.g., `[3] = ...`). + */ + public boolean isEntry() { + return VmModifier.isEntry(modifiers); + } + + public @Nullable Object getConstantValue() { + return constantValue; + } + + public @Nullable MemberNode getMemberNode() { + return memberNode; + } + + public RootCallTarget getCallTarget() { + assert constantValue == null : "Must not call getCallTarget() if constantValue is non-null."; + + assert getMemberNode() != null + : "Either constantValue or memberNode must be set, but both are null."; + + var callTarget = getMemberNode().getCallTarget(); + assert callTarget != null; + return callTarget; + } + + public boolean isUndefined() { + return getMemberNode() != null && getMemberNode().isUndefined(); + } + + public @Nullable Object getLocalPropertyDefaultValue() { + assert isProp() && isLocal(); + return getMemberNode() instanceof LocalTypedPropertyNode + ? ((LocalTypedPropertyNode) getMemberNode()).getDefaultValue() + : VmDynamic.empty(); + } + + @Override + public @Nullable String getCallSignature() { + return name != null ? name.toString() : null; + } + + public SourceSection getBodySection() { + if (getMemberNode() != null) { + return getMemberNode().getBodySection(); + } + + var source = sourceSection.getSource(); + var start = headerSection.getCharEndIndex(); + var offset = start - sourceSection.getCharIndex(); + var candidate = source.createSection(start, sourceSection.getCharLength() - offset); + if (candidate.getCharLength() == 0) { + // TODO: return null or candidate? + return VmUtils.unavailableSourceSection(); + } + + var skip = 0; + var text = candidate.getCharacters(); + var ch = text.charAt(skip); + while (ch == '=' || Character.isWhitespace(ch)) { + ch = text.charAt(++skip); + } + return source.createSection(candidate.getCharIndex() + skip, candidate.getCharLength() - skip); + } + + // sometimes used as key in VmObject.setCachedValue() + @Override + public boolean equals(@Nullable Object obj) { + return this == obj; + } + + // sometimes used as key in VmObject.setCachedValue() + @Override + public int hashCode() { + return System.identityHashCode(this); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMethodNode.java new file mode 100644 index 00000000..5bae1241 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMethodNode.java @@ -0,0 +1,87 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +public final class ObjectMethodNode extends MemberNode { + private final VmLanguage language; + private final int parameterCount; + @Children private final @Nullable UnresolvedTypeNode[] unresolvedParameterTypeNodes; + @Child private @Nullable UnresolvedTypeNode unresolvedReturnTypeNode; + + @CompilationFinal @LateInit private FunctionNode functionNode; + + public ObjectMethodNode( + VmLanguage language, + FrameDescriptor descriptor, + ObjectMember member, + ExpressionNode bodyNode, + int parameterCount, + @Nullable UnresolvedTypeNode[] unresolvedParameterTypeNodes, + @Nullable UnresolvedTypeNode unresolvedReturnTypeNode) { + + super(language, descriptor, member, bodyNode); + + this.language = language; + this.parameterCount = parameterCount; + this.unresolvedParameterTypeNodes = unresolvedParameterTypeNodes; + this.unresolvedReturnTypeNode = unresolvedReturnTypeNode; + } + + public @Nullable TypeNode getReturnTypeNode() { + // this method is only called from child nodes + assert functionNode != null; + return functionNode.getReturnTypeNode(); + } + + @Override + public CallTarget execute(VirtualFrame frame) { + if (functionNode == null) { + CompilerDirectives.transferToInterpreter(); + + var parameterTypeNodes = + VmUtils.resolveParameterTypes(frame, getFrameDescriptor(), unresolvedParameterTypeNodes); + + var returnTypeNode = + unresolvedReturnTypeNode != null ? unresolvedReturnTypeNode.execute(frame) : null; + + functionNode = + new FunctionNode( + language, + getFrameDescriptor(), + member, + parameterCount, + parameterTypeNodes, + returnTypeNode, + true, + bodyNode); + } + + return functionNode.getCallTarget(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/PropertyTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/PropertyTypeNode.java new file mode 100644 index 00000000..2edabf7f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/PropertyTypeNode.java @@ -0,0 +1,111 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.PType; +import org.pkl.core.ast.PklRootNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.TypeNode.UnknownTypeNode; +import org.pkl.core.ast.type.VmTypeMismatchException; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class PropertyTypeNode extends PklRootNode { + private final String qualifiedPropertyName; + @Child private TypeNode typeNode; + + private @Nullable Object defaultValue; + private boolean defaultValueInitialized; + + @TruffleBoundary + public PropertyTypeNode( + VmLanguage language, + FrameDescriptor descriptor, + String qualifiedPropertyName, + TypeNode childNode) { + + super(language, descriptor); + this.qualifiedPropertyName = qualifiedPropertyName; + this.typeNode = childNode; + } + + public TypeNode getTypeNode() { + return typeNode; + } + + @Override + public SourceSection getSourceSection() { + return typeNode.getSourceSection(); + } + + @Override + public String getName() { + return qualifiedPropertyName; + } + + @Override + public @Nullable Object execute(VirtualFrame frame) { + try { + typeNode.execute(frame, frame.getArguments()[2]); + return null; + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw e.toVmException(); + } catch (Exception e) { + CompilerDirectives.transferToInterpreter(); + if (e instanceof VmException) { + throw e; + } else { + throw exceptionBuilder().bug(e.getMessage()).withCause(e).build(); + } + } + } + + public @Nullable Object getDefaultValue() { + if (!defaultValueInitialized) { + defaultValue = + typeNode.createDefaultValue( + VmLanguage.get(this), getSourceSection(), qualifiedPropertyName); + defaultValueInitialized = true; + } + return defaultValue; + } + + public boolean isUnknownType() { + return typeNode instanceof UnknownTypeNode; + } + + public PType export() { + return TypeNode.export(typeNode); + } + + public VmTyped getMirror() { + return TypeNode.getMirror(typeNode); + } + + public static PType export(@Nullable PropertyTypeNode node) { + return node != null ? node.export() : PType.UNKNOWN; + } + + public static VmTyped getMirror(@Nullable PropertyTypeNode node) { + return node != null ? node.getMirror() : MirrorFactories.unknownTypeFactory.create(null); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/TypeAliasNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeAliasNode.java new file mode 100644 index 00000000..f3a6614c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeAliasNode.java @@ -0,0 +1,92 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import java.util.ArrayList; +import java.util.List; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.VmTypeAlias; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.util.Nullable; + +public final class TypeAliasNode extends ExpressionNode { + private final SourceSection headerSection; + private final @Nullable SourceSection docComment; + @Children private final ExpressionNode[] annotationNodes; + private final int modifiers; + private final String simpleName; + private final String qualifiedName; + private final List typeParameters; + private @Child UnresolvedTypeNode typeAnnotationNode; + + // use same caching scheme as ClassNode + @CompilationFinal private @Nullable VmTypeAlias cachedTypeAlias; + + public TypeAliasNode( + SourceSection sourceSection, + SourceSection headerSection, + @Nullable SourceSection docComment, + ExpressionNode[] annotationNodes, + int modifiers, + String simpleName, + String qualifiedName, + List typeParameters, + UnresolvedTypeNode typeAnnotationNode) { + super(sourceSection); + this.headerSection = headerSection; + this.docComment = docComment; + this.annotationNodes = annotationNodes; + this.modifiers = modifiers; + this.simpleName = simpleName; + this.qualifiedName = qualifiedName; + this.typeParameters = typeParameters; + this.typeAnnotationNode = typeAnnotationNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (cachedTypeAlias != null) return cachedTypeAlias; + + CompilerDirectives.transferToInterpreter(); + + var annotations = new ArrayList(); + var module = VmUtils.getTypedObjectReceiver(frame); + + cachedTypeAlias = + new VmTypeAlias( + getSourceSection(), + headerSection, + docComment, + modifiers, + annotations, + simpleName, + module, + qualifiedName, + typeParameters); + + VmUtils.evaluateAnnotations(frame, annotationNodes, annotations); + cachedTypeAlias.initTypeCheckNode(typeAnnotationNode.execute(frame)); + + return cachedTypeAlias; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/TypeCheckedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeCheckedPropertyNode.java new file mode 100644 index 00000000..c3014d90 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeCheckedPropertyNode.java @@ -0,0 +1,101 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Executed; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.ast.expression.primary.GetOwnerNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +/** A property definition that does not have a type annotation but should be type-checked. */ +public abstract class TypeCheckedPropertyNode extends MemberNode { + @Child @Executed protected ExpressionNode ownerNode = new GetOwnerNode(); + + protected TypeCheckedPropertyNode( + @Nullable VmLanguage language, + FrameDescriptor descriptor, + ObjectMember member, + ExpressionNode bodyNode) { + + super(language, descriptor, member, bodyNode); + + assert member.isProp(); + } + + @SuppressWarnings("unused") + @Specialization(guards = "owner.getVmClass() == cachedOwnerClass") + protected Object evalTypedObjectCached( + VirtualFrame frame, + VmTyped owner, + @Cached("owner.getVmClass()") VmClass cachedOwnerClass, + @Cached("getProperty(cachedOwnerClass)") ClassProperty property, + @Cached("createTypeCheckCallNode(property)") @Nullable DirectCallNode callNode) { + + var result = executeBody(frame); + + // TODO: propagate SUPER_CALL_MARKER to disable constraint (but not type) check + if (callNode != null && !shouldRunTypecheck(frame)) { + callNode.call(VmUtils.getReceiverOrNull(frame), property.getOwner(), result); + } + + return result; + } + + @Specialization(guards = "!owner.isDynamic()") + protected Object eval( + VirtualFrame frame, VmObjectLike owner, @Cached("create()") IndirectCallNode callNode) { + + var result = executeBody(frame); + + if (!shouldRunTypecheck(frame)) { + var property = getProperty(owner.getVmClass()); + var typeAnnNode = property.getTypeNode(); + if (typeAnnNode != null) { + callNode.call( + typeAnnNode.getCallTarget(), + VmUtils.getReceiverOrNull(frame), + property.getOwner(), + result); + } + } + + return result; + } + + @Specialization + protected Object eval(VirtualFrame frame, @SuppressWarnings("unused") VmDynamic owner) { + return executeBody(frame); + } + + protected ClassProperty getProperty(VmClass ownerClass) { + ClassProperty result = ownerClass.getProperty(member.getName()); + assert result != null; + return result; + } + + protected @Nullable DirectCallNode createTypeCheckCallNode(ClassProperty property) { + var typeCheckNode = property.getTypeNode(); + return typeCheckNode == null ? null : DirectCallNode.create(typeCheckNode.getCallTarget()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/TypedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/TypedPropertyNode.java new file mode 100644 index 00000000..495844a2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/TypedPropertyNode.java @@ -0,0 +1,54 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.DirectCallNode; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.runtime.VmUtils; + +/** A property definition that has a type annotation. */ +public final class TypedPropertyNode extends MemberNode { + @Child private DirectCallNode typeCheckCallNode; + + @TruffleBoundary + public TypedPropertyNode( + VmLanguage language, + FrameDescriptor descriptor, + ObjectMember member, + ExpressionNode bodyNode, + PropertyTypeNode typeNode) { + + super(language, descriptor, member, bodyNode); + + assert member.isProp(); + + typeCheckCallNode = DirectCallNode.create(typeNode.getCallTarget()); + } + + @Override + public Object execute(VirtualFrame frame) { + var propertyValue = executeBody(frame); + if (!shouldRunTypecheck(frame)) { + typeCheckCallNode.call(VmUtils.getReceiver(frame), VmUtils.getOwner(frame), propertyValue); + } + return propertyValue; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedClassMemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedClassMemberNode.java new file mode 100644 index 00000000..2bd9f318 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedClassMemberNode.java @@ -0,0 +1,65 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.util.Nullable; + +public abstract class UnresolvedClassMemberNode extends PklNode { + protected final SourceSection headerSection; + protected final VmLanguage language; + protected final FrameDescriptor descriptor; + protected final @Nullable SourceSection docComment; + protected final @Children ExpressionNode[] annotationNodes; + protected final int modifiers; + protected final Identifier name; + protected final String qualifiedName; + + public UnresolvedClassMemberNode( + VmLanguage language, + SourceSection sourceSection, + SourceSection headerSection, + FrameDescriptor descriptor, + @Nullable SourceSection docComment, + ExpressionNode[] annotationNodes, + int modifiers, + Identifier name, + String qualifiedName) { + + super(sourceSection); + this.headerSection = headerSection; + this.language = language; + this.descriptor = descriptor; + this.docComment = docComment; + this.annotationNodes = annotationNodes; + this.modifiers = modifiers; + this.name = name; + this.qualifiedName = qualifiedName; + } + + public abstract ClassMember execute(VirtualFrame frame, VmClass clazz); + + public String getQualifiedName() { + return qualifiedName; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedFunctionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedFunctionNode.java new file mode 100644 index 00000000..bf1f8348 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedFunctionNode.java @@ -0,0 +1,74 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class UnresolvedFunctionNode extends PklNode { + private final VmLanguage language; + private final FrameDescriptor descriptor; + private final Member member; + private final int parameterCount; + @Children private final @Nullable UnresolvedTypeNode[] unresolvedParameterTypeNodes; + @Child private @Nullable UnresolvedTypeNode unresolvedReturnTypeNode; + private final ExpressionNode bodyNode; + + public UnresolvedFunctionNode( + VmLanguage language, + FrameDescriptor descriptor, + Member member, + int parameterCount, + @Nullable UnresolvedTypeNode[] unresolvedParameterTypeNodes, + @Nullable UnresolvedTypeNode unresolvedReturnTypeNode, + ExpressionNode bodyNode) { + + super(member.getSourceSection()); + + this.language = language; + this.descriptor = descriptor; + this.member = member; + this.parameterCount = parameterCount; + this.unresolvedParameterTypeNodes = unresolvedParameterTypeNodes; + this.unresolvedReturnTypeNode = unresolvedReturnTypeNode; + this.bodyNode = bodyNode; + } + + public FunctionNode execute(VirtualFrame frame) { + CompilerAsserts.neverPartOfCompilation(); + + var parameterTypeNodes = + VmUtils.resolveParameterTypes(frame, descriptor, unresolvedParameterTypeNodes); + var returnTypeNode = + unresolvedReturnTypeNode != null ? unresolvedReturnTypeNode.execute(frame) : null; + + return new FunctionNode( + language, + descriptor, + member, + parameterCount, + parameterTypeNodes, + returnTypeNode, + true, + bodyNode); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedMethodNode.java new file mode 100644 index 00000000..5eba2f62 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedMethodNode.java @@ -0,0 +1,132 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class UnresolvedMethodNode extends UnresolvedClassMemberNode { + private final int parameterCount; + private final List typeParameters; + @Children private final @Nullable UnresolvedTypeNode[] unresolvedParameterTypeNodes; + @Child private @Nullable UnresolvedTypeNode unresolvedReturnTypeNode; + private final boolean isReturnTypeChecked; + private final ExpressionNode bodyNode; + + public UnresolvedMethodNode( + VmLanguage language, + SourceSection sourceSection, + SourceSection headerSection, + FrameDescriptor descriptor, + @Nullable SourceSection docComment, + ExpressionNode[] annotationNodes, + int modifiers, + Identifier name, + String qualifiedName, + int parameterCount, + List typeParameters, + @Nullable UnresolvedTypeNode[] unresolvedParameterTypeNodes, + @Nullable UnresolvedTypeNode unresolvedReturnTypeNode, + boolean isReturnTypeChecked, + ExpressionNode bodyNode) { + + super( + language, + sourceSection, + headerSection, + descriptor, + docComment, + annotationNodes, + modifiers, + name, + qualifiedName); + + this.parameterCount = parameterCount; + this.typeParameters = typeParameters; + this.unresolvedParameterTypeNodes = unresolvedParameterTypeNodes; + this.unresolvedReturnTypeNode = unresolvedReturnTypeNode; + this.isReturnTypeChecked = isReturnTypeChecked; + this.bodyNode = bodyNode; + } + + public Identifier getName() { + return name; + } + + public SourceSection getHeaderSection() { + return headerSection; + } + + public boolean isLocal() { + return VmModifier.isLocal(modifiers); + } + + @Override + public ClassMethod execute(VirtualFrame frame, VmClass clazz) { + CompilerDirectives.transferToInterpreter(); + + var annotations = VmUtils.evaluateAnnotations(frame, annotationNodes); + var parameterTypeNodes = + VmUtils.resolveParameterTypes(frame, descriptor, unresolvedParameterTypeNodes); + var returnTypeNode = + unresolvedReturnTypeNode != null ? unresolvedReturnTypeNode.execute(frame) : null; + + String deprecation = null; + for (var annotation : annotations) { + if (annotation.getVmClass() == BaseModule.getDeprecatedClass()) { + var messageObj = VmUtils.readMemberOrNull(annotation, Identifier.MESSAGE); + deprecation = messageObj instanceof String ? (String) messageObj : ""; + break; + } + } + + ClassMethod method = + new ClassMethod( + sourceSection, + headerSection, + modifiers, + name, + qualifiedName, + docComment, + annotations, + clazz.getPrototype(), + typeParameters, + deprecation); + + FunctionNode functionNode = + new FunctionNode( + language, + descriptor, + method, + parameterCount, + parameterTypeNodes, + returnTypeNode, + isReturnTypeChecked, + bodyNode); + + method.initFunctionNode(functionNode); + return method; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedPropertyNode.java new file mode 100644 index 00000000..050df2b1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedPropertyNode.java @@ -0,0 +1,204 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.type.UnresolvedTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.Nullable; + +public final class UnresolvedPropertyNode extends UnresolvedClassMemberNode { + private final SourceSection propertyNameSection; + private @Child @Nullable UnresolvedTypeNode unresolvedTypeNode; + private final @Nullable ExpressionNode bodyNode; + + public UnresolvedPropertyNode( + VmLanguage language, + SourceSection sourceSection, + SourceSection headerSection, + SourceSection propertyNameSection, + FrameDescriptor descriptor, + @Nullable SourceSection docComment, + ExpressionNode[] annotationNodes, + int modifiers, + Identifier name, + String qualifiedName, + @Nullable UnresolvedTypeNode unresolvedTypeNode, + @Nullable ExpressionNode bodyNode) { + + super( + language, + sourceSection, + headerSection, + descriptor, + docComment, + annotationNodes, + modifiers, + name, + qualifiedName); + this.propertyNameSection = propertyNameSection; + this.unresolvedTypeNode = unresolvedTypeNode; + this.bodyNode = bodyNode; + } + + public Identifier getName() { + return name; + } + + public SourceSection getHeaderSection() { + return headerSection; + } + + public boolean isLocal() { + return VmModifier.isLocal(modifiers); + } + + public boolean isClass() { + return VmModifier.isClass(modifiers); + } + + public boolean isTypeAlias() { + return VmModifier.isTypeAlias(modifiers); + } + + public boolean isImport() { + return VmModifier.isImport(modifiers); + } + + private void checkOverride(VmClass clazz) { + var superClass = clazz.getSuperclass(); + if (superClass == null) { + return; + } + var superProperty = superClass.getProperty(name); + if (superProperty == null) { + return; + } + var isFixed = VmModifier.isFixed(modifiers); + if (superProperty.isFixed() == isFixed) { + return; + } + CompilerDirectives.transferToInterpreter(); + if (superProperty.isFixed()) { + throw exceptionBuilder() + .withSourceSection(headerSection) + .evalError( + "missingFixedModifier", + name, + superClass.getQualifiedName(), + sourceSection.getCharacters()) + .build(); + } + var source = headerSection.getCharacters().toString(); + var fixedModifierIdx = source.indexOf("fixed"); + throw exceptionBuilder() + .withSourceSection( + headerSection + .getSource() + .createSection(headerSection.getCharIndex() + fixedModifierIdx, 5)) + .evalError("cannotApplyFixedModifier", name, superClass.getQualifiedName()) + .build(); + } + + private void checkConst(VmClass clazz) { + var superClass = clazz.getSuperclass(); + if (superClass == null) { + return; + } + var superProperty = superClass.getProperty(name); + if (superProperty == null) { + return; + } + var isConst = VmModifier.isConst(modifiers); + if (superProperty.isConst() == isConst) { + return; + } + CompilerDirectives.transferToInterpreter(); + + if (superProperty.isConst()) { + throw exceptionBuilder() + .withSourceSection(headerSection) + .evalError( + "missingConstModifier", + name, + superClass.getQualifiedName(), + sourceSection.getCharacters()) + .build(); + } + var source = headerSection.getCharacters().toString(); + var constModifierIdx = source.indexOf("const"); + throw exceptionBuilder() + .withSourceSection( + headerSection + .getSource() + .createSection(headerSection.getCharIndex() + constModifierIdx, 5)) + .evalError("cannotApplyConstModifier", name, superClass.getQualifiedName()) + .build(); + } + + @Override + public ClassProperty execute(VirtualFrame frame, VmClass clazz) { + CompilerDirectives.transferToInterpreter(); + + var annotations = VmUtils.evaluateAnnotations(frame, annotationNodes); + + var typeNode = + unresolvedTypeNode == null + ? null + : new PropertyTypeNode( + language, descriptor, qualifiedName, unresolvedTypeNode.execute(frame)); + + checkOverride(clazz); + checkConst(clazz); + + var effectiveBodyNode = + bodyNode != null + ? bodyNode + : + // use propertyNameSection as source section of implicit property default + // to improve stack traces signaling failed type check of such a default + new DefaultPropertyBodyNode(propertyNameSection, name, typeNode); + + var initializer = + VmUtils.createObjectProperty( + language, + sourceSection, + headerSection, + name, + qualifiedName, + descriptor, + modifiers, + effectiveBodyNode, + typeNode); + + return new ClassProperty( + sourceSection, + headerSection, + modifiers, + name, + qualifiedName, + docComment, + annotations, + clazz.getPrototype(), + typeNode, + initializer); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/UntypedObjectMemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/UntypedObjectMemberNode.java new file mode 100644 index 00000000..6aeed879 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/UntypedObjectMemberNode.java @@ -0,0 +1,39 @@ +/** + * 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.ast.member; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.util.Nullable; + +public final class UntypedObjectMemberNode extends MemberNode { + public UntypedObjectMemberNode( + @Nullable VmLanguage language, + FrameDescriptor descriptor, + ObjectMember member, + ExpressionNode bodyNode) { + + super(language, descriptor, member, bodyNode); + } + + @Override + public Object execute(VirtualFrame frame) { + return executeBody(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/member/package-info.java new file mode 100644 index 00000000..9639c970 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.member; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/package-info.java new file mode 100644 index 00000000..f846f332 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/repl/ResolveClassMemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/repl/ResolveClassMemberNode.java new file mode 100644 index 00000000..dc05887d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/repl/ResolveClassMemberNode.java @@ -0,0 +1,55 @@ +/** + * 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.ast.repl; + +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.PklRootNode; +import org.pkl.core.ast.member.UnresolvedClassMemberNode; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmLanguage; + +public final class ResolveClassMemberNode extends PklRootNode { + @Child private UnresolvedClassMemberNode unresolvedNode; + private final VmClass clazz; + + public ResolveClassMemberNode( + VmLanguage language, + FrameDescriptor descriptor, + UnresolvedClassMemberNode unresolvedNode, + VmClass clazz) { + + super(language, descriptor); + this.clazz = clazz; + this.unresolvedNode = unresolvedNode; + } + + @Override + public SourceSection getSourceSection() { + return unresolvedNode.getSourceSection(); + } + + @Override + public String getName() { + return unresolvedNode.getQualifiedName(); + } + + @Override + public Object execute(VirtualFrame frame) { + return unresolvedNode.execute(frame, clazz); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/repl/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/repl/package-info.java new file mode 100644 index 00000000..8632be47 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/repl/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.repl; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/GetParentForTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/GetParentForTypeNode.java new file mode 100644 index 00000000..be5727ac --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/GetParentForTypeNode.java @@ -0,0 +1,62 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.LateInit; + +/** Resolves `` to the type's default value in `new { ... }`. */ +public final class GetParentForTypeNode extends ExpressionNode { + @Child private UnresolvedTypeNode unresolvedTypeNode; + private final String qualifiedName; + + @CompilationFinal @LateInit Object defaultValue; + + public GetParentForTypeNode( + SourceSection sourceSection, UnresolvedTypeNode unresolvedTypeNode, String qualifiedName) { + super(sourceSection); + this.unresolvedTypeNode = unresolvedTypeNode; + this.qualifiedName = qualifiedName; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (defaultValue != null) return defaultValue; + + CompilerDirectives.transferToInterpreterAndInvalidate(); + + var typeNode = unresolvedTypeNode.execute(frame); + defaultValue = typeNode.createDefaultValue(VmLanguage.get(this), sourceSection, qualifiedName); + + if (defaultValue != null) { + unresolvedTypeNode = null; + return defaultValue; + } + + // try to produce a more specific error message than "cannotInstantiateType" + var clazz = typeNode.getVmClass(); + if (clazz != null) VmUtils.checkIsInstantiable(clazz, typeNode); + + throw exceptionBuilder() + .evalError("cannotInstantiateType", typeNode.getSourceSection().getCharacters()) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/IdentityMixinNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/IdentityMixinNode.java new file mode 100644 index 00000000..eb0889d2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/IdentityMixinNode.java @@ -0,0 +1,84 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.PklRootNode; +import org.pkl.core.runtime.VmException; +import org.pkl.core.runtime.VmLanguage; +import org.pkl.core.util.Nullable; + +/** Root node for a mixin used as default value for type `Mixin`. */ +public final class IdentityMixinNode extends PklRootNode { + private final SourceSection sourceSection; + private final String qualifiedName; + @Child private @Nullable TypeNode argumentTypeNode; + + public IdentityMixinNode( + VmLanguage language, + FrameDescriptor descriptor, + SourceSection sourceSection, + String qualifiedName, + @Nullable TypeNode argumentTypeNode) { + super(language, descriptor); + this.qualifiedName = qualifiedName; + this.sourceSection = sourceSection; + this.argumentTypeNode = argumentTypeNode; + } + + @Override + public SourceSection getSourceSection() { + return sourceSection; + } + + @Override + public String getName() { + return qualifiedName; + } + + @Override + public Object execute(VirtualFrame frame) { + var arguments = frame.getArguments(); + if (arguments.length != 3) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("wrongFunctionArgumentCount", 1, arguments.length - 2) + .withSourceSection(sourceSection) + .build(); + } + + try { + var argument = arguments[2]; + if (argumentTypeNode != null) { + argumentTypeNode.execute(frame, argument); + } + return argument; + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw e.toVmException(); + } catch (Exception e) { + CompilerDirectives.transferToInterpreter(); + if (e instanceof VmException) { + throw e; + } else { + throw exceptionBuilder().bug(e.getMessage()).withCause(e).build(); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java new file mode 100644 index 00000000..f2ee4958 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java @@ -0,0 +1,101 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmObjectLike; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.util.Nullable; + +public abstract class ResolveDeclaredTypeNode extends ExpressionNode { + @Child private IndirectCallNode callNode = IndirectCallNode.create(); + + protected ResolveDeclaredTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + protected VmTyped getEnclosingModule(VmObjectLike initialOwner) { + var curr = initialOwner; + var next = curr.getEnclosingOwner(); + + while (next != null) { + curr = next; + next = next.getEnclosingOwner(); + } + + assert curr.isModuleObject(); + return (VmTyped) curr; + } + + protected VmTyped getImport( + VmTyped module, Identifier importName, SourceSection importNameSection) { + assert importName.isLocalProp(); + + var member = module.getMember(importName); + if (member == null) { + throw exceptionBuilder() + .evalError("cannotFindModuleImport", importName) + .withSourceSection(importNameSection) + .build(); + } + + if (!member.isImport()) { + throw exceptionBuilder() + .evalError("notAModuleImport", importName) + .withSourceSection(importNameSection) + .build(); + } + + if (member.isGlob()) { + throw exceptionBuilder() + .evalError("notAType", importName) + .withSourceSection(importNameSection) + .build(); + } + + assert member.getConstantValue() == null; + var result = module.getCachedValue(importName); + if (result == null) { + result = callNode.call(member.getCallTarget(), module, module, importName); + module.setCachedValue(importName, result); + } + return (VmTyped) result; + } + + protected @Nullable Object getType( + VmTyped module, Identifier typeName, SourceSection typeNameSection) { + var member = module.getMember(typeName); + if (member == null) return null; + + if (!member.isType()) { + throw exceptionBuilder() + .evalError("notAType", typeName) + .withSourceSection(typeNameSection) + .build(); + } + + assert member.getConstantValue() == null; + var result = module.getCachedValue(typeName); + if (result == null) { + result = callNode.call(member.getCallTarget(), module, module, typeName); + module.setCachedValue(typeName, result); + } + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveQualifiedDeclaredTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveQualifiedDeclaredTypeNode.java new file mode 100644 index 00000000..1dce4ca0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveQualifiedDeclaredTypeNode.java @@ -0,0 +1,66 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; + +public final class ResolveQualifiedDeclaredTypeNode extends ResolveDeclaredTypeNode { + private final SourceSection moduleNameSection; + private final SourceSection typeNameSection; + private final Identifier moduleName; + private final Identifier typeName; + + public ResolveQualifiedDeclaredTypeNode( + SourceSection sourceSection, + SourceSection moduleNameSection, + SourceSection typeNameSection, + Identifier moduleName, + Identifier typeName) { + + super(sourceSection); + this.moduleNameSection = moduleNameSection; + this.typeNameSection = typeNameSection; + this.moduleName = moduleName; + this.typeName = typeName; + + assert moduleName.isLocalProp(); + assert typeName.isRegular(); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var enclosingModule = getEnclosingModule(VmUtils.getOwner(frame)); + var importedModule = getImport(enclosingModule, moduleName, moduleNameSection); + + // search module hierarchy + // (type declared in base module is accessible through extending and amending modules) + for (var currModule = importedModule; currModule != null; currModule = currModule.getParent()) { + var result = getType(currModule, typeName, sourceSection); + if (result != null) return result; + } + + throw exceptionBuilder() + .evalError( + "cannotFindQualifiedType", typeName, importedModule.getModuleInfo().getModuleName()) + .withSourceSection(typeNameSection) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveSimpleDeclaredTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveSimpleDeclaredTypeNode.java new file mode 100644 index 00000000..7e4b783c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveSimpleDeclaredTypeNode.java @@ -0,0 +1,66 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.runtime.*; + +public final class ResolveSimpleDeclaredTypeNode extends ResolveDeclaredTypeNode { + private final Identifier typeName; + private final boolean isBaseModule; + + public ResolveSimpleDeclaredTypeNode( + SourceSection sourceSection, Identifier typeName, boolean isBaseModule) { + + super(sourceSection); + this.typeName = typeName; + this.isBaseModule = isBaseModule; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var localTypeName = typeName.toLocalProperty(); + var enclosingModule = getEnclosingModule(VmUtils.getOwner(frame)); + + // search enclosing module for local class/type alias or module import + var result = getType(enclosingModule, localTypeName, sourceSection); + if (result != null) return result; + + // search module hierarchy + var currModule = enclosingModule; + do { + result = getType(currModule, typeName, sourceSection); + if (result != null) return result; + + // search base module (after enclosing module, before parent modules) + if (!isBaseModule && currModule == enclosingModule) { + result = getType(BaseModule.getModule(), typeName, sourceSection); + if (result != null) return result; + } + + currModule = currModule.getParent(); + } while (currModule != null); + + throw exceptionBuilder() + .evalError( + "cannotFindSimpleType", typeName, enclosingModule.getModuleInfo().getModuleName()) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeCastNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeCastNode.java new file mode 100644 index 00000000..87e77916 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeCastNode.java @@ -0,0 +1,57 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.util.LateInit; + +public final class TypeCastNode extends ExpressionNode { + @Child private ExpressionNode valueNode; + @Child private UnresolvedTypeNode unresolvedTypeNode; + @Child @LateInit private TypeNode typeNode; + + public TypeCastNode( + SourceSection sourceSection, + ExpressionNode valueNode, + UnresolvedTypeNode unresolvedTypeNode) { + super(sourceSection); + this.valueNode = valueNode; + this.unresolvedTypeNode = unresolvedTypeNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + if (typeNode == null) { + // don't compile unresolvedTypeNode.execute() + // invalidation is done by insert() + CompilerDirectives.transferToInterpreter(); + typeNode = insert(unresolvedTypeNode.execute(frame)); + unresolvedTypeNode = null; + } + + var value = valueNode.executeGeneric(frame); + try { + typeNode.execute(frame, value); + return value; + } catch (VmTypeMismatchException e) { + CompilerDirectives.transferToInterpreter(); + throw e.toVmException(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeConstraintNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeConstraintNode.java new file mode 100644 index 00000000..fc429d0c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeConstraintNode.java @@ -0,0 +1,93 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; +import org.pkl.core.ast.lambda.ApplyVmFunction1Node; +import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.VmFunction; +import org.pkl.core.runtime.VmUtils; + +@NodeChild(value = "bodyNode", type = ExpressionNode.class) +public abstract class TypeConstraintNode extends PklNode { + @CompilationFinal private int customThisSlot = -1; + + protected TypeConstraintNode(SourceSection sourceSection) { + super(sourceSection); + } + + public abstract void execute(VirtualFrame frame); + + public String export() { + return getSourceSection().getCharacters().toString(); + } + + @Specialization + protected void eval(VirtualFrame frame, boolean result) { + initConstraintSlot(frame); + + if (!result) { + throw new VmTypeMismatchException.Constraint( + sourceSection, frame.getAuxiliarySlot(customThisSlot)); + } + } + + @Specialization + protected void eval( + VirtualFrame frame, + VmFunction function, + @Cached("createApplyNode()") ApplyVmFunction1Node applyNode) { + initConstraintSlot(frame); + + var value = frame.getAuxiliarySlot(customThisSlot); + var result = applyNode.executeBoolean(function, value); + if (!result) { + throw new VmTypeMismatchException.Constraint(sourceSection, value); + } + } + + @Fallback + protected void fallback(Object object) { + // supplying a type constraint that's neither a boolean nor a function + // is always fatal (even within a union type), hence throw VmEvalException + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .typeMismatch(object, BaseModule.getBooleanClass(), BaseModule.getFunctionClass()) + .build(); + } + + protected static ApplyVmFunction1Node createApplyNode() { + return ApplyVmFunction1Node.create(); + } + + private void initConstraintSlot(VirtualFrame frame) { + if (customThisSlot == -1) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + // deferred until execution time s.t. nodes of inlined type aliases get the right frame slot + customThisSlot = VmUtils.findAuxiliarySlot(frame, CustomThisScope.FRAME_SLOT_ID); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java new file mode 100644 index 00000000..cf535162 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java @@ -0,0 +1,1940 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameSlotKind; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.LoopNode; +import com.oracle.truffle.api.source.SourceSection; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import org.pkl.core.PType; +import org.pkl.core.PType.StringLiteral; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.*; +import org.pkl.core.ast.builder.SymbolTable.CustomThisScope; +import org.pkl.core.ast.expression.primary.GetModuleNode; +import org.pkl.core.ast.frame.WriteFrameSlotNode; +import org.pkl.core.ast.frame.WriteFrameSlotNodeGen; +import org.pkl.core.ast.member.DefaultPropertyBodyNode; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.member.UntypedObjectMemberNode; +import org.pkl.core.runtime.*; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +public abstract class TypeNode extends PklNode { + + protected TypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + public boolean isNoopTypeCheck() { + return false; + } + + public abstract FrameSlotKind getFrameSlotKind(); + + /** + * Initializes this node's frame slot. Called if this node is a function/method parameter type. + * Kept separate from constructor so that {@link TypeAliasTypeNode} can initialize frame slot of + * its cloned child node. + */ + public abstract TypeNode initWriteSlotNode(int slot); + + /** + * Checks if `value` conforms to this type. If so, returns normally. Otherwise, throws a + * `VmTypeMismatchException`. + */ + public abstract void execute(VirtualFrame frame, Object value); + + /** + * Checks if `value` conforms to this type. If so, sets `slot` to `value`. Otherwise, throws a + * `VmTypeMismatchException`. + */ + public abstract void executeAndSet(VirtualFrame frame, Object value); + + // method arguments are used when default value contains a root node + public @Nullable Object createDefaultValue( + VmLanguage language, + // header section of the property or method that carries the type annotation + SourceSection headerSection, + // qualified name of the property or method that carries the type annotation + String qualifiedName) { + return null; + } + + public static TypeNode forClass(SourceSection sourceSection, VmClass clazz) { + return clazz.isClosed() + ? new FinalClassTypeNode(sourceSection, clazz) + : TypeNodeFactory.NonFinalClassTypeNodeGen.create(sourceSection, clazz); + } + + public static PType export(@Nullable TypeNode node) { + return node != null ? node.doExport() : PType.UNKNOWN; + } + + public static VmTyped getMirror(@Nullable TypeNode node) { + return node != null ? node.getMirror() : MirrorFactories.unknownTypeFactory.create(null); + } + + public static VmList getMirrors(TypeNode[] nodes) { + var builder = VmList.EMPTY.builder(); + for (var node : nodes) { + builder.add(node.getMirror()); + } + return builder.build(); + } + + protected PType doExport() { + var alias = getVmTypeAlias(); + // needs to come before `clazz != null` check + if (alias != null) { + return new PType.Alias(alias.export()); + } + var clazz = getVmClass(); + if (clazz != null) { + return new PType.Class(clazz.export()); + } + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .bug("`%s` must override method `doExport()`.", getClass().getTypeName()) + .build(); + } + + public @Nullable VmClass getVmClass() { + return null; + } + + public @Nullable VmTypeAlias getVmTypeAlias() { + return null; + } + + public VmTyped getMirror() { + return MirrorFactories.classTypeFactory.create(this); + } + + public VmList getTypeArgumentMirrors() { + return VmList.EMPTY; + } + + protected final VmTypeMismatchException typeMismatch(Object actualValue, Object expectedType) { + return new VmTypeMismatchException.Simple(sourceSection, actualValue, expectedType); + } + + /** + * Base class for types whose `executeAndSet` method assigns values to slots with + * `frame.setXYZ(slot, value)`. + */ + public abstract static class FrameSlotTypeNode extends TypeNode { + @CompilationFinal protected int slot = -1; + + @CompilationFinal @Child protected WriteFrameSlotNode writeFrameSlotNode; + + protected FrameSlotTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public TypeNode initWriteSlotNode(int slot) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + this.slot = slot; + //noinspection DataFlowIssue + writeFrameSlotNode = WriteFrameSlotNodeGen.create(sourceSection, slot, null); + return this; + } + } + + public abstract static class IntSlotTypeNode extends FrameSlotTypeNode { + protected IntSlotTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + public final FrameSlotKind getFrameSlotKind() { + return FrameSlotKind.Long; + } + + @Override + public final void executeAndSet(VirtualFrame frame, Object value) { + execute(frame, value); + + frame.setLong(slot, (long) value); + } + } + + public abstract static class ObjectSlotTypeNode extends FrameSlotTypeNode { + protected ObjectSlotTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + public final FrameSlotKind getFrameSlotKind() { + return FrameSlotKind.Object; + } + + @Override + public final void executeAndSet(VirtualFrame frame, Object value) { + execute(frame, value); + + frame.setObject(slot, value); + } + } + + /** + * Base class for types whose `executeAndSet` method assigns values to slots with a + * `WriteFrameSlotNode`. + */ + public abstract static class WriteFrameSlotTypeNode extends TypeNode { + @CompilationFinal protected int slot; + @Child @LateInit private WriteFrameSlotNode writeSlotNode; + + protected WriteFrameSlotTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public final FrameSlotKind getFrameSlotKind() { + return FrameSlotKind.Illegal; + } + + @Override + public TypeNode initWriteSlotNode(int slot) { + //noinspection ConstantConditions + writeSlotNode = WriteFrameSlotNodeGen.create(VmUtils.unavailableSourceSection(), slot, null); + this.slot = slot; + return this; + } + + @Override + public final void executeAndSet(VirtualFrame frame, Object value) { + execute(frame, value); + writeSlotNode.executeWithValue(frame, value); + } + } + + /** The `unknown` type. */ + public static final class UnknownTypeNode extends WriteFrameSlotTypeNode { + public UnknownTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public boolean isNoopTypeCheck() { + return true; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + // do nothing + } + + public VmTyped getMirror() { + return MirrorFactories.unknownTypeFactory.create(null); + } + + @Override + protected PType doExport() { + return PType.UNKNOWN; + } + } + + /** The `nothing` type. */ + public static final class NothingTypeNode extends TypeNode { + public NothingTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public TypeNode initWriteSlotNode(int slot) { + // do nothing + return this; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + CompilerDirectives.transferToInterpreter(); + throw new VmTypeMismatchException.Nothing(sourceSection, value); + } + + @Override + public void executeAndSet(VirtualFrame frame, Object value) { + execute(frame, value); + } + + @Override + public FrameSlotKind getFrameSlotKind() { + return FrameSlotKind.Illegal; + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.nothingTypeFactory.create(null); + } + + @Override + protected PType doExport() { + return PType.NOTHING; + } + } + + /** The `module` type for a final module. */ + public static final class FinalModuleTypeNode extends ObjectSlotTypeNode { + private final VmClass moduleClass; + + public FinalModuleTypeNode(SourceSection sourceSection, VmClass moduleClass) { + super(sourceSection); + this.moduleClass = moduleClass; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof VmTyped && ((VmTyped) value).getVmClass() == moduleClass) return; + + throw typeMismatch(value, moduleClass); + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.moduleTypeFactory.create(null); + } + + @Override + protected PType doExport() { + return PType.MODULE; + } + + @Override + public VmClass getVmClass() { + return moduleClass; + } + } + + /** The `module` type for an open module. */ + public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode { + private final VmClass moduleClass; // only used by getVmClass() + @Child private ExpressionNode getModuleNode; + + public NonFinalModuleTypeNode(SourceSection sourceSection, VmClass moduleClass) { + super(sourceSection); + this.moduleClass = moduleClass; + getModuleNode = new GetModuleNode(sourceSection); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + var moduleClass = ((VmTyped) getModuleNode.executeGeneric(frame)).getVmClass(); + + if (value instanceof VmTyped) { + var valueClass = ((VmTyped) value).getVmClass(); + if (moduleClass.isSuperclassOf(valueClass)) return; + } + + throw typeMismatch(value, moduleClass); + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.moduleTypeFactory.create(null); + } + + @Override + protected PType doExport() { + return PType.MODULE; + } + + @Override + public VmClass getVmClass() { + return moduleClass; + } + } + + public static final class StringLiteralTypeNode extends ObjectSlotTypeNode { + private final String literal; + + public StringLiteralTypeNode(SourceSection sourceSection, String literal) { + super(sourceSection); + this.literal = literal; + } + + public String getLiteral() { + return literal; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (literal.equals(value)) return; + + throw typeMismatch(value, literal); + } + + @Override + public Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return literal; + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.stringLiteralTypeFactory.create(this); + } + + @Override + protected PType doExport() { + return new PType.StringLiteral(literal); + } + } + + public static final class TypedTypeNode extends ObjectSlotTypeNode { + public TypedTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof VmTyped) return; + + throw typeMismatch(value, BaseModule.getTypedClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getTypedClass(); + } + } + + public static final class DynamicTypeNode extends ObjectSlotTypeNode { + public DynamicTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof VmDynamic) return; + + throw typeMismatch(value, BaseModule.getDynamicClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getDynamicClass(); + } + + @Override + public Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return VmDynamic.empty(); + } + } + + /** + * A non-open and non-abstract class type. Since this node is not used for + * String/Boolean/Int/Float and their supertypes, only `VmValue`s can possibly pass its type + * check. + */ + public static final class FinalClassTypeNode extends ObjectSlotTypeNode { + private final VmClass clazz; + + public FinalClassTypeNode(SourceSection sourceSection, VmClass clazz) { + super(sourceSection); + this.clazz = clazz; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof VmValue && clazz == ((VmValue) value).getVmClass()) return; + + throw typeMismatch(value, clazz); + } + + @Override + public VmClass getVmClass() { + return clazz; + } + + @Override + public VmList getTypeArgumentMirrors() { + // `List` is represented by `ListTypeNode`, + // but `List` is represented by `FinalClassTypeNode` + return createUnknownTypeArgumentMirrors(clazz); + } + + @Override + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return TypeNode.createDefaultValue(clazz); + } + } + + /** + * An `open` or `abstract` class type. Since this node is not used for String/Boolean/Int/Float + * and their supertypes, only `VmValue`s can possibly pass its type check. + */ + public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode { + protected final VmClass clazz; + + public NonFinalClassTypeNode(SourceSection sourceSection, VmClass clazz) { + super(sourceSection); + this.clazz = clazz; + } + + public final VmClass getVmClass() { + return clazz; + } + + @Override + public VmList getTypeArgumentMirrors() { + // `Collection` is represented by `CollectionTypeNode`, + // but `Collection` is represented by `NonFinalClassTypeNode` + return createUnknownTypeArgumentMirrors(clazz); + } + + @ExplodeLoop + @SuppressWarnings("unused") + @Specialization(guards = "value.getVmClass() == cachedClass") + protected void eval( + VmValue value, + @Cached("value.getVmClass()") VmClass cachedClass, + @Cached("clazz.isSuperclassOf(cachedClass)") boolean isSuperclass) { + + if (isSuperclass) return; + + throw typeMismatch(value, clazz); + } + + @Specialization + protected void eval(VmValue value) { + if (clazz.isSuperclassOf(value.getVmClass())) return; + + throw typeMismatch(value, clazz); + } + + @Fallback + protected void eval(Object value) { + throw typeMismatch(value, clazz); + } + + @Override + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return TypeNode.createDefaultValue(clazz); + } + } + + public abstract static class NullableTypeNode extends WriteFrameSlotTypeNode { + @Child private TypeNode elementTypeNode; + + public NullableTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { + + super(sourceSection); + this.elementTypeNode = elementTypeNode; + } + + public TypeNode getElementTypeNode() { + return elementTypeNode; + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.nullableTypeFactory.create(this); + } + + public VmTyped getElementTypeMirror() { + return elementTypeNode.getMirror(); + } + + @Override + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return VmNull.withDefault( + elementTypeNode.createDefaultValue(language, headerSection, qualifiedName)); + } + + @Override + protected final PType doExport() { + return new PType.Nullable(elementTypeNode.doExport()); + } + + @Specialization + @SuppressWarnings("unused") + protected void eval(VmNull value) {} // do nothing + + @Fallback + protected void eval(VirtualFrame frame, Object value) { + elementTypeNode.execute(frame, value); + } + } + + public static final class UnionTypeNode extends WriteFrameSlotTypeNode { + @Children final TypeNode[] elementTypeNodes; + private final boolean skipElementTypeChecks; + private final int defaultIndex; + + public UnionTypeNode( + SourceSection sourceSection, + int defaultIndex, + TypeNode[] elementTypeNodes, + boolean skipElementTypeChecks) { + super(sourceSection); + assert elementTypeNodes.length > 0; + this.elementTypeNodes = elementTypeNodes; + this.defaultIndex = defaultIndex; + this.skipElementTypeChecks = skipElementTypeChecks; + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.unionTypeFactory.create(this); + } + + public VmList getElementTypeMirrors() { + return getMirrors(elementTypeNodes); + } + + public TypeNode[] getElementTypeNodes() { + return elementTypeNodes; + } + + @Override + public boolean isNoopTypeCheck() { + return skipElementTypeChecks; + } + + @Override + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return defaultIndex == -1 + ? null + : elementTypeNodes[defaultIndex].createDefaultValue( + language, headerSection, qualifiedName); + } + + @Override + protected PType doExport() { + var elementTypes = + Arrays.stream(elementTypeNodes).map(TypeNode::export).collect(Collectors.toList()); + return new PType.Union(elementTypes); + } + + @Override + @ExplodeLoop + public void execute(VirtualFrame frame, Object value) { + if (skipElementTypeChecks) return; + + // escape analysis should remove this allocation in compiled code + var typeMismatches = new VmTypeMismatchException[elementTypeNodes.length]; + + for (var i = 0; i < elementTypeNodes.length; i++) { + try { + elementTypeNodes[i].execute(frame, value); + return; + } catch (VmTypeMismatchException e) { + typeMismatches[i] = e; + } + } + + throw new VmTypeMismatchException.Union(sourceSection, value, this, typeMismatches); + } + } + + public static final class UnionOfStringLiteralsTypeNode extends ObjectSlotTypeNode { + private final Set stringLiterals; + private final @Nullable String unionDefault; + + UnionOfStringLiteralsTypeNode( + SourceSection sourceSection, int defaultIndex, Set stringLiterals) { + super(sourceSection); + + assert !stringLiterals.isEmpty(); + this.stringLiterals = stringLiterals; + if (defaultIndex == -1) { + unionDefault = null; + } else { + unionDefault = stringLiterals.toArray(new String[0])[defaultIndex]; + } + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.unionOfStringLiteralsTypeFactory.create(this); + } + + public VmList getElementTypeMirrors() { + var builder = VmList.EMPTY.builder(); + for (var literal : stringLiterals) { + builder.add(MirrorFactories.stringLiteralTypeFactory2.create(literal)); + } + return builder.build(); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (contains(value)) return; + + throw typeMismatch(value, stringLiterals); + } + + @TruffleBoundary + private boolean contains(Object value) { + //noinspection SuspiciousMethodCalls + return stringLiterals.contains(value); + } + + @Override + protected PType doExport() { + return new PType.Union( + stringLiterals.stream().map(StringLiteral::new).collect(Collectors.toList())); + } + + @Override + @TruffleBoundary + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return unionDefault; + } + } + + public abstract static class CollectionTypeNode extends ObjectSlotTypeNode { + @Child private TypeNode elementTypeNode; + + protected CollectionTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { + + super(sourceSection); + this.elementTypeNode = elementTypeNode; + } + + @Override + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return VmList.EMPTY; + } + + @Override + public VmClass getVmClass() { + return BaseModule.getCollectionClass(); + } + + @Override + protected final PType doExport() { + return new PType.Class(BaseModule.getCollectionClass().export(), elementTypeNode.doExport()); + } + + @Override + public final VmList getTypeArgumentMirrors() { + return VmList.of(elementTypeNode.getMirror()); + } + + @Specialization + protected void eval(VirtualFrame frame, VmList value) { + for (var elem : value) { + elementTypeNode.execute(frame, elem); + } + + LoopNode.reportLoopCount(this, value.getLength()); + } + + @Specialization + protected void eval(VirtualFrame frame, VmSet value) { + for (var elem : value) { + elementTypeNode.execute(frame, elem); + } + + LoopNode.reportLoopCount(this, value.getLength()); + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, BaseModule.getCollectionClass()); + } + } + + public abstract static class ListTypeNode extends ObjectSlotTypeNode { + @Child private TypeNode elementTypeNode; + private final boolean skipElementTypeChecks; + + protected ListTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { + super(sourceSection); + this.elementTypeNode = elementTypeNode; + skipElementTypeChecks = elementTypeNode.isNoopTypeCheck(); + } + + @Override + public final Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return VmList.EMPTY; + } + + @Override + public final VmClass getVmClass() { + return BaseModule.getListClass(); + } + + public TypeNode getElementTypeNode() { + return elementTypeNode; + } + + @Override + public final VmList getTypeArgumentMirrors() { + return VmList.of(elementTypeNode.getMirror()); + } + + @Override + protected final PType doExport() { + return new PType.Class(BaseModule.getListClass().export(), elementTypeNode.doExport()); + } + + @Specialization + protected void eval(VirtualFrame frame, VmList value) { + if (skipElementTypeChecks) return; + + for (var elem : value) { + elementTypeNode.execute(frame, elem); + } + + LoopNode.reportLoopCount(this, value.getLength()); + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, BaseModule.getListClass()); + } + } + + public abstract static class SetTypeNode extends ObjectSlotTypeNode { + @Child private TypeNode elementTypeNode; + private final boolean skipElementTypeChecks; + + protected SetTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { + super(sourceSection); + this.elementTypeNode = elementTypeNode; + skipElementTypeChecks = elementTypeNode.isNoopTypeCheck(); + } + + @Override + public final Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return VmSet.EMPTY; + } + + @Override + public final VmClass getVmClass() { + return BaseModule.getSetClass(); + } + + public TypeNode getElementTypeNode() { + return elementTypeNode; + } + + @Override + public final VmList getTypeArgumentMirrors() { + return VmList.of(elementTypeNode.getMirror()); + } + + @Override + protected final PType doExport() { + return new PType.Class(BaseModule.getSetClass().export(), elementTypeNode.doExport()); + } + + @Specialization + protected void eval(VirtualFrame frame, VmSet value) { + if (skipElementTypeChecks) return; + + for (var elem : value) { + elementTypeNode.execute(frame, elem); + } + + LoopNode.reportLoopCount(this, value.getLength()); + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, BaseModule.getSetClass()); + } + } + + public abstract static class MapTypeNode extends ObjectSlotTypeNode { + @Child private TypeNode keyTypeNode; + @Child private TypeNode valueTypeNode; + private final boolean skipEntryTypeChecks; + + protected MapTypeNode( + SourceSection sourceSection, TypeNode keyTypeNode, TypeNode valueTypeNode) { + + super(sourceSection); + this.keyTypeNode = keyTypeNode; + this.valueTypeNode = valueTypeNode; + skipEntryTypeChecks = keyTypeNode.isNoopTypeCheck() && valueTypeNode.isNoopTypeCheck(); + } + + @Override + public final Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return VmMap.EMPTY; + } + + @Override + public final VmClass getVmClass() { + return BaseModule.getMapClass(); + } + + public TypeNode getValueTypeNode() { + return valueTypeNode; + } + + @Override + public final VmList getTypeArgumentMirrors() { + return VmList.of(keyTypeNode.getMirror(), valueTypeNode.getMirror()); + } + + @Override + protected final PType doExport() { + return new PType.Class( + BaseModule.getMapClass().export(), keyTypeNode.doExport(), valueTypeNode.doExport()); + } + + @Specialization + protected void eval(VirtualFrame frame, VmMap value) { + if (skipEntryTypeChecks) return; + + for (var entry : value) { + keyTypeNode.execute(frame, VmUtils.getKey(entry)); + valueTypeNode.execute(frame, VmUtils.getValue(entry)); + } + + LoopNode.reportLoopCount(this, value.getLength()); + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, BaseModule.getMapClass()); + } + } + + public abstract static class ListingTypeNode extends ListingOrMappingTypeNode { + public ListingTypeNode(SourceSection sourceSection, TypeNode valueTypeNode) { + super(sourceSection, null, valueTypeNode); + } + + @Specialization + protected void eval(VirtualFrame frame, VmListing value) { + doEval(frame, value); + } + + @Override + public final VmClass getVmClass() { + return BaseModule.getListingClass(); + } + + @Override + public final VmList getTypeArgumentMirrors() { + return VmList.of(valueTypeNode.getMirror()); + } + + @Override + protected final PType doExport() { + return new PType.Class(BaseModule.getListingClass().export(), valueTypeNode.doExport()); + } + } + + public abstract static class MappingTypeNode extends ListingOrMappingTypeNode { + public MappingTypeNode( + SourceSection sourceSection, TypeNode keyTypeNode, TypeNode valueTypeNode) { + + super(sourceSection, keyTypeNode, valueTypeNode); + } + + @Specialization + protected void eval(VirtualFrame frame, VmMapping value) { + doEval(frame, value); + } + + @Override + public final VmClass getVmClass() { + return BaseModule.getMappingClass(); + } + + @Override + public final VmList getTypeArgumentMirrors() { + assert keyTypeNode != null; + return VmList.of(keyTypeNode.getMirror(), valueTypeNode.getMirror()); + } + + @Override + protected final PType doExport() { + assert keyTypeNode != null; + return new PType.Class( + BaseModule.getMappingClass().export(), keyTypeNode.doExport(), valueTypeNode.doExport()); + } + } + + public abstract static class ListingOrMappingTypeNode extends ObjectSlotTypeNode { + @Child protected @Nullable TypeNode keyTypeNode; + @Child protected TypeNode valueTypeNode; + + private final boolean skipKeyTypeChecks; + private final boolean skipValueTypeChecks; + + protected ListingOrMappingTypeNode( + SourceSection sourceSection, @Nullable TypeNode keyTypeNode, TypeNode valueTypeNode) { + + super(sourceSection); + this.keyTypeNode = keyTypeNode; + this.valueTypeNode = valueTypeNode; + + skipKeyTypeChecks = keyTypeNode == null || keyTypeNode.isNoopTypeCheck(); + skipValueTypeChecks = valueTypeNode.isNoopTypeCheck(); + } + + private boolean isListing() { + return keyTypeNode == null; + } + + public @Nullable TypeNode getKeyTypeNode() { + return keyTypeNode; + } + + public TypeNode getValueTypeNode() { + return valueTypeNode; + } + + // either (if defaultMemberValue != null): + // x: Listing // = new Listing { + // default = name -> new Foo {} + // } + // or (if defaultMemberValue == null): + // x: Listing // = new Listing { + // default = Undefined() + // } + @Override + @TruffleBoundary + public final Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + if (valueTypeNode instanceof UnknownTypeNode) { + if (isListing()) { + return new VmListing( + VmUtils.createEmptyMaterializedFrame(), + BaseModule.getListingClass().getPrototype(), + EconomicMaps.create(), + 0); + } + + return new VmMapping( + VmUtils.createEmptyMaterializedFrame(), + BaseModule.getMappingClass().getPrototype(), + EconomicMaps.create()); + } + + var defaultMember = + new ObjectMember( + headerSection, + headerSection, + VmModifier.HIDDEN, + Identifier.DEFAULT, + qualifiedName + ".default"); + + var defaultMemberValue = + valueTypeNode.createDefaultValue(language, headerSection, qualifiedName); + + if (defaultMemberValue == null) { + defaultMember.initMemberNode( + new UntypedObjectMemberNode( + language, + new FrameDescriptor(), + defaultMember, + new DefaultPropertyBodyNode(headerSection, Identifier.DEFAULT, null))); + } else { + //noinspection ConstantConditions + defaultMember.initConstantValue( + new VmFunction( + VmUtils.createEmptyMaterializedFrame(), + // Assumption: don't need to set the correct `thisValue` + // because it is guaranteed to be never accessed. + null, + 1, + new SimpleRootNode( + language, + new FrameDescriptor(), + headerSection, + defaultMember.getQualifiedName() + ".", + new ConstantValueNode(defaultMemberValue)), + null)); + } + + if (isListing()) { + return new VmListing( + VmUtils.createEmptyMaterializedFrame(), + BaseModule.getListingClass().getPrototype(), + EconomicMaps.of(Identifier.DEFAULT, defaultMember), + 0); + } + + return new VmMapping( + VmUtils.createEmptyMaterializedFrame(), + BaseModule.getMappingClass().getPrototype(), + EconomicMaps.of(Identifier.DEFAULT, defaultMember)); + } + + protected void doEval(VirtualFrame frame, VmObject object) { + if (skipKeyTypeChecks && skipValueTypeChecks) return; + + var loopCount = 0; + + // similar to shallow forcing + for (var owner = object; owner != null; owner = owner.getParent()) { + var cursor = EconomicMaps.getEntries(owner.getMembers()); + while (cursor.advance()) { + loopCount += 1; + var member = cursor.getValue(); + if (member.isProp()) continue; + + var memberKey = cursor.getKey(); + + if (!skipKeyTypeChecks) { + assert keyTypeNode != null; + keyTypeNode.execute(frame, memberKey); + } + + if (!skipValueTypeChecks) { + var memberValue = object.getCachedValue(memberKey); + if (memberValue == null) { + memberValue = member.getConstantValue(); + if (memberValue == null) { + var callTarget = member.getCallTarget(); + memberValue = callTarget.call(object, owner, memberKey); + } + object.setCachedValue(memberKey, memberValue); + } + valueTypeNode.execute(frame, memberValue); + } + } + } + + LoopNode.reportLoopCount(this, loopCount); + } + + @Fallback + protected void fallback(Object value) { + var clazz = getVmClass(); + assert clazz != null; // either Listing or Mapping + throw typeMismatch(value, clazz); + } + } + + // A type such as `(Int, String) -> Duration`. + public abstract static class FunctionTypeNode extends ObjectSlotTypeNode { + private final TypeNode[] parameterTypeNodes; + private final TypeNode returnTypeNode; + + protected FunctionTypeNode( + SourceSection sourceSection, TypeNode[] parameterTypeNodes, TypeNode returnTypeNode) { + super(sourceSection); + this.parameterTypeNodes = parameterTypeNodes; + this.returnTypeNode = returnTypeNode; + } + + @Override + public final VmClass getVmClass() { + return getFunctionNClass(); + } + + @Override + public final VmTyped getMirror() { + return MirrorFactories.functionTypeFactory.create(this); + } + + public final VmList getParameterTypeMirrors() { + return getMirrors(parameterTypeNodes); + } + + public final VmTyped getReturnTypeMirror() { + return returnTypeNode.getMirror(); + } + + @Override + protected final PType doExport() { + var parameterTypes = + Arrays.stream(parameterTypeNodes).map(TypeNode::export).collect(Collectors.toList()); + return new PType.Function(parameterTypes, TypeNode.export(returnTypeNode)); + } + + @SuppressWarnings("unused") + @Specialization(guards = "value.getVmClass() == getFunctionNClass()") + protected void eval(VmFunction value) { + /* do nothing */ + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, getFunctionNClass()); + } + + // not a field to avoid a circular evaluation error + protected VmClass getFunctionNClass() { + return BaseModule.getFunctionNClass(parameterTypeNodes.length); + } + } + + // A type such as `Function` (but not `FunctionN<...>`). + public abstract static class FunctionClassTypeNode extends ObjectSlotTypeNode { + private final TypeNode typeArgumentNode; + + protected FunctionClassTypeNode(SourceSection sourceSection, TypeNode typeArgumentNode) { + super(sourceSection); + this.typeArgumentNode = typeArgumentNode; + } + + @Override + public final VmClass getVmClass() { + return BaseModule.getFunctionClass(); + } + + public final VmList getTypeArgumentMirrors() { + return VmList.of(typeArgumentNode.getMirror()); + } + + @Override + protected final PType doExport() { + return new PType.Class( + BaseModule.getFunctionClass().export(), TypeNode.export(typeArgumentNode)); + } + + @Specialization + protected void eval(@SuppressWarnings("unused") VmFunction value) { + /* do nothing */ + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, BaseModule.getFunctionClass()); + } + } + + // A type such as `Function2`. + public abstract static class FunctionNClassTypeNode extends ObjectSlotTypeNode { + private final TypeNode[] typeArgumentNodes; + + protected FunctionNClassTypeNode(SourceSection sourceSection, TypeNode[] typeArgumentNodes) { + super(sourceSection); + this.typeArgumentNodes = typeArgumentNodes; + } + + @Override + public final VmClass getVmClass() { + return getFunctionNClass(); + } + + public final VmList getTypeArgumentMirrors() { + return getMirrors(typeArgumentNodes); + } + + @Override + protected final PType doExport() { + var typeArguments = + Arrays.stream(typeArgumentNodes).map(TypeNode::export).collect(Collectors.toList()); + return new PType.Class(getFunctionNClass().export(), typeArguments); + } + + @SuppressWarnings("unused") + @Specialization(guards = "value.getVmClass() == getFunctionNClass()") + protected void eval(VmFunction value) { + /* do nothing */ + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, getFunctionNClass()); + } + + // not a field to avoid a circular evaluation error + protected VmClass getFunctionNClass() { + return BaseModule.getFunctionNClass(typeArgumentNodes.length - 1); + } + } + + public abstract static class PairTypeNode extends ObjectSlotTypeNode { + @Child private TypeNode firstTypeNode; + @Child private TypeNode secondTypeNode; + + protected PairTypeNode( + SourceSection sourceSection, TypeNode firstTypeNode, TypeNode secondTypeNode) { + + super(sourceSection); + this.firstTypeNode = firstTypeNode; + this.secondTypeNode = secondTypeNode; + } + + @Override + public final VmClass getVmClass() { + return BaseModule.getPairClass(); + } + + @Override + public final VmList getTypeArgumentMirrors() { + return VmList.of(firstTypeNode.getMirror(), secondTypeNode.getMirror()); + } + + @Override + protected final PType doExport() { + return new PType.Class( + BaseModule.getPairClass().export(), firstTypeNode.doExport(), secondTypeNode.doExport()); + } + + @Specialization + protected void eval(VirtualFrame frame, VmPair value) { + firstTypeNode.execute(frame, value.getFirst()); + secondTypeNode.execute(frame, value.getSecond()); + } + + @Fallback + protected void fallback(Object value) { + throw typeMismatch(value, BaseModule.getPairClass()); + } + } + + public static class VarArgsTypeNode extends ObjectSlotTypeNode { + @Child private TypeNode elementTypeNode; + + public VarArgsTypeNode(SourceSection sourceSection, TypeNode elementTypeNode) { + + super(sourceSection); + this.elementTypeNode = elementTypeNode; + } + + @Override + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("internalStdLibClass", "VarArgs") + .withSourceSection(headerSection) + .build(); + } + + @Override + protected final PType doExport() { + return new PType.Class(BaseModule.getVarArgsClass().export(), elementTypeNode.doExport()); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("internalStdLibClass", "VarArgs").build(); + } + } + + public static final class TypeVariableNode extends WriteFrameSlotTypeNode { + private final TypeParameter typeParameter; + + public TypeVariableNode(SourceSection sourceSection, TypeParameter typeParameter) { + + super(sourceSection); + this.typeParameter = typeParameter; + } + + public int getTypeParameterIndex() { + return typeParameter.getIndex(); + } + + @Override + public boolean isNoopTypeCheck() { + return true; + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.typeVariableFactory.create(this); + } + + public VmTyped getTypeParameterMirror() { + return MirrorFactories.typeParameterFactory.create(typeParameter); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + // do nothing + } + + @Override + protected PType doExport() { + return new PType.TypeVariable(typeParameter); + } + } + + public static final class NonNullTypeAliasTypeNode extends WriteFrameSlotTypeNode { + public NonNullTypeAliasTypeNode() { + super(VmUtils.unavailableSourceSection()); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof VmNull) { + throw new VmTypeMismatchException.Constraint( + BaseModule.getNonNullTypeAlias().getConstraintSection(), value); + } + } + + @Override + public VmTypeAlias getVmTypeAlias() { + return BaseModule.getNonNullTypeAlias(); + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.typeAliasTypeFactory.create(this); + } + } + + public static final class UIntTypeAliasTypeNode extends IntSlotTypeNode { + private final VmTypeAlias typeAlias; + private final long mask; + + public UIntTypeAliasTypeNode(VmTypeAlias typeAlias, long mask) { + + super(VmUtils.unavailableSourceSection()); + this.typeAlias = typeAlias; + this.mask = mask; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Long) { + var v = (long) value; + if ((v & mask) == v) return; + + throw new VmTypeMismatchException.Constraint(typeAlias.getConstraintSection(), value); + } + + throw new VmTypeMismatchException.Simple( + typeAlias.getBaseTypeSection(), value, BaseModule.getIntClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getIntClass(); + } + + @Override + public VmTypeAlias getVmTypeAlias() { + return typeAlias; + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.typeAliasTypeFactory.create(this); + } + } + + public static final class Int8TypeAliasTypeNode extends IntSlotTypeNode { + public Int8TypeAliasTypeNode() { + super(VmUtils.unavailableSourceSection()); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Long) { + var v = (long) value; + if (v == (byte) v) return; + + throw new VmTypeMismatchException.Constraint( + BaseModule.getInt8TypeAlias().getConstraintSection(), value); + } + + throw new VmTypeMismatchException.Simple( + BaseModule.getInt8TypeAlias().getBaseTypeSection(), value, BaseModule.getIntClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getIntClass(); + } + + @Override + public VmTypeAlias getVmTypeAlias() { + return BaseModule.getInt8TypeAlias(); + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.typeAliasTypeFactory.create(this); + } + } + + public static final class Int16TypeAliasTypeNode extends IntSlotTypeNode { + public Int16TypeAliasTypeNode() { + super(VmUtils.unavailableSourceSection()); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Long) { + var v = (long) value; + if (v == (short) v) return; + + throw new VmTypeMismatchException.Constraint( + BaseModule.getInt16TypeAlias().getConstraintSection(), value); + } + + throw new VmTypeMismatchException.Simple( + BaseModule.getInt16TypeAlias().getBaseTypeSection(), value, BaseModule.getIntClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getIntClass(); + } + + @Override + public VmTypeAlias getVmTypeAlias() { + return BaseModule.getInt16TypeAlias(); + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.typeAliasTypeFactory.create(this); + } + } + + public static final class Int32TypeAliasTypeNode extends IntSlotTypeNode { + public Int32TypeAliasTypeNode() { + super(VmUtils.unavailableSourceSection()); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Long) { + var v = (long) value; + if (v == (int) v) return; + + throw new VmTypeMismatchException.Constraint( + BaseModule.getInt32TypeAlias().getConstraintSection(), value); + } + + throw new VmTypeMismatchException.Simple( + BaseModule.getInt32TypeAlias().getBaseTypeSection(), value, BaseModule.getIntClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getIntClass(); + } + + @Override + public VmTypeAlias getVmTypeAlias() { + return BaseModule.getInt32TypeAlias(); + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.typeAliasTypeFactory.create(this); + } + } + + public static final class TypeAliasTypeNode extends TypeNode { + private final VmTypeAlias typeAlias; + private final TypeNode[] typeArgumentNodes; + @Child private TypeNode aliasedTypeNode; + + public TypeAliasTypeNode( + SourceSection sourceSection, VmTypeAlias typeAlias, TypeNode[] typeArgumentNodes) { + super(sourceSection); + + if (!typeAlias.isInitialized()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder().evalError("cyclicTypeAlias").build(); + } + + if (typeArgumentNodes.length > 0 + && typeArgumentNodes.length != typeAlias.getTypeParameterCount()) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError( + "wrongTypeArgumentCount", + typeAlias.getTypeParameterCount(), + typeArgumentNodes.length) + .build(); + } + + this.typeAlias = typeAlias; + this.typeArgumentNodes = typeArgumentNodes; + aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes); + } + + @Override + public FrameSlotKind getFrameSlotKind() { + return aliasedTypeNode.getFrameSlotKind(); + } + + @Override + public TypeNode initWriteSlotNode(int slot) { + aliasedTypeNode.initWriteSlotNode(slot); + return this; + } + + @Override + public VmTyped getMirror() { + return MirrorFactories.typeAliasTypeFactory.create(this); + } + + @Override + public VmList getTypeArgumentMirrors() { + return getMirrors(typeArgumentNodes); + } + + public void execute(VirtualFrame frame, Object value) { + aliasedTypeNode.execute(frame, value); + } + + @Override + public void executeAndSet(VirtualFrame frame, Object value) { + aliasedTypeNode.executeAndSet(frame, value); + } + + @Override + @TruffleBoundary + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + if (typeAlias == BaseModule.getMixinTypeAlias()) { + //noinspection ConstantConditions + return new VmFunction( + VmUtils.createEmptyMaterializedFrame(), + // Assumption: don't need to set the correct `thisValue` + // because it is guaranteed to be never accessed. + null, + 1, + new IdentityMixinNode( + language, + new FrameDescriptor(), + getSourceSection(), + qualifiedName, + typeArgumentNodes.length == 1 + ? + // shouldn't need to deepCopy() this node because it isn't used as @Child + // anywhere else + typeArgumentNodes[0] + : null), + null); + } + + return aliasedTypeNode.createDefaultValue(language, headerSection, qualifiedName); + } + + @Override + public @Nullable VmClass getVmClass() { + return aliasedTypeNode.getVmClass(); + } + + @Override + public VmTypeAlias getVmTypeAlias() { + return typeAlias; + } + + @Override + protected PType doExport() { + return new PType.Alias( + typeAlias.export(), + Arrays.stream(typeArgumentNodes).map(TypeNode::export).collect(Collectors.toList()), + aliasedTypeNode.doExport()); + } + } + + public static final class ConstrainedTypeNode extends TypeNode { + @Child private TypeNode childNode; + @Children private final TypeConstraintNode[] constraintNodes; + + @CompilationFinal private int customThisSlot = -1; + + public ConstrainedTypeNode( + SourceSection sourceSection, TypeNode childNode, TypeConstraintNode[] constraintNodes) { + super(sourceSection); + this.childNode = childNode; + this.constraintNodes = constraintNodes; + } + + @Override + public FrameSlotKind getFrameSlotKind() { + return childNode.getFrameSlotKind(); + } + + @Override + public TypeNode initWriteSlotNode(int slot) { + childNode.initWriteSlotNode(slot); + return this; + } + + @ExplodeLoop + public void execute(VirtualFrame frame, Object value) { + if (customThisSlot == -1) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + // deferred until execution time s.t. nodes of inlined type aliases get the right frame slot + customThisSlot = + frame.getFrameDescriptor().findOrAddAuxiliarySlot(CustomThisScope.FRAME_SLOT_ID); + } + + childNode.execute(frame, value); + + frame.setAuxiliarySlot(customThisSlot, value); + for (var node : constraintNodes) { + node.execute(frame); + } + } + + @Override + public void executeAndSet(VirtualFrame frame, Object value) { + execute(frame, value); + childNode.executeAndSet(frame, value); + } + + @Override + public @Nullable Object createDefaultValue( + VmLanguage language, SourceSection headerSection, String qualifiedName) { + + return childNode.createDefaultValue(language, headerSection, qualifiedName); + } + + public SourceSection getBaseTypeSection() { + return childNode.getSourceSection(); + } + + public SourceSection getFirstConstraintSection() { + return constraintNodes[0].getSourceSection(); + } + + @Override + protected PType doExport() { + return new PType.Constrained( + childNode.doExport(), + Arrays.stream(constraintNodes) + .map(TypeConstraintNode::export) + .collect(Collectors.toList())); + } + + public VmTyped getMirror() { + // pkl:reflect doesn't currently expose constraints + return childNode.getMirror(); + } + } + + public static final class AnyTypeNode extends WriteFrameSlotTypeNode { + public AnyTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public boolean isNoopTypeCheck() { + return true; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + // do nothing + } + + @Override + public VmClass getVmClass() { + return BaseModule.getAnyClass(); + } + } + + public static final class StringTypeNode extends ObjectSlotTypeNode { + public StringTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof String) return; + + throw typeMismatch(value, BaseModule.getStringClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getStringClass(); + } + } + + public static final class NumberTypeNode extends FrameSlotTypeNode { + public NumberTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public FrameSlotKind getFrameSlotKind() { + return FrameSlotKind.Illegal; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Long || value instanceof Double) return; + + throw typeMismatch(value, BaseModule.getNumberClass()); + } + + @Override + public void executeAndSet(VirtualFrame frame, Object value) { + var kind = frame.getFrameDescriptor().getSlotKind(slot); + if (value instanceof Long) { + if (kind == FrameSlotKind.Double || kind == FrameSlotKind.Object) { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Object); + frame.setObject(slot, value); + } else { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Long); + frame.setLong(slot, (long) value); + } + } else if (value instanceof Double) { + if (kind == FrameSlotKind.Long || kind == FrameSlotKind.Object) { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Object); + frame.setObject(slot, value); + } else { + frame.getFrameDescriptor().setSlotKind(slot, FrameSlotKind.Double); + frame.setDouble(slot, (double) value); + } + } else { + throw typeMismatch(value, BaseModule.getNumberClass()); + } + } + + @Override + public VmClass getVmClass() { + return BaseModule.getNumberClass(); + } + } + + public static final class IntTypeNode extends IntSlotTypeNode { + public IntTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Long) return; + + throw typeMismatch(value, BaseModule.getIntClass()); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getIntClass(); + } + } + + public static final class FloatTypeNode extends FrameSlotTypeNode { + public FloatTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public FrameSlotKind getFrameSlotKind() { + return FrameSlotKind.Double; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Double) return; + + throw typeMismatch(value, BaseModule.getFloatClass()); + } + + @Override + public void executeAndSet(VirtualFrame frame, Object value) { + execute(frame, value); + frame.setDouble(slot, (double) value); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getFloatClass(); + } + } + + public static final class BooleanTypeNode extends FrameSlotTypeNode { + public BooleanTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public FrameSlotKind getFrameSlotKind() { + return FrameSlotKind.Boolean; + } + + @Override + public void execute(VirtualFrame frame, Object value) { + if (value instanceof Boolean) return; + + throw typeMismatch(value, BaseModule.getBooleanClass()); + } + + @Override + public void executeAndSet(VirtualFrame frame, Object value) { + execute(frame, value); + frame.setBoolean(slot, (boolean) value); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getBooleanClass(); + } + } + + private static @Nullable Object createDefaultValue(VmClass clazz) { + if (clazz.isInstantiable()) { + if (clazz.isListingClass()) return VmListing.empty(); + if (clazz.isMappingClass()) return VmMapping.empty(); + return clazz.getPrototype(); + } + + if (clazz.isListClass()) return VmList.EMPTY; + if (clazz.isSetClass()) return VmSet.EMPTY; + if (clazz.isMapClass()) return VmMap.EMPTY; + if (clazz.isCollectionClass()) return VmList.EMPTY; + if (clazz.isNullClass()) return VmNull.withoutDefault(); + + return null; + } + + private static VmList createUnknownTypeArgumentMirrors(VmClass clazz) { + var typeParameterCount = clazz.getTypeParameterCount(); + if (typeParameterCount == 0) return VmList.EMPTY; + + var builder = VmList.EMPTY.builder(); + for (var i = 0; i < typeParameterCount; i++) { + builder.add(MirrorFactories.unknownTypeFactory.create(null)); + } + return builder.build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java new file mode 100644 index 00000000..6f528619 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeTestNode.java @@ -0,0 +1,61 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.util.Nullable; + +public final class TypeTestNode extends ExpressionNode { + @Child private ExpressionNode valueNode; + @Child private UnresolvedTypeNode unresolvedTypeNode; + @Child private @Nullable TypeNode typeNode; + + public TypeTestNode( + SourceSection sourceSection, + ExpressionNode valueNode, + UnresolvedTypeNode unresolvedTypeNode) { + super(sourceSection); + this.valueNode = valueNode; + this.unresolvedTypeNode = unresolvedTypeNode; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return executeBoolean(frame); + } + + @Override + public boolean executeBoolean(VirtualFrame frame) { + if (typeNode == null) { + // don't compile unresolvedTypeNode.execute() + // invalidation is done by insert() + CompilerDirectives.transferToInterpreter(); + typeNode = insert(unresolvedTypeNode.execute(frame)); + unresolvedTypeNode = null; + } + + Object value = valueNode.executeGeneric(frame); + try { + typeNode.execute(frame, value); + return true; + } catch (VmTypeMismatchException e) { + return false; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java new file mode 100644 index 00000000..66130ce8 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java @@ -0,0 +1,429 @@ +/** + * 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.ast.type; + +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.source.SourceSection; +import java.util.Set; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.PklNode; +import org.pkl.core.ast.expression.primary.GetModuleNode; +import org.pkl.core.ast.type.TypeNode.*; +import org.pkl.core.ast.type.TypeNodeFactory.*; +import org.pkl.core.runtime.*; + +public abstract class UnresolvedTypeNode extends PklNode { + protected UnresolvedTypeNode(SourceSection sourceSection) { + super(sourceSection); + } + + public abstract TypeNode execute(VirtualFrame frame); + + public static final class Constrained extends UnresolvedTypeNode { + @Child UnresolvedTypeNode childNode; + TypeConstraintNode[] constraintCheckNodes; + + public Constrained( + SourceSection sourceSection, + UnresolvedTypeNode childNode, + TypeConstraintNode[] constraintCheckNodes) { + super(sourceSection); + this.childNode = childNode; + this.constraintCheckNodes = constraintCheckNodes; + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + return new ConstrainedTypeNode(sourceSection, childNode.execute(frame), constraintCheckNodes); + } + } + + public static final class Unknown extends UnresolvedTypeNode { + public Unknown(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + return new UnknownTypeNode(sourceSection); + } + } + + public static final class Nothing extends UnresolvedTypeNode { + public Nothing(SourceSection sourceSection) { + super(sourceSection); + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + return new NothingTypeNode(sourceSection); + } + } + + /** The `module` type. */ + public static final class Module extends UnresolvedTypeNode { + @Child private ExpressionNode getModuleNode; + + public Module(SourceSection sourceSection) { + super(sourceSection); + getModuleNode = new GetModuleNode(sourceSection); + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var module = (VmTyped) getModuleNode.executeGeneric(frame); + var moduleClass = module.getVmClass(); + return moduleClass.isClosed() + ? new FinalModuleTypeNode(sourceSection, moduleClass) + : new NonFinalModuleTypeNode(sourceSection, moduleClass); + } + } + + public static final class StringLiteral extends UnresolvedTypeNode { + private final String literal; + + public StringLiteral(SourceSection sourceSection, String literal) { + super(sourceSection); + this.literal = literal; + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + return new StringLiteralTypeNode(sourceSection, literal); + } + } + + public static final class Declared extends UnresolvedTypeNode { + @Child private ExpressionNode resolveTypeNode; + + public Declared(SourceSection sourceSection, ExpressionNode resolveTypeNode) { + super(sourceSection); + this.resolveTypeNode = resolveTypeNode; + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var type = resolveTypeNode.executeGeneric(frame); + + if (type instanceof VmClass) { + VmClass clazz = (VmClass) type; + + // Note: FinalClassTypeNode and NonFinalClassTypeNode assume that + // String/Boolean/Int/Float and their supertypes are handled separately. + + if (clazz.getModuleName().equals("pkl.base")) { + switch (clazz.getSimpleName()) { + case "String": + return new StringTypeNode(sourceSection); + case "Boolean": + return new BooleanTypeNode(sourceSection); + case "Int": + return new IntTypeNode(sourceSection); + case "Float": + return new FloatTypeNode(sourceSection); + case "Number": + return new NumberTypeNode(sourceSection); + case "Any": + return new AnyTypeNode(sourceSection); + case "Typed": + return new TypedTypeNode(sourceSection); + case "Dynamic": + return new DynamicTypeNode(sourceSection); + } + } + + return TypeNode.forClass(sourceSection, clazz); + } + + if (type instanceof VmTypeAlias) { + var alias = (VmTypeAlias) type; + + if (alias.getModuleName().equals("pkl.base")) { + switch (alias.getSimpleName()) { + case "NonNull": + return new NonNullTypeAliasTypeNode(); + case "Int8": + return new Int8TypeAliasTypeNode(); + case "UInt8": + return new UIntTypeAliasTypeNode(alias, 0x00000000000000FFL); + case "Int16": + return new Int16TypeAliasTypeNode(); + case "UInt16": + return new UIntTypeAliasTypeNode(alias, 0x000000000000FFFFL); + case "Int32": + return new Int32TypeAliasTypeNode(); + case "UInt32": + return new UIntTypeAliasTypeNode(alias, 0x00000000FFFFFFFFL); + case "UInt": + return new UIntTypeAliasTypeNode(alias, 0x7FFFFFFFFFFFFFFFL); + } + } + + return new TypeAliasTypeNode(sourceSection, alias, new TypeNode[0]); + } + + var module = (VmTyped) type; + assert module.isModuleObject(); + var clazz = module.getVmClass(); + if (!module.isPrototype()) { + throw exceptionBuilder().evalError("notAModuleType", clazz.getModuleName()).build(); + } + return TypeNode.forClass(sourceSection, module.getVmClass()); + } + } + + public static final class Parameterized extends UnresolvedTypeNode { + @Child private ExpressionNode resolveTypeNode; + @Children private final UnresolvedTypeNode[] typeArgumentNodes; + + public Parameterized( + SourceSection sourceSection, + ExpressionNode resolveTypeNode, + UnresolvedTypeNode[] typeArgumentNodes) { + super(sourceSection); + this.resolveTypeNode = resolveTypeNode; + this.typeArgumentNodes = typeArgumentNodes; + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var baseType = resolveTypeNode.executeGeneric(frame); + + if (baseType instanceof VmClass) { + var clazz = (VmClass) baseType; + checkNumberOfTypeArguments(clazz); + + if (clazz.isCollectionClass()) { + return CollectionTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + } + + if (clazz.isListClass()) { + return ListTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + } + + if (clazz.isSetClass()) { + return SetTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + } + + if (clazz.isMapClass()) { + return MapTypeNodeGen.create( + sourceSection, + typeArgumentNodes[0].execute(frame), + typeArgumentNodes[1].execute(frame)); + } + + if (clazz.isListingClass()) { + return ListingTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame)); + } + + if (clazz.isMappingClass()) { + return MappingTypeNodeGen.create( + sourceSection, + typeArgumentNodes[0].execute(frame), + typeArgumentNodes[1].execute(frame)); + } + + if (clazz.isPairClass()) { + return PairTypeNodeGen.create( + sourceSection, + typeArgumentNodes[0].execute(frame), + typeArgumentNodes[1].execute(frame)); + } + + if (clazz.isFunctionClass()) { + return FunctionClassTypeNodeGen.create( + sourceSection, typeArgumentNodes[0].execute(frame)); + } + + if (clazz.isFunctionNClass()) { + var argLength = typeArgumentNodes.length; + var resolvedTypeArgumentNodes = new TypeNode[argLength]; + for (var i = 0; i < argLength; i++) { + resolvedTypeArgumentNodes[i] = typeArgumentNodes[i].execute(frame); + } + return FunctionNClassTypeNodeGen.create(sourceSection, resolvedTypeArgumentNodes); + } + + // erase `x: Class` to `x: Class` for now (cf. function types) + if (clazz.isClassClass()) { + return new FinalClassTypeNode(sourceSection, clazz); + } + + if (clazz.isVarArgsClass()) { + return new VarArgsTypeNode(sourceSection, typeArgumentNodes[0].execute(frame)); + } + + throw exceptionBuilder() + .evalError("notAParameterizableClass", clazz.getDisplayName()) + .withSourceSection(typeArgumentNodes[0].sourceSection) + .build(); + } + + if (baseType instanceof VmTypeAlias) { + var typeAlias = (VmTypeAlias) baseType; + var argLength = typeArgumentNodes.length; + var resolvedTypeArgumentNodes = new TypeNode[argLength]; + for (var i = 0; i < argLength; i++) { + resolvedTypeArgumentNodes[i] = typeArgumentNodes[i].execute(frame); + } + return new TypeAliasTypeNode(sourceSection, typeAlias, resolvedTypeArgumentNodes); + } + + var module = (VmTyped) baseType; + throw exceptionBuilder() + .evalError("notAParameterizableClass", module.getModuleInfo().getModuleName()) + .withSourceSection(typeArgumentNodes[0].sourceSection) + .build(); + } + + private void checkNumberOfTypeArguments(VmClass clazz) { + var expectedCount = clazz.getTypeParameterCount(); + var actualCount = typeArgumentNodes.length; + if (expectedCount != actualCount) { + CompilerDirectives.transferToInterpreter(); + throw exceptionBuilder() + .evalError("wrongTypeArgumentCount", expectedCount, actualCount) + .build(); + } + } + } + + public static final class Nullable extends UnresolvedTypeNode { + @Child private UnresolvedTypeNode elementTypeNode; + + public Nullable(SourceSection sourceSection, UnresolvedTypeNode elementTypeNode) { + super(sourceSection); + this.elementTypeNode = elementTypeNode; + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + return NullableTypeNodeGen.create(sourceSection, elementTypeNode.execute(frame)); + } + } + + public static final class Union extends UnresolvedTypeNode { + @Children private final UnresolvedTypeNode[] unresolvedElementTypeNodes; + private final int defaultIndex; + + public Union( + SourceSection sourceSection, int defaultIndex, UnresolvedTypeNode[] elementTypeNodes) { + super(sourceSection); + this.unresolvedElementTypeNodes = elementTypeNodes; + this.defaultIndex = defaultIndex; + } + + @Override + @ExplodeLoop + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var elementTypeNodes = new TypeNode[unresolvedElementTypeNodes.length]; + var skipElementTypeChecks = true; + + for (var i = 0; i < elementTypeNodes.length; i++) { + var elementTypeNode = unresolvedElementTypeNodes[i].execute(frame); + elementTypeNodes[i] = elementTypeNode; + skipElementTypeChecks &= elementTypeNode.isNoopTypeCheck(); + } + + return new UnionTypeNode( + sourceSection, defaultIndex, elementTypeNodes, skipElementTypeChecks); + } + } + + public static final class UnionOfStringLiterals extends UnresolvedTypeNode { + private final Set stringLiterals; + private final int defaultIndex; + + public UnionOfStringLiterals( + SourceSection sourceSection, int defaultIndex, Set stringLiterals) { + super(sourceSection); + this.stringLiterals = stringLiterals; + this.defaultIndex = defaultIndex; + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + return new UnionOfStringLiteralsTypeNode(sourceSection, defaultIndex, stringLiterals); + } + } + + public static final class Function extends UnresolvedTypeNode { + @Children private final UnresolvedTypeNode[] parameterTypeNodes; + @Child private UnresolvedTypeNode returnTypeNode; + + public Function( + SourceSection sourceSection, + UnresolvedTypeNode[] parameterTypeNodes, + UnresolvedTypeNode returnTypeNode) { + super(sourceSection); + this.parameterTypeNodes = parameterTypeNodes; + this.returnTypeNode = returnTypeNode; + } + + @Override + @ExplodeLoop + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + var parameterTypeNodes = new TypeNode[this.parameterTypeNodes.length]; + for (var i = 0; i < parameterTypeNodes.length; i++) { + parameterTypeNodes[i] = this.parameterTypeNodes[i].execute(frame); + } + + return FunctionTypeNodeGen.create( + sourceSection, parameterTypeNodes, returnTypeNode.execute(frame)); + } + } + + public static final class TypeVariable extends UnresolvedTypeNode { + private final TypeParameter typeParameter; + + public TypeVariable(SourceSection sourceSection, TypeParameter typeParameter) { + super(sourceSection); + this.typeParameter = typeParameter; + } + + @Override + public TypeNode execute(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + return new TypeVariableNode(sourceSection, typeParameter); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/VmTypeMismatchException.java b/pkl-core/src/main/java/org/pkl/core/ast/type/VmTypeMismatchException.java new file mode 100644 index 00000000..b751c7c6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/VmTypeMismatchException.java @@ -0,0 +1,317 @@ +/** + * 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.ast.type; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.nodes.ControlFlowException; +import com.oracle.truffle.api.source.SourceSection; +import java.util.*; +import java.util.stream.Collectors; +import org.pkl.core.ValueFormatter; +import org.pkl.core.ast.type.TypeNode.UnionTypeNode; +import org.pkl.core.runtime.*; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.util.ErrorMessages; + +/** + * Indicates that a type check failed. [TypeNode]s use this exception instead of [VmException] to + * make type checking of union types efficient. Note that any [TruffleBoundary] between throw and + * catch location of this exception must set `transferToInterpreterOnException = false`. (Currently + * there aren't any.) + */ +public abstract class VmTypeMismatchException extends ControlFlowException { + protected final SourceSection sourceSection; + protected final Object actualValue; + + protected VmTypeMismatchException(SourceSection sourceSection, Object actualValue) { + this.sourceSection = sourceSection; + this.actualValue = actualValue; + } + + @TruffleBoundary + public abstract void describe(StringBuilder builder, String indent); + + @TruffleBoundary + public abstract VmException toVmException(); + + public static final class Simple extends VmTypeMismatchException { + private final Object expectedType; + + public Simple(SourceSection sourceSection, Object actualValue, Object expectedType) { + super(sourceSection, actualValue); + + assert expectedType instanceof VmClass + || expectedType instanceof VmTypeAlias + || expectedType instanceof String // string literal type + || expectedType instanceof Set; // union of string literal types + + this.expectedType = expectedType; + } + + @Override + @TruffleBoundary + public void describe(StringBuilder builder, String indent) { + String renderedType; + var valueFormatter = ValueFormatter.basic(); + if (expectedType instanceof String) { + // string literal type + renderedType = valueFormatter.formatStringValue((String) expectedType, ""); + } else if (expectedType instanceof Set) { + // union of string literal types + @SuppressWarnings("unchecked") + var stringLiterals = (Set) expectedType; + renderedType = + stringLiterals.stream() + .map((l) -> valueFormatter.formatStringValue(l, "")) + .collect(Collectors.joining("|")); + } else { + renderedType = expectedType.toString(); + } + + if (actualValue instanceof VmNull + || actualValue instanceof String && expectedType instanceof Set) { + builder.append( + ErrorMessages.createIndented( + "typeMismatchValue", indent, renderedType, new ProgramValue("", actualValue))); + return; + } + + // give better error than "expected foo.Bar, but got foo.Bar" in case of naming conflict + if (actualValue instanceof VmTyped && expectedType instanceof VmClass) { + var actualObj = (VmTyped) actualValue; + var actualClass = actualObj.getVmClass(); + var expectedClass = (VmClass) expectedType; + if (actualClass.getQualifiedName().equals(expectedClass.getQualifiedName())) { + var actualModuleUri = actualClass.getModule().getModuleInfo().getModuleKey().getUri(); + var expectedModuleUri = expectedClass.getModule().getModuleInfo().getModuleKey().getUri(); + + builder + .append( + ErrorMessages.createIndented( + actualClass.getPClassInfo().isModuleClass() + ? "typeMismatchVersionConflict1" + : "typeMismatchVersionConflict2", + indent, + renderedType, + expectedModuleUri, + actualModuleUri)) + .append("\n"); + return; + } + } + + builder + .append( + ErrorMessages.createIndented( + "typeMismatch", indent, renderedType, VmUtils.getClass(actualValue))) + .append("\n") + .append(indent) + .append("Value: ") + .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); + } + + @Override + @TruffleBoundary + public VmException toVmException() { + return exceptionBuilder().build(); + } + + private VmExceptionBuilder exceptionBuilder() { + var builder = new StringBuilder(); + describe(builder, ""); + + return new VmExceptionBuilder() + .adhocEvalError(builder.toString()) + .withSourceSection(sourceSection); + } + } + + public static final class Constraint extends VmTypeMismatchException { + public Constraint(SourceSection sourceSection, Object actualValue) { + super(sourceSection, actualValue); + } + + @Override + @TruffleBoundary + public void describe(StringBuilder builder, String indent) { + builder + .append( + ErrorMessages.createIndented( + "typeConstraintViolated", indent, sourceSection.getCharacters().toString())) + .append("\n") + .append(indent) + .append("Value: ") + .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); + } + + @Override + @TruffleBoundary + public VmException toVmException() { + return exceptionBuilder().build(); + } + + private VmExceptionBuilder exceptionBuilder() { + var builder = new StringBuilder(); + describe(builder, ""); + + return new VmExceptionBuilder() + .adhocEvalError(builder.toString()) + .withSourceSection(sourceSection); + } + } + + public static final class Union extends VmTypeMismatchException { + private final UnionTypeNode typeCheckNode; + private final VmTypeMismatchException[] children; + + public Union( + SourceSection sourceSection, + Object actualValue, + UnionTypeNode typeCheckNode, + VmTypeMismatchException[] children) { + super(sourceSection, actualValue); + this.typeCheckNode = typeCheckNode; + this.children = children; + } + + @Override + @TruffleBoundary + public void describe(StringBuilder builder, String indent) { + describeSummary(builder, indent); + describeDetails(builder, indent); + } + + @Override + @TruffleBoundary + public VmException toVmException() { + return exceptionBuilder().build(); + } + + private VmExceptionBuilder exceptionBuilder() { + var summary = new StringBuilder(); + describeSummary(summary, ""); + + var details = new StringBuilder(); + describeDetails(details, ""); + + return new VmExceptionBuilder() + .adhocEvalError(summary.toString()) + .withSourceSection(sourceSection) + .withHint(details.toString()); + } + + private void describeSummary(StringBuilder builder, String indent) { + var nonTrivialMismatches = findNonTrivialMismatches(); + + if (nonTrivialMismatches.isEmpty()) { + if (actualValue instanceof VmNull) { + builder.append( + ErrorMessages.createIndented( + "typeMismatchValue", indent, sourceSection.getCharacters().toString(), "null")); + } else { + builder.append( + ErrorMessages.createIndented( + "typeMismatch", + indent, + sourceSection.getCharacters().toString(), + VmUtils.getClass(actualValue))); + } + } else { + builder.append( + ErrorMessages.createIndented( + "typeMismatchDifferent", + indent, + sourceSection.getCharacters().toString(), + VmUtils.getClass(actualValue))); + } + + builder + .append("\n") + .append(indent) + .append("Value: ") + .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); + } + + private void describeDetails(StringBuilder builder, String indent) { + var nonTrivialMismatches = findNonTrivialMismatches(); + + var isPeerError = false; + for (var idx : nonTrivialMismatches) { + if (!indent.isEmpty() || isPeerError) builder.append("\n\n"); + isPeerError = true; + builder + .append( + ErrorMessages.createIndented( + "typeMismatchBecause", + indent, + typeCheckNode + .elementTypeNodes[idx] + .getSourceSection() + .getCharacters() + .toString())) + .append("\n"); + children[idx].describe(builder, indent + " "); + } + } + + private List findNonTrivialMismatches() { + var result = new ArrayList(); + for (int idx = 0; idx < children.length; idx++) { + VmTypeMismatchException child = children[idx]; + if (!(child instanceof VmTypeMismatchException.Simple) + // identity comparison is intentional + || child.sourceSection != typeCheckNode.elementTypeNodes[idx].getSourceSection()) { + result.add(idx); + } + } + return result; + } + } + + public static final class Nothing extends VmTypeMismatchException { + public Nothing(SourceSection sourceSection, Object actualValue) { + super(sourceSection, actualValue); + } + + @Override + @TruffleBoundary + public void describe(StringBuilder builder, String indent) { + builder + .append( + ErrorMessages.createIndented( + "cannotAssignToNothing", indent, VmUtils.getClass(actualValue))) + .append("\n") + .append(indent) + .append("Value: ") + .append(VmValueRenderer.singleLine(80 - indent.length()).render(actualValue)); + } + + @Override + @TruffleBoundary + public VmException toVmException() { + return exceptionBuilder().build(); + } + + private VmExceptionBuilder exceptionBuilder() { + var builder = new StringBuilder(); + describe(builder, ""); + + return new VmExceptionBuilder() + .adhocEvalError(builder.toString()) + .withSourceSection(sourceSection); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/package-info.java b/pkl-core/src/main/java/org/pkl/core/ast/type/package-info.java new file mode 100644 index 00000000..0b9e262c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.ast.type; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/module/FileResolver.java b/pkl-core/src/main/java/org/pkl/core/module/FileResolver.java new file mode 100644 index 00000000..953a066f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/FileResolver.java @@ -0,0 +1,58 @@ +/** + * 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.module; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FileResolver { + private FileResolver() {} + + public static List listElements(URI baseUri) throws IOException { + return listElements(Path.of(baseUri)); + } + + public static List listElements(Path path) throws IOException { + try (var stream = Files.newDirectoryStream(path)) { + var ret = new ArrayList(); + for (var entry : stream) { + // skip symlinks to prevent cyclical globs + if (Files.isSymbolicLink(entry)) { + continue; + } + ret.add(new PathElement(entry.getFileName().toString(), Files.isDirectory(entry))); + } + return ret; + } catch (NotDirectoryException | NoSuchFileException ignored) { + return Collections.emptyList(); + } + } + + public static boolean hasElement(URI elementUri) { + return Files.exists(Path.of(elementUri)); + } + + public static boolean hasElement(Path path) { + return Files.exists(path); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModuleKey.java b/pkl-core/src/main/java/org/pkl/core/module/ModuleKey.java new file mode 100644 index 00000000..07d95572 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ModuleKey.java @@ -0,0 +1,85 @@ +/** + * 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.module; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.runtime.ReaderBase; +import org.pkl.core.util.Nullable; + +/** + * SPI for identifying, resolving, caching, and resolving a Pkl module. Standard implementations can + * be created using {@link ModuleKeys}. + */ +public interface ModuleKey extends ReaderBase { + /** + * Returns the absolute URI of this module. This URI is used for identifying the module in user + * facing messages, for example stack traces. Typically, this URI contains all information + * necessary for resolving and loading the module. + */ + URI getUri(); + + /** + * Resolves this module to a canonical form suitable for loading and caching the module. This may + * involve I/O. Throws {@link FileNotFoundException} if this module cannot be found. + */ + ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException; + + default URI resolveUri(URI uri) throws IOException, SecurityManagerException { + return resolveUri(getUri(), uri); + } + + /** + * Tells if this module should be cached in memory. + * + *

Caching a module means caching its entire evaluation state, not just its resolved URI or + * source code. + * + *

Turning off module caching is mostly useful for synthetic modules that cannot be referenced + * (imported) from other modules. An example for this is a module representing code typed in a + * REPL. + */ + default boolean isCached() { + return true; + } + + /** + * Tells if the modules represented by this module key is local to the environment. + * + *

A module that is local, and also {@link #hasHierarchicalUris()}, supports triple-dot + * imports. + * + *

As a best practice, a module key should be considered local if its source can be loaded in a + * low latency environment (for example, from disk or from memory). On the flip-side, a module + * loaded from a remove server should not be considered local. + */ + default boolean isLocal() { + return false; + } + + /** + * The relative file cache path for this module, or {@code null} if this module should not be + * cached on the file system. + */ + default @Nullable Path getFileCacheLocation() { + return null; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactories.java b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactories.java new file mode 100644 index 00000000..4443f378 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactories.java @@ -0,0 +1,207 @@ +/** + * 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.module; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; +import org.pkl.core.util.IoUtils; + +/** Utilities for obtaining and using module key factories. */ +public final class ModuleKeyFactories { + private ModuleKeyFactories() {} + + /** A factory for standard library module keys. */ + public static final ModuleKeyFactory standardLibrary = new StandardLibrary(); + + /** A factory for file based module keys. */ + public static final ModuleKeyFactory file = new File(); + + /** A factory for URL based module keys. */ + public static final ModuleKeyFactory genericUrl = new GenericUrl(); + + /** + * Returns factories registered as {@link ServiceLoader service providers} of type {@code + * org.pkl.core.module.ModuleKeyFactory}. + */ + public static List fromServiceProviders() { + return FromServiceProviders.INSTANCE; + } + + /** A factory for {@code package:} modules. */ + public static final ModuleKeyFactory pkg = new Package(); + + /** A factory for {@code projectpackage:} modules. */ + public static final ModuleKeyFactory projectpackage = new ProjectPackage(); + + /** + * Returns a factory for {@code modulepath:} modules resolved on the given module path. + * + *

NOTE: {@code resolver} needs to be {@link ModulePathResolver#close closed} to avoid resource + * leaks. + */ + public static ModuleKeyFactory modulePath(ModulePathResolver resolver) { + return new ModulePath(resolver); + } + + /** Returns a factory for {@code modulepath:} modules resolved with the given class loader. */ + public static ModuleKeyFactory classPath(ClassLoader classLoader) { + return new ClassPath(classLoader); + } + + /** Closes the given factories, ignoring any exceptions. */ + public static void closeQuietly(Iterable factories) { + for (ModuleKeyFactory factory : factories) { + try { + factory.close(); + } catch (Exception ignored) { + } + } + } + + private static class StandardLibrary implements ModuleKeyFactory { + private StandardLibrary() {} + + @Override + public Optional create(URI uri) { + if (!uri.getScheme().equals("pkl")) return Optional.empty(); + return Optional.of(ModuleKeys.standardLibrary(uri)); + } + } + + private static class ModulePath implements ModuleKeyFactory { + final ModulePathResolver resolver; + + public ModulePath(ModulePathResolver resolver) { + this.resolver = resolver; + } + + @Override + public Optional create(URI uri) { + if (uri.getScheme().equals("modulepath")) { + return Optional.of(ModuleKeys.modulePath(uri, resolver)); + } + if (uri.getScheme().equals("jar")) { + try { + // modulepaths that resolve to jar-file URIs will register a FileSystemProvider that + // such that `Paths.get("jar:file")` returns a path. + // Otherwise, `FileSystemNotFoundException` gets thrown. + var path = Paths.get(uri); + return Optional.of(ModuleKeys.modulePath(URI.create("modulepath:" + path), resolver)); + } catch (FileSystemNotFoundException e) { + return Optional.empty(); + } + } + return Optional.empty(); + } + + @Override + public void close() { + resolver.close(); + } + } + + private static class ClassPath implements ModuleKeyFactory { + private final ClassLoader classLoader; + + public ClassPath(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public Optional create(URI uri) { + if (!uri.getScheme().equals("modulepath")) return Optional.empty(); + return Optional.of(ModuleKeys.classPath(uri, classLoader)); + } + } + + private static class File implements ModuleKeyFactory { + @Override + public Optional create(URI uri) { + Path path; + try { + path = Path.of(uri); + } catch (FileSystemNotFoundException | IllegalArgumentException e) { + // none of the installed file system providers can handle this URI + return Optional.empty(); + } + + return Optional.of(ModuleKeys.file(uri, path)); + } + } + + private static class GenericUrl implements ModuleKeyFactory { + private GenericUrl() {} + + @Override + public Optional create(URI uri) { + if (!uri.isAbsolute()) return Optional.empty(); + if (uri.isOpaque() && !"jar".equalsIgnoreCase(uri.getScheme())) return Optional.empty(); + + // Blindly accept this URI, assuming ModuleKeys.genericUrl() can handle it. + // This means that ModuleKeyFactories.GenericUrl must come last in the handler chain. + return Optional.of(ModuleKeys.genericUrl(uri)); + } + } + + /** + * Represents a module from a package. + * + *

Packages are shareable libraries published to the internet in the form of a zip ball, or + * optionally, a local project declared as a dependency of the current project. + */ + private static final class Package implements ModuleKeyFactory { + public Optional create(URI uri) throws URISyntaxException { + if (uri.getScheme().equalsIgnoreCase("package")) { + return Optional.of(ModuleKeys.pkg(uri)); + } + return Optional.empty(); + } + } + + /** + * Represents a module from a project-relative package. + * + *

A project-relative package has the scheme {@code projectpackage}. It can either be a remote + * dependency, or a local dependency + */ + private static final class ProjectPackage implements ModuleKeyFactory { + public Optional create(URI uri) throws URISyntaxException { + if (uri.getScheme().equalsIgnoreCase("projectpackage")) { + return Optional.of(ModuleKeys.projectpackage(uri)); + } + return Optional.empty(); + } + } + + private static class FromServiceProviders { + private static final List INSTANCE; + + static { + var loader = IoUtils.createServiceLoader(ModuleKeyFactory.class); + var factories = new ArrayList(); + loader.forEach(factories::add); + INSTANCE = Collections.unmodifiableList(factories); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactory.java b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactory.java new file mode 100644 index 00000000..6a0b8369 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeyFactory.java @@ -0,0 +1,46 @@ +/** + * 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.module; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + +/** A factory for {@link ModuleKey}s. */ +public interface ModuleKeyFactory extends AutoCloseable { + /** + * Returns a {@link ModuleKey} for the given absolute normalized URI, or {@code Optional.empty()} + * if this factory cannot handle the given URI. + * + *

Implementations must not perform any I/O related to the given URI. For example, they must + * not check if the module represented by the given URI exists. {@link + * org.pkl.core.SecurityManager} checks for the returned module will be performed by clients of + * this method. + * + *

Throws {@link URISyntaxException} if the given URI has invalid syntax. + * + * @param uri an absolute normalized URI + * @return a module key for the given URI + */ + Optional create(URI uri) throws URISyntaxException; + + /** + * Closes this factory, releasing any resources held. See the documentation of factory methods in + * {@link ModuleKeyFactories} for which factories need to be closed. + */ + @Override + default void close() {} +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java new file mode 100644 index 00000000..d6257fb9 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java @@ -0,0 +1,744 @@ +/** + * 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.module; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.packages.Dependency; +import org.pkl.core.packages.Dependency.LocalDependency; +import org.pkl.core.packages.PackageAssetUri; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.packages.PackageResolver; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; + +/** Utilities for creating and using {@link ModuleKey}s. */ +public final class ModuleKeys { + private ModuleKeys() {} + + /** + * Tells if the given module is a standard library module. Standard library modules ship with the + * language and have a URI of the form {@code pkl:}. + */ + public static boolean isStdLibModule(ModuleKey module) { + return module instanceof StandardLibrary; + } + + /** Tells if the given module is the standard library module with URI {@code pkl:base}. */ + @TruffleBoundary + public static boolean isBaseModule(ModuleKey module) { + return isStdLibModule(module) && module.getUri().getSchemeSpecificPart().equals("base"); + } + + /** + * Creates a module key identified by the given URI and backed by the given source code. Shorthand + * for {@code synthetic(uri, uri, uri, sourceText, false}. + */ + public static ModuleKey synthetic(URI uri, String sourceText) { + return new Synthetic(uri, uri, uri, sourceText, false); + } + + /** + * Creates a module key identified by the given URI and backed by the given source code. Module + * imports will be resolved against the given {@code importBaseUri}. If {@code isCached} is {@code + * true}, the resulting module will be cached with {@code uri} and {@code resolvedUri} used as + * cache keys. + */ + public static ModuleKey synthetic( + URI uri, URI importBaseUri, URI resolvedUri, String sourceText, boolean isCached) { + return new Synthetic(uri, importBaseUri, resolvedUri, sourceText, isCached); + } + + /** + * Creates a module key for the standard library module with the given URI. The URI for a standard + * library module is {@code pkl:}. For example, the URI for the base library + * module is {@code pkl:base}. + */ + public static ModuleKey standardLibrary(URI uri) { + return new StandardLibrary(uri); + } + + /** Creates a module key for a {@code file:} module. */ + public static ModuleKey file(URI uri, Path path) { + return new File(uri, path); + } + + /** + * Creates a module key for a {@code modulepath:} module to be resolved with the given resolver. + */ + public static ModuleKey modulePath(URI uri, ModulePathResolver resolver) { + return new ModulePath(uri, resolver); + } + + /** + * Creates a module key for a {@code modulepath:} module to be resolved with the given class + * loader. + */ + public static ModuleKey classPath(URI uri, ClassLoader classLoader) { + return new ClassPath(uri, classLoader); + } + + /** Creates a module key for the module with the given URL. */ + public static ModuleKey genericUrl(URI url) { + return new GenericUrl(url); + } + + /** Creates a module key for the given package. */ + public static ModuleKey pkg(URI uri) throws URISyntaxException { + var assetUri = new PackageAssetUri(uri); + return new Package(assetUri); + } + + public static ModuleKey projectpackage(URI uri) throws URISyntaxException { + var assetUri = new PackageAssetUri(uri); + return new ProjectPackage(assetUri); + } + + /** + * Creates a module key that behaves like {@code delegate}, except that it returns {@code text} as + * its loaded source. + */ + public static ModuleKey cached(ModuleKey delegate, String text) { + return new CachedModuleKey(delegate, text); + } + + private static class CachedModuleKey implements ModuleKey, ResolvedModuleKey { + private final ModuleKey delegate; + private final String text; + + public CachedModuleKey(ModuleKey delegate, String text) { + this.delegate = delegate; + this.text = text; + } + + @Override + public ModuleKey getOriginal() { + return this; + } + + @Override + public URI getUri() { + return delegate.getUri(); + } + + @Override + public String loadSource() throws IOException { + return text; + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException { + return this; + } + + @Override + public boolean hasHierarchicalUris() { + return delegate.hasHierarchicalUris(); + } + + @Override + public boolean isLocal() { + return delegate.isLocal(); + } + + @Override + public boolean isGlobbable() { + return delegate.isGlobbable(); + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI uri) + throws IOException, SecurityManagerException { + return delegate.hasElement(securityManager, uri); + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + return delegate.listElements(securityManager, baseUri); + } + } + + private static class Synthetic implements ModuleKey { + final URI uri; + final URI importBaseUri; + final boolean isCached; + final ResolvedModuleKey resolvedKey; + + Synthetic(URI uri, URI importBaseUri, URI resolvedUri, String sourceText, boolean isCached) { + this.uri = uri; + this.importBaseUri = importBaseUri; + this.isCached = isCached; + resolvedKey = ResolvedModuleKeys.virtual(this, resolvedUri, sourceText, isCached); + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean hasHierarchicalUris() { + return false; + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws SecurityManagerException { + securityManager.checkResolveModule(uri); + return resolvedKey; + } + + @Override + public boolean isCached() { + return isCached; + } + } + + private static class StandardLibrary implements ModuleKey, ResolvedModuleKey { + final URI uri; + + StandardLibrary(URI uri) { + if (!uri.getScheme().equals("pkl")) { + throw new IllegalArgumentException("Expected URI with scheme `pkl`, but got: " + uri); + } + this.uri = uri; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean hasHierarchicalUris() { + return false; + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws SecurityManagerException { + securityManager.checkResolveModule(uri); + return this; + } + + @Override + public boolean isCached() { + return true; + } + + @Override + public ModuleKey getOriginal() { + return this; + } + + @Override + public String loadSource() throws IOException { + return IoUtils.readClassPathResourceAsString( + getClass(), "/org/pkl/core/stdlib/" + uri.getSchemeSpecificPart() + ".pkl"); + } + } + + private static class File extends DependencyAwareModuleKey { + final URI uri; + final Path path; + + File(URI uri, Path path) { + super(uri); + this.uri = uri; + this.path = path; + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI uri) + throws SecurityManagerException { + securityManager.checkResolveModule(uri); + return FileResolver.hasElement(uri); + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(baseUri); + return FileResolver.listElements(baseUri); + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(uri); + var realPath = path.toRealPath(); + var resolvedUri = realPath.toUri(); + securityManager.checkResolveModule(resolvedUri); + return ResolvedModuleKeys.file(this, resolvedUri, realPath); + } + + @Override + protected Map getDependencies() { + var projectDepsManager = VmContext.get(null).getProjectDependenciesManager(); + if (projectDepsManager == null || !projectDepsManager.hasPath(path)) { + throw new PackageLoadError("cannotResolveDependencyNoProject"); + } + return projectDepsManager.getDependencies(); + } + + @Override + protected PackageLoadError cannotFindDependency(String name) { + return new PackageLoadError("cannotFindDependencyInProject", name); + } + } + + private static final class ModulePath implements ModuleKey { + final URI uri; + final ModulePathResolver resolver; + + ModulePath(URI uri, ModulePathResolver resolver) { + if (uri.getPath() == null) { + throw new IllegalArgumentException( + ErrorMessages.create("invalidModuleUriMissingSlash", uri, "modulepath")); + } + + this.uri = uri; + this.resolver = resolver; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI uri) + throws SecurityManagerException { + securityManager.checkResolveModule(uri); + return resolver.hasElement(uri); + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(uri); + + var path = resolver.resolve(uri).toRealPath(); + return ResolvedModuleKeys.file(this, path.toUri(), path); + } + } + + private static final class ClassPath implements ModuleKey { + + final URI uri; + + final ClassLoader classLoader; + + ClassPath(URI uri, ClassLoader classLoader) { + if (uri.getPath() == null) { + throw new IllegalArgumentException( + ErrorMessages.create("invalidModuleUriMissingSlash", uri, "modulepath")); + } + this.uri = uri; + this.classLoader = classLoader; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + + @Override + public boolean hasElement(SecurityManager manager, URI uri) throws SecurityManagerException { + manager.checkResolveModule(uri); + var uriPath = uri.getPath(); + assert uriPath.charAt(0) == '/'; + return classLoader.getResource(uriPath.substring(1)) != null; + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(uri); + var url = classLoader.getResource(getResourcePath()); + if (url == null) throw new FileNotFoundException(); + try { + return ResolvedModuleKeys.url(this, url.toURI(), url); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + private String getResourcePath() { + String path = uri.getPath(); + assert path.charAt(0) == '/'; + return path.substring(1); + } + } + + private static class GenericUrl implements ModuleKey { + final URI uri; + + GenericUrl(URI uri) { + this.uri = uri; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isLocal() { + return false; + } + + @Override + public boolean isGlobbable() { + return false; + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(uri); + + var url = IoUtils.toUrl(uri); + var conn = url.openConnection(); + conn.connect(); + try (InputStream stream = conn.getInputStream()) { + URI redirected; + try { + redirected = conn.getURL().toURI(); + } catch (URISyntaxException e1) { + // should never happen because we started from a URI + throw new AssertionError(e1); + } + securityManager.checkResolveModule(redirected); + var text = IoUtils.readString(stream); + return ResolvedModuleKeys.virtual(this, uri, text, true); + } + } + } + + /** Base implementation; knows how to resolve dependencies prefixed with @. */ + private abstract static class DependencyAwareModuleKey implements ModuleKey { + + protected final URI uri; + + DependencyAwareModuleKey(URI uri) { + this.uri = uri; + } + + @Override + public URI getUri() { + return uri; + } + + protected Pair parseDependencyNotation(String importPath) { + var idx = importPath.indexOf('/'); + if (idx == -1) { + // treat named dependency wihout a subpath as the root path. + // i.e. resolve to `@foo` to `package://example.com/foo@1.0.0#/` + return Pair.of(importPath.substring(1), "/"); + } + return Pair.of(importPath.substring(1, idx), importPath.substring(idx)); + } + + protected abstract Map getDependencies() + throws IOException, SecurityManagerException; + + @Override + public boolean isLocal() { + return true; + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return true; + } + + private URI resolveDependencyNotation(String notation) + throws IOException, SecurityManagerException { + var parsed = parseDependencyNotation(notation); + var name = parsed.getFirst(); + var path = parsed.getSecond(); + var dependency = getDependencies().get(name); + if (dependency == null) { + throw cannotFindDependency(name); + } + return dependency.getPackageUri().toPackageAssetUri(path).getUri(); + } + + @Override + public URI resolveUri(URI baseUri, URI importUri) throws IOException, SecurityManagerException { + if (importUri.isAbsolute() || !importUri.getPath().startsWith("@")) { + return ModuleKey.super.resolveUri(baseUri, importUri); + } + return resolveDependencyNotation(importUri.getPath()); + } + + protected abstract PackageLoadError cannotFindDependency(String name); + } + + /** Represents a module imported via the {@code package} scheme. */ + private static class Package extends DependencyAwareModuleKey { + + private final PackageAssetUri packageAssetUri; + + Package(PackageAssetUri packageAssetUri) { + super(packageAssetUri.getUri()); + this.packageAssetUri = packageAssetUri; + } + + private PackageResolver getPackageResolver() { + var packageResolver = VmContext.get(null).getPackageResolver(); + assert packageResolver != null; + return packageResolver; + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(uri); + var bytes = + getPackageResolver() + .getBytes(packageAssetUri, false, packageAssetUri.getPackageUri().getChecksums()); + return ResolvedModuleKeys.virtual(this, uri, new String(bytes, StandardCharsets.UTF_8), true); + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(baseUri); + var assetUri = PackageAssetUri.create(baseUri); + return getPackageResolver().listElements(assetUri, assetUri.getPackageUri().getChecksums()); + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI elementUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(elementUri); + var assetUri = PackageAssetUri.create(elementUri); + return getPackageResolver().hasElement(assetUri, assetUri.getPackageUri().getChecksums()); + } + + @Override + public boolean hasFragmentPaths() { + return true; + } + + @Override + protected Map getDependencies() + throws IOException, SecurityManagerException { + return getPackageResolver() + .getDependencyMetadata( + packageAssetUri.getPackageUri(), packageAssetUri.getPackageUri().getChecksums()) + .getDependencies(); + } + + @Override + protected PackageLoadError cannotFindDependency(String name) { + return new PackageLoadError( + "cannotFindDependencyInPackage", name, packageAssetUri.getPackageUri().getDisplayName()); + } + } + + /** + * Represents a module imported via the {@code projectpackage} scheme. + * + *

The {@code projectpackage} scheme is what project-local dependencies resolve to when + * imported using dependency notation (for example, {@code import "@foo/bar.pkl"}). This scheme is + * an internal implementation detail, and we do not expect a project to declare this. + */ + private static class ProjectPackage extends DependencyAwareModuleKey { + + private final PackageAssetUri packageAssetUri; + + ProjectPackage(PackageAssetUri packageAssetUri) { + super(packageAssetUri.getUri()); + this.packageAssetUri = packageAssetUri; + } + + private PackageResolver getPackageResolver() { + var packageResolver = VmContext.get(null).getPackageResolver(); + assert packageResolver != null; + return packageResolver; + } + + private ProjectDependenciesManager getProjectDepsResolver() { + var projectDepsManager = VmContext.get(null).getProjectDependenciesManager(); + assert projectDepsManager != null; + return projectDepsManager; + } + + private @Nullable Path getLocalPath(Dependency dependency) { + if (!(dependency instanceof LocalDependency)) { + return null; + } + return ((LocalDependency) dependency) + .resolveAssetPath(getProjectDepsResolver().getProjectDir(), packageAssetUri); + } + + @Override + public ResolvedModuleKey resolve(SecurityManager securityManager) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(packageAssetUri.getUri()); + var dependency = + getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); + var path = getLocalPath(dependency); + if (path != null) { + securityManager.checkResolveModule(path.toUri()); + return ResolvedModuleKeys.file(this, path.toUri(), path); + } + var dep = (Dependency.RemoteDependency) dependency; + assert dep.getChecksums() != null; + var bytes = getPackageResolver().getBytes(packageAssetUri, false, dep.getChecksums()); + return ResolvedModuleKeys.virtual(this, uri, new String(bytes, StandardCharsets.UTF_8), true); + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(baseUri); + var packageAssetUri = PackageAssetUri.create(baseUri); + var dependency = + getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); + var path = getLocalPath(dependency); + if (path != null) { + securityManager.checkResolveModule(path.toUri()); + return FileResolver.listElements(path); + } + var dep = (Dependency.RemoteDependency) dependency; + assert dep.getChecksums() != null; + return getPackageResolver().listElements(packageAssetUri, dep.getChecksums()); + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI elementUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveModule(elementUri); + var packageAssetUri = PackageAssetUri.create(elementUri); + var dependency = + getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); + var path = getLocalPath(dependency); + if (path != null) { + securityManager.checkResolveModule(path.toUri()); + return FileResolver.hasElement(path); + } + var dep = (Dependency.RemoteDependency) dependency; + assert dep.getChecksums() != null; + return getPackageResolver().hasElement(packageAssetUri, dep.getChecksums()); + } + + @Override + public boolean hasFragmentPaths() { + return true; + } + + @Override + protected Map getDependencies() + throws IOException, SecurityManagerException { + var packageUri = packageAssetUri.getPackageUri(); + var projectResolver = getProjectDepsResolver(); + if (projectResolver.isLocalPackage(packageUri)) { + return projectResolver.getLocalPackageDependencies(packageUri); + } + var dep = + (Dependency.RemoteDependency) getProjectDepsResolver().getResolvedDependency(packageUri); + assert dep.getChecksums() != null; + var dependencyMetadata = + getPackageResolver().getDependencyMetadata(packageUri, dep.getChecksums()); + return projectResolver.getResolvedDependenciesForPackage(packageUri, dependencyMetadata); + } + + @Override + protected PackageLoadError cannotFindDependency(String name) { + return new PackageLoadError( + "cannotFindDependencyInPackage", name, packageAssetUri.getPackageUri().getDisplayName()); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModulePathResolver.java b/pkl-core/src/main/java/org/pkl/core/module/ModulePathResolver.java new file mode 100644 index 00000000..0aa6d6c7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ModulePathResolver.java @@ -0,0 +1,193 @@ +/** + * 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.module; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.concurrent.GuardedBy; +import org.pkl.core.module.PathElement.TreePathElement; +import org.pkl.core.runtime.FileSystemManager; +import org.pkl.core.util.LateInit; + +/** + * Resolves {@code modulepath} URIs from ZIP or JAR files, or from directory paths. + * + *

NOTE: Do not initialize two resolvers for the same jar or zip file. Instead, share the same + * resolver for that jar or zip file. + */ +public class ModulePathResolver implements AutoCloseable { + private final Iterable modulePath; + + private final Object lock = new Object(); + + @LateInit + @GuardedBy("lock") + private Map fileCache; + + @LateInit + @GuardedBy("lock") + private List zipFileSystems; + + @LateInit + @GuardedBy("lock") + private TreePathElement cachedPathElementRoot; + + private @GuardedBy("lock") boolean isClosed = false; + + private static final ModulePathResolver EMPTY = new ModulePathResolver(Collections.emptyList()); + + public static ModulePathResolver empty() { + return EMPTY; + } + + public ModulePathResolver(Iterable modulePath) { + this.modulePath = modulePath; + } + + private void populateCaches() throws IOException { + fileCache = new HashMap<>(); + zipFileSystems = new ArrayList<>(); + cachedPathElementRoot = new TreePathElement("", false); + for (var entry : modulePath) { + if (Files.isDirectory(entry)) { + populateFileCache(entry); + } else if (isJarOrZipFile(entry)) { + // Use the constructor that accepts a jar-file URI because + // this enables `Paths.get("jar:file...")` API calls. + var zipFileSystem = FileSystemManager.getFileSystem(URI.create("jar:" + entry.toUri())); + zipFileSystems.add(zipFileSystem); + for (var root : zipFileSystem.getRootDirectories()) { + populateFileCache(root); + } + } + // ignore + } + } + + private Map getFileCache() throws IOException { + synchronized (lock) { + if (isClosed) { + throw new IllegalStateException("Module path loader has already been closed."); + } + + // create cache lazily so that we only pay the cost if/when a modulepath: URI is first + // resolved + if (fileCache == null) { + populateCaches(); + } + + return fileCache; + } + } + + public Path resolve(URI uri) throws IOException { + var modulePath = getModulePath(uri); + var result = getFileCache().get(modulePath); + if (result != null) return result; + + throw new FileNotFoundException(); + } + + public boolean hasElement(URI elementUri) { + var path = elementUri.getPath(); + try { + assert path.charAt(0) == '/'; + return getFileCache().containsKey(path.substring(1)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void close() { + synchronized (lock) { + if (isClosed || fileCache == null) return; + + for (var fileSystem : zipFileSystems) { + try { + fileSystem.close(); + } catch (IOException ignored) { + } + } + fileCache = null; + cachedPathElementRoot = null; + zipFileSystems = null; + isClosed = true; + } + } + + private void populateFileCache(Path basePath) throws IOException { + try (var stream = + Files.find( + basePath, + Integer.MAX_VALUE, + // reduce file cache size by filtering out .class files + // (currently `read()` only supports text resources, no known use case for accessing + // .class files from Pkl code) + (path, attributes) -> + attributes.isRegularFile() && !path.toString().endsWith(".class"))) { + // in case of duplicate path, first entry wins (cf. class loader) + stream.forEach( + (path) -> { + var relativized = basePath.relativize(path); + fileCache.putIfAbsent(relativized.toString(), path); + var element = cachedPathElementRoot; + for (var i = 0; i < relativized.getNameCount(); i++) { + var name = relativized.getName(i).toString(); + var isDirectory = i < (relativized.getNameCount() - 1); + element = element.putIfAbsent(name, new TreePathElement(name, isDirectory)); + } + }); + } + } + + private static boolean isJarOrZipFile(Path path) { + if (!Files.isRegularFile(path)) return false; + + if (path.endsWith(".jar") || path.endsWith(".zip")) return true; + + byte[] buffer; + try (var fis = new FileInputStream(path.toFile())) { + buffer = fis.readNBytes(39); + } catch (IOException e) { + return false; + } + // file starts with zip header + if (buffer[0] == 0x50 && buffer[1] == 0x4b && buffer[2] == 0x03 && buffer[3] == 0x04) { + return true; + } + // executable jar, e.g. jpkl + return Arrays.equals( + buffer, "#!/bin/sh\n exec java -jar $0 \"$@\"".getBytes(StandardCharsets.UTF_8)); + } + + private static String getModulePath(URI moduleUri) { + var path = moduleUri.getPath(); + assert path.charAt(0) == '/'; + return path.substring(1); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/PathElement.java b/pkl-core/src/main/java/org/pkl/core/module/PathElement.java new file mode 100644 index 00000000..137f4ebf --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/PathElement.java @@ -0,0 +1,131 @@ +/** + * 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.module; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; + +public class PathElement { + public static Comparator comparator = + (o1, o2) -> { + if (o1.isDirectory && !o2.isDirectory) { + return 1; + } else if (!o1.isDirectory && o2.isDirectory) { + return -1; + } + return o1.name.compareTo(o2.name); + }; + + private final String name; + + private final boolean isDirectory; + + public static PathElement opaque(String name) { + return new PathElement(name, false); + } + + public PathElement(String name, boolean isDirectory) { + this.name = name; + this.isDirectory = isDirectory; + } + + public String getName() { + return name; + } + + public PathElement withName(String name) { + if (name.equals(this.name)) { + return this; + } + return new PathElement(name, isDirectory); + } + + public boolean isDirectory() { + return isDirectory; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PathElement)) { + return false; + } + PathElement that = (PathElement) o; + return isDirectory == that.isDirectory && name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), isDirectory()); + } + + @Override + public String toString() { + return "PathElement{" + "name='" + name + '\'' + ", isDirectory=" + isDirectory + '}'; + } + + public static class TreePathElement extends PathElement { + private final EconomicMap children = EconomicMaps.create(); + + public TreePathElement(String name, boolean isDirectory) { + super(name, isDirectory); + } + + public TreePathElement putIfAbsent(String name, TreePathElement child) { + children.putIfAbsent(name, child); + return children.get(name); + } + + /** Returns the element at path {@code basePath}, given a {@link Path}. */ + public @Nullable TreePathElement getElement(Path basePath) { + var path = basePath.normalize(); + var element = this; + for (var i = 0; i < path.getNameCount(); i++) { + var part = path.getName(i).toString(); + element = element.getChildren().get(part); + if (element == null) { + return null; + } + } + return element; + } + + /** Returns the element at path {@code basePath}, given a path string. */ + public @Nullable TreePathElement getElement(String basePath) { + return getElement(Path.of(basePath)); + } + + public EconomicMap getChildren() { + return children; + } + + public List getChildrenValues() { + var ret = new ArrayList(children.size()); + for (var elem : EconomicMaps.getValues(children)) { + ret.add(elem); + } + return ret; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java b/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java new file mode 100644 index 00000000..85a62c7e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java @@ -0,0 +1,225 @@ +/** + * 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.module; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.concurrent.GuardedBy; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.packages.Dependency; +import org.pkl.core.packages.DependencyMetadata; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.packages.PackageUri; +import org.pkl.core.project.CanonicalPackageUri; +import org.pkl.core.project.DeclaredDependencies; +import org.pkl.core.project.ProjectDeps; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.json.Json.JsonParseException; + +public class ProjectDependenciesManager { + public static final String PKL_PROJECT_FILENAME = "PklProject"; + + public static final String PKL_PROJECT_DEPS_FILENAME = "PklProject.deps.json"; + + private final DeclaredDependencies declaredDependencies; + private final Path projectDir; + + @GuardedBy("lock") + private ProjectDeps projectDeps; + + @GuardedBy("lock") + private Map myDependencies = null; + + @GuardedBy("lock") + private EconomicMap> localPackageDependencies = null; + + @GuardedBy("lock") + private EconomicMap> packageDependencies = + EconomicMaps.create(); + + private final Object lock = new Object(); + + public ProjectDependenciesManager(DeclaredDependencies declaredDependencies) { + this.declaredDependencies = declaredDependencies; + this.projectDir = Path.of(declaredDependencies.getProjectFileUri()).getParent(); + } + + public boolean hasPath(Path path) { + return path.startsWith(projectDir); + } + + private void ensureDependenciesInitialized() { + synchronized (lock) { + if (myDependencies != null) { + return; + } + var projectDeps = getProjectDeps(); + myDependencies = doBuildResolvedDependenciesForProject(declaredDependencies, projectDeps); + localPackageDependencies = EconomicMaps.create(); + for (var localPkg : declaredDependencies.getLocalDependencies().values()) { + ensureLocalProjectDependencyInitialized(localPkg, projectDeps); + } + } + } + + private void ensureLocalProjectDependencyInitialized( + DeclaredDependencies localProjectDependencies, ProjectDeps projectDeps) { + assert localPackageDependencies != null; + // turn `package:` scheme into `projectpackage`: scheme + var uri = PackageUri.create("project" + localProjectDependencies.getMyPackageUri()); + if (localPackageDependencies.containsKey(uri)) { + return; + } + var resolvedDeps = doBuildResolvedDependenciesForProject(localProjectDependencies, projectDeps); + localPackageDependencies.put(uri, resolvedDeps); + // TODO: check circular imports (should not be possible) + for (var declaredDeps : localProjectDependencies.getLocalDependencies().values()) { + ensureLocalProjectDependencyInitialized(declaredDeps, projectDeps); + } + } + + private void checkProjectDependencyOutOfDate( + URI projectFileUri, PackageUri declaredPackage, Dependency resolvedDependency) { + if (resolvedDependency.getVersion().compareTo(declaredPackage.getVersion()) < 0) { + throw new PackageLoadError( + "projectDependenciesOutOfDateInProject", + projectFileUri, + declaredPackage.getDisplayName(), + resolvedDependency.getPackageUri().getDisplayName()); + } + } + + private Map doBuildResolvedDependenciesForProject( + DeclaredDependencies declaredDeps, ProjectDeps resolvedProjectDeps) { + var ret = + new HashMap( + declaredDeps.getRemoteDependencies().size() + + declaredDeps.getLocalDependencies().size()); + for (var entry : declaredDeps.getLocalDependencies().entrySet()) { + var localDeclaredDependencies = entry.getValue(); + var packageUri = localDeclaredDependencies.getMyPackageUri(); + assert packageUri != null; + var canonicalPackageUri = + CanonicalPackageUri.fromPackageUri(localDeclaredDependencies.getMyPackageUri()); + var resolvedDep = resolvedProjectDeps.get(canonicalPackageUri); + if (resolvedDep == null) { + throw new PackageLoadError("unresolvedProjectDependency", packageUri); + } + checkProjectDependencyOutOfDate(declaredDeps.getProjectFileUri(), packageUri, resolvedDep); + ret.put(entry.getKey(), resolvedDep); + } + for (var entry : declaredDeps.getRemoteDependencies().entrySet()) { + var remoteDep = entry.getValue(); + var packageUri = CanonicalPackageUri.fromPackageUri(remoteDep.getPackageUri()); + var resolvedDep = resolvedProjectDeps.get(packageUri); + if (resolvedDep == null) { + throw new PackageLoadError("unresolvedProjectDependency", entry.getValue().getPackageUri()); + } + checkProjectDependencyOutOfDate( + declaredDeps.getProjectFileUri(), remoteDep.getPackageUri(), resolvedDep); + ret.put(entry.getKey(), resolvedDep); + } + return ret; + } + + public Map getDependencies() { + ensureDependenciesInitialized(); + return myDependencies; + } + + public boolean isLocalPackage(PackageUri packageUri) { + ensureDependenciesInitialized(); + return localPackageDependencies.containsKey(packageUri); + } + + public Map getLocalPackageDependencies(PackageUri packageUri) { + ensureDependenciesInitialized(); + var dep = localPackageDependencies.get(packageUri); + assert dep != null; + return dep; + } + + public Map getResolvedDependenciesForPackage( + PackageUri packageUri, DependencyMetadata dependencyMetadata) { + synchronized (lock) { + if (!packageDependencies.containsKey(packageUri)) { + var declaredDependencies = dependencyMetadata.getDependencies(); + var resolvedDeps = new HashMap(declaredDependencies.size()); + for (var entry : declaredDependencies.entrySet()) { + var packageDependency = entry.getValue(); + var canonicalPackage = + CanonicalPackageUri.fromPackageUri(packageDependency.getPackageUri()); + var resolvedDep = projectDeps.get(canonicalPackage); + if (resolvedDep == null) { + throw new PackageLoadError("unresolvedProjectDependency", packageDependency); + } + if (resolvedDep.getVersion().compareTo(packageDependency.getVersion()) < 0) { + throw new PackageLoadError( + "projectDependenciesOutOfDateInPackage", + packageUri.getDisplayName(), + packageDependency.getPackageUri().getDisplayName(), + resolvedDep.getPackageUri().getDisplayName()); + } + resolvedDeps.put(entry.getKey(), resolvedDep); + } + packageDependencies.put(packageUri, resolvedDeps); + } + return packageDependencies.get(packageUri); + } + } + + public Dependency getResolvedDependency(PackageUri packageUri) { + var dep = getProjectDeps().get(CanonicalPackageUri.fromPackageUri(packageUri)); + if (dep == null) { + throw new PackageLoadError("unresolvedProjectDependency", packageUri); + } + return dep; + } + + public Path getProjectDir() { + return projectDir; + } + + public Path getProjectDepsFile() { + return projectDir.resolve(PKL_PROJECT_DEPS_FILENAME); + } + + private ProjectDeps getProjectDeps() { + synchronized (lock) { + if (projectDeps == null) { + var depsPath = getProjectDepsFile(); + if (!Files.exists(depsPath)) { + throw new VmExceptionBuilder().evalError("missingProjectDepsJson", projectDir).build(); + } + try { + projectDeps = ProjectDeps.parse(depsPath); + } catch (IOException | URISyntaxException | JsonParseException e) { + throw new VmExceptionBuilder() + .evalError("invalidProjectDepsJson", depsPath, e.getMessage()) + .withCause(e) + .build(); + } + } + return projectDeps; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ResolvedModuleKey.java b/pkl-core/src/main/java/org/pkl/core/module/ResolvedModuleKey.java new file mode 100644 index 00000000..ad8aec3e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ResolvedModuleKey.java @@ -0,0 +1,44 @@ +/** + * 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.module; + +import java.io.IOException; +import java.net.URI; +import org.pkl.core.SecurityManager; + +/** SPI for identifying a resolved module and loading its source code. */ +public interface ResolvedModuleKey { + ModuleKey getOriginal(); + + /** + * Returns the URI of the resolved module. This URI identifies the location that the module's + * source code will be loaded from. If {@link ModuleKey#isCached()} is {@code true}, this URI will + * also be used as a cache key for the module. + */ + URI getUri(); + + /** + * Loads module(s) from {@link #getUri()}. All necessary {@link SecurityManager} checks have + * already been performed before invoking this method. + * + *

In some cases, a module's source code is already loaded as part of resolving the module, + * typically for performance reasons. In such a case, this method will not need to perform any + * additional I/O. + * + * @throws IOException if an I/O error occurs while loading the source code + */ + String loadSource() throws IOException; +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/ResolvedModuleKeys.java b/pkl-core/src/main/java/org/pkl/core/module/ResolvedModuleKeys.java new file mode 100644 index 00000000..647bd9c3 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/ResolvedModuleKeys.java @@ -0,0 +1,137 @@ +/** + * 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.module; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.pkl.core.util.IoUtils; + +/** Utilities for obtaining and using resolved module keys. */ +public final class ResolvedModuleKeys { + private ResolvedModuleKeys() {} + + /** + * Creates a resolved module key backed by the given file path. The resulting module will be + * loaded from that file path and cached using the given URI as cache key. + */ + public static ResolvedModuleKey file(ModuleKey original, URI uri, Path path) { + return new File(original, uri, path); + } + + /** + * Creates a resolved module key backed by the given URL. The resulting module will be loaded from + * that URL and cached using the given URI as cache key. + */ + public static ResolvedModuleKey url(ModuleKey original, URI uri, URL url) { + return new Url(original, uri, url); + } + + /** + * Creates a resolved module key backed by the given source code. If {@code cached} is {@code + * true}, the resulting module will be cached using the given URI as cache key. + */ + public static ResolvedModuleKey virtual( + ModuleKey original, URI uri, String sourceText, boolean cached) { + return new Virtual(original, uri, sourceText, cached); + } + + private static class File implements ResolvedModuleKey { + final ModuleKey original; + final URI uri; + final Path path; + + File(ModuleKey original, URI uri, Path path) { + this.original = original; + this.uri = uri; + this.path = path; + } + + @Override + public ModuleKey getOriginal() { + return original; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public String loadSource() throws IOException { + return Files.readString(path, StandardCharsets.UTF_8); + } + } + + private static class Url implements ResolvedModuleKey { + final ModuleKey original; + final URI uri; + final URL url; + + Url(ModuleKey original, URI uri, URL url) { + this.original = original; + this.uri = uri; + this.url = url; + } + + @Override + public ModuleKey getOriginal() { + return original; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public String loadSource() throws IOException { + return IoUtils.readString(url); + } + } + + private static class Virtual implements ResolvedModuleKey { + final ModuleKey original; + final URI uri; + final String sourceText; + final boolean cached; + + Virtual(ModuleKey original, URI uri, String sourceText, boolean cached) { + this.original = original; + this.uri = uri; + this.sourceText = sourceText; + this.cached = cached; + } + + @Override + public ModuleKey getOriginal() { + return original; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public String loadSource() { + return sourceText; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/module/package-info.java b/pkl-core/src/main/java/org/pkl/core/module/package-info.java new file mode 100644 index 00000000..65da9906 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/module/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.module; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/package-info.java b/pkl-core/src/main/java/org/pkl/core/package-info.java new file mode 100644 index 00000000..d78232b6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/packages/Checksums.java b/pkl-core/src/main/java/org/pkl/core/packages/Checksums.java new file mode 100644 index 00000000..a9ebd29a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/Checksums.java @@ -0,0 +1,52 @@ +/** + * 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.packages; + +import java.util.Objects; + +public final class Checksums { + private final String sha256; + + @Override + public String toString() { + return "pkl.Project#Checksums {sha256='" + sha256 + "'}"; + } + + public Checksums(String sha256) { + this.sha256 = sha256; + } + + public String getSha256() { + return sha256; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Checksums that = (Checksums) o; + return sha256.equals(that.sha256); + } + + @Override + public int hashCode() { + return Objects.hash(getSha256()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/Dependency.java b/pkl-core/src/main/java/org/pkl/core/packages/Dependency.java new file mode 100644 index 00000000..2fe41448 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/Dependency.java @@ -0,0 +1,119 @@ +/** + * 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.packages; + +import java.nio.file.Path; +import java.util.Objects; +import org.pkl.core.Version; +import org.pkl.core.util.Nullable; + +public abstract class Dependency { + + protected final PackageUri packageUri; + + Dependency(PackageUri packageUri) { + this.packageUri = packageUri; + } + + public PackageUri getPackageUri() { + return packageUri; + } + + public Version getVersion() { + return packageUri.getVersion(); + } + + public static final class LocalDependency extends Dependency { + private final Path path; + + public LocalDependency(PackageUri packageUri, Path path) { + super(packageUri); + this.path = path; + } + + public Path getPath() { + return path; + } + + public Path resolveAssetPath(Path projectDir, PackageAssetUri packageAssetUri) { + // drop 1 to remove leading `/` + var assetPath = packageAssetUri.getAssetPath().toString().substring(1); + return projectDir.resolve(path).resolve(assetPath); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LocalDependency that = (LocalDependency) o; + return packageUri.equals(that.packageUri) && path.equals(that.path); + } + + @Override + public int hashCode() { + return Objects.hash(packageUri, path); + } + + @Override + public String toString() { + return "LocalDependency{" + "path=" + path + ", packageUri=" + packageUri + '}'; + } + } + + /** Java representation of {@code pkl.Project#RemoteDependency}. */ + public static final class RemoteDependency extends Dependency { + private @Nullable Checksums checksums; + + public RemoteDependency(PackageUri packageUri, @Nullable Checksums checksums) { + super(packageUri); + this.checksums = checksums; + } + + public @Nullable Checksums getChecksums() { + return checksums; + } + + public void setChecksums(Checksums checksums) { + this.checksums = checksums; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RemoteDependency that = (RemoteDependency) o; + return packageUri.equals(that.packageUri) && Objects.equals(checksums, that.checksums); + } + + @Override + public int hashCode() { + return Objects.hash(packageUri, checksums); + } + + @Override + public String toString() { + return "RemoteDependency{" + "checksums=" + checksums + ", packageUri=" + packageUri + '}'; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/DependencyMetadata.java b/pkl-core/src/main/java/org/pkl/core/packages/DependencyMetadata.java new file mode 100644 index 00000000..357ba799 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/DependencyMetadata.java @@ -0,0 +1,434 @@ +/** + * 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.packages; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.pkl.core.Version; +import org.pkl.core.packages.Dependency.RemoteDependency; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.json.Json; +import org.pkl.core.util.json.Json.FormatException; +import org.pkl.core.util.json.Json.JsArray; +import org.pkl.core.util.json.Json.JsObject; +import org.pkl.core.util.json.Json.JsonParseException; +import org.pkl.core.util.json.JsonWriter; + +/** + * Java representation of a package's dependency metadata. + * + *

Sample metadata: + * + *

+ *   
+ *     {
+ *       "name": "my-proj-name",
+ *       "packageUri": "package://example.com/my-proj-name@0.5.3",
+ *       "version": "0.5.3",
+ *       "packageZipChecksums": {
+ *         "sha256": "abc123",
+ *       }
+ *       "packageZipUrl": "https://example.com/foo/bar@0.5.3.zip",
+ *       "sourceCodeUrlScheme": "https://github.com/foo/bar/blob/v0.5.3/%{path}#L%{line}-L%{endLine}",
+ *       "documentation": "https://my/docs",
+ *       "description": "The description for my package",
+ *       "issueTracker": "https://example.com/my/issues",
+ *       "sourceCode": "https://github.com/foo/bar",
+ *       "license": "Apache-2",
+ *       "dependencies": {
+ *         "foo": {
+ *           "uri": "package://example.com/foo@0.5.3",
+ *           "checksums": {
+ *             "sha256": "abc123"
+ *           }
+ *         }
+ *       }
+ *     }
+ *   
+ * 
+ */ +@SuppressWarnings({"JavadocLinkAsPlainText", "unused"}) +// incorrectly thinks link within sample metadata is a JavaDoc link +public class DependencyMetadata { + + public static DependencyMetadata parse(String input) throws JsonParseException { + var parsed = Json.parseObject(input); + var name = parsed.getString("name"); + var packageUri = parsed.get("packageUri", (it) -> new PackageUri((String) it)); + var version = parsed.getVersion("version"); + var packageZipUrl = parsed.getURI("packageZipUrl"); + var packageZipChecksums = parsed.get("packageZipChecksums", DependencyMetadata::parseChecksums); + var dependencies = parsed.get("dependencies", DependencyMetadata::parseDependencies); + var sourceCodeUrlScheme = parsed.getStringOrNull("sourceCodeUrlScheme"); + var sourceCode = parsed.getURIOrNull("sourceCode"); + var documentation = parsed.getURIOrNull("documentation"); + var license = parsed.getStringOrNull("license"); + var licenseText = parsed.getStringOrNull("licenseText"); + var authors = parsed.getNullable("authors", DependencyMetadata::parseAuthors); + var issueTracker = parsed.getURIOrNull("issueTracker"); + var description = parsed.getStringOrNull("description"); + return new DependencyMetadata( + name, + packageUri, + version, + packageZipUrl, + packageZipChecksums, + dependencies, + sourceCodeUrlScheme, + sourceCode, + documentation, + license, + licenseText, + authors, + issueTracker, + description); + } + + private static Map parseDependencies(Object deps) + throws JsonParseException { + if (!(deps instanceof JsObject)) { + throw new FormatException("object", deps.getClass()); + } + var dependencies = (JsObject) deps; + var ret = new HashMap(dependencies.size()); + for (var key : dependencies.keySet()) { + var remoteDependency = + dependencies.get( + key, + (dep) -> { + if (!(dep instanceof JsObject)) { + throw new FormatException("object", dep.getClass()); + } + var obj = (JsObject) dep; + var checksums = obj.get("checksums", DependencyMetadata::parseChecksums); + var packageUri = obj.get("uri", PackageUtils::parsePackageUriWithoutChecksums); + return new RemoteDependency(packageUri, checksums); + }); + ret.put(key, remoteDependency); + } + return ret; + } + + public static Checksums parseChecksums(Object obj) throws JsonParseException { + if (!(obj instanceof JsObject)) { + throw new FormatException("object", obj.getClass()); + } + var jsObj = (JsObject) obj; + var sha256 = jsObj.getString("sha256"); + return new Checksums(sha256); + } + + public static List parseAuthors(Object obj) throws JsonParseException { + if (!(obj instanceof JsArray)) { + throw new FormatException("array", obj.getClass()); + } + var arr = (JsArray) obj; + var ret = new ArrayList(arr.size()); + for (var elem : arr) { + if (!(elem instanceof String)) { + throw new FormatException("string", elem.getClass()); + } + ret.add((String) elem); + } + return ret; + } + + private final String name; + private final PackageUri packageUri; + private final Version version; + private final URI packageZipUrl; + private final Checksums packageZipChecksums; + private final Map dependencies; + private final @Nullable String sourceCodeUrlScheme; + private final @Nullable URI sourceCode; + private final @Nullable URI documentation; + private final @Nullable String license; + private final @Nullable String licenseText; + private final @Nullable List authors; + private final @Nullable URI issueTracker; + private final @Nullable String description; + + public DependencyMetadata( + String name, + PackageUri packageUri, + Version version, + URI packageZipUrl, + Checksums packageZipChecksums, + Map dependencies, + @Nullable String sourceCodeUrlScheme, + @Nullable URI sourceCode, + @Nullable URI documentation, + @Nullable String license, + @Nullable String licenseText, + @Nullable List authors, + @Nullable URI issueTracker, + @Nullable String description) { + this.name = name; + this.packageUri = packageUri; + this.version = version; + this.packageZipUrl = packageZipUrl; + this.packageZipChecksums = packageZipChecksums; + this.dependencies = dependencies; + this.sourceCodeUrlScheme = sourceCodeUrlScheme; + this.sourceCode = sourceCode; + this.documentation = documentation; + this.license = license; + this.licenseText = licenseText; + this.authors = authors; + this.issueTracker = issueTracker; + this.description = description; + } + + public String getName() { + return name; + } + + public Version getVersion() { + return version; + } + + public URI getPackageZipUrl() { + return packageZipUrl; + } + + public Checksums getPackageZipChecksums() { + return packageZipChecksums; + } + + public Map getDependencies() { + return dependencies; + } + + public @Nullable String getSourceCodeUrlScheme() { + return sourceCodeUrlScheme; + } + + public @Nullable URI getSourceCode() { + return sourceCode; + } + + public @Nullable URI getDocumentation() { + return documentation; + } + + public @Nullable String getLicense() { + return license; + } + + public @Nullable String getLicenseText() { + return licenseText; + } + + public @Nullable List getAuthors() { + return authors; + } + + @Nullable + public URI getIssueTracker() { + return issueTracker; + } + + @Nullable + public String getDescription() { + return description; + } + + /** Serializes project dependencies to JSON, and writes it to the provided output stream. */ + public void writeTo(OutputStream out) throws IOException { + new DependencyMetadataWriter(out, this).write(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DependencyMetadata that = (DependencyMetadata) o; + return name.equals(that.name) + && packageUri.equals(that.packageUri) + && version.equals(that.version) + && packageZipUrl.equals(that.packageZipUrl) + && packageZipChecksums.equals(that.packageZipChecksums) + && dependencies.equals(that.dependencies) + && Objects.equals(sourceCodeUrlScheme, that.sourceCodeUrlScheme) + && Objects.equals(sourceCode, that.sourceCode) + && Objects.equals(documentation, that.documentation) + && Objects.equals(license, that.license) + && Objects.equals(licenseText, that.licenseText) + && Objects.equals(authors, that.authors) + && Objects.equals(issueTracker, that.issueTracker) + && Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash( + name, + packageUri, + version, + packageZipUrl, + packageZipChecksums, + dependencies, + sourceCodeUrlScheme, + sourceCode, + documentation, + license, + licenseText, + authors, + issueTracker, + description); + } + + @Override + public String toString() { + return "DependencyMetadata{" + + "name='" + + name + + '\'' + + "packageUri='" + + packageUri + + '\'' + + ", version=" + + version + + ", packageZipUrl=" + + packageZipUrl + + ", packageZipChecksums=" + + packageZipChecksums + + ", dependencies=" + + dependencies + + ", sourceCodeUrlScheme='" + + sourceCodeUrlScheme + + '\'' + + ", sourceCode='" + + sourceCode + + '\'' + + ", documentation='" + + documentation + + '\'' + + ", license='" + + license + + '\'' + + ", licenseText='" + + licenseText + + '\'' + + ", authors=" + + authors + + '\'' + + ", issueTracker=" + + issueTracker + + '\'' + + ", description=" + + description + + '}'; + } + + private static final class DependencyMetadataWriter { + + private final JsonWriter jsonWriter; + private final DependencyMetadata dependencyMetadata; + + private DependencyMetadataWriter( + OutputStream outputStream, DependencyMetadata dependencyMetadata) { + jsonWriter = new JsonWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); + jsonWriter.setIndent(" "); + this.dependencyMetadata = dependencyMetadata; + } + + private void writeChecksums() throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("sha256").value(dependencyMetadata.packageZipChecksums.getSha256()); + jsonWriter.endObject(); + } + + private void writeDependencies() throws IOException { + jsonWriter.beginObject(); + for (var entry : dependencyMetadata.getDependencies().entrySet()) { + jsonWriter.name(entry.getKey()); + jsonWriter.beginObject(); + jsonWriter.name("uri").value(entry.getValue().getPackageUri().toString()); + var checksums = entry.getValue().getChecksums(); + if (checksums != null) { + jsonWriter.name("checksums"); + jsonWriter.beginObject(); + jsonWriter.name("sha256").value(entry.getValue().getChecksums().getSha256()); + jsonWriter.endObject(); + } + jsonWriter.endObject(); + } + jsonWriter.endObject(); + } + + private void writeAuthors() throws IOException { + var authors = dependencyMetadata.authors; + assert authors != null; + jsonWriter.beginArray(); + for (var author : authors) { + jsonWriter.value(author); + } + jsonWriter.endArray(); + } + + private void write() throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("name").value(dependencyMetadata.name); + jsonWriter.name("packageUri").value(dependencyMetadata.packageUri.toString()); + jsonWriter.name("version").value(dependencyMetadata.version.toString()); + jsonWriter.name("packageZipUrl").value(dependencyMetadata.packageZipUrl.toString()); + jsonWriter.name("packageZipChecksums"); + writeChecksums(); + jsonWriter.name("dependencies"); + writeDependencies(); + if (dependencyMetadata.sourceCodeUrlScheme != null) { + jsonWriter.name("sourceCodeUrlScheme").value(dependencyMetadata.sourceCodeUrlScheme); + } + if (dependencyMetadata.sourceCode != null) { + jsonWriter.name("sourceCode").value(dependencyMetadata.sourceCode.toString()); + } + if (dependencyMetadata.documentation != null) { + jsonWriter.name("documentation").value(dependencyMetadata.documentation.toString()); + } + if (dependencyMetadata.license != null) { + jsonWriter.name("license").value(dependencyMetadata.license); + } + if (dependencyMetadata.licenseText != null) { + jsonWriter.name("licenseText").value(dependencyMetadata.licenseText); + } + if (dependencyMetadata.authors != null) { + jsonWriter.name("authors"); + writeAuthors(); + } + if (dependencyMetadata.issueTracker != null) { + jsonWriter.name("issueTracker").value(dependencyMetadata.issueTracker.toString()); + } + if (dependencyMetadata.description != null) { + jsonWriter.name("description").value(dependencyMetadata.description); + } + jsonWriter.endObject(); + jsonWriter.close(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/PackageAssetUri.java b/pkl-core/src/main/java/org/pkl/core/packages/PackageAssetUri.java new file mode 100644 index 00000000..7c39af5f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/PackageAssetUri.java @@ -0,0 +1,107 @@ +/** + * 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.packages; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import org.pkl.core.Version; +import org.pkl.core.util.ErrorMessages; + +/** + * The canonical URI of an asset within a package, i.e., a package URI with a fragment path. For + * example, {@code package://example.com/my/package@1.0.0#/my/module.pkl} + */ +public class PackageAssetUri { + private final URI uri; + private final PackageUri packageUri; + private final Path assetPath; + + public static PackageAssetUri create(URI uri) { + try { + return new PackageAssetUri(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + public PackageAssetUri(PackageUri packageUri, String assetPath) { + this.uri = packageUri.getUri().resolve("#" + assetPath); + this.packageUri = packageUri; + this.assetPath = Path.of(assetPath); + } + + public PackageAssetUri(String uri) throws URISyntaxException { + this(new URI(uri)); + } + + public PackageAssetUri(URI uri) throws URISyntaxException { + this.uri = uri; + this.packageUri = new PackageUri(uri); + var fragment = uri.getFragment(); + if (fragment == null) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("invalidUriMissingFragment", uri)); + } + if (!fragment.startsWith("/")) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("cannotHaveRelativeFragment", fragment, uri)); + } + this.assetPath = Path.of(fragment); + } + + public URI getUri() { + return uri; + } + + public PackageUri getPackageUri() { + return packageUri; + } + + public Path getAssetPath() { + return assetPath; + } + + public Version getVersion() { + return packageUri.getVersion(); + } + + @Override + public String toString() { + return uri.toString(); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PackageAssetUri that = (PackageAssetUri) o; + return getUri().equals(that.getUri()); + } + + public PackageAssetUri resolve(String path) { + return new PackageAssetUri(packageUri, assetPath.resolve(path).toString()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/PackageLoadError.java b/pkl-core/src/main/java/org/pkl/core/packages/PackageLoadError.java new file mode 100644 index 00000000..499abb9d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/PackageLoadError.java @@ -0,0 +1,44 @@ +/** + * 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.packages; + +import org.pkl.core.util.ErrorMessages; + +public class PackageLoadError extends RuntimeException { + + private final String messageName; + private final Object[] arguments; + + public PackageLoadError(Throwable cause, String messageName, Object... arguments) { + super(ErrorMessages.create(messageName, arguments), cause); + this.messageName = messageName; + this.arguments = arguments; + } + + public PackageLoadError(String messageName, Object... arguments) { + super(ErrorMessages.create(messageName, arguments)); + this.messageName = messageName; + this.arguments = arguments; + } + + public String getMessageName() { + return messageName; + } + + public Object[] getArguments() { + return arguments; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/PackageResolver.java b/pkl-core/src/main/java/org/pkl/core/packages/PackageResolver.java new file mode 100644 index 00000000..dfc210ff --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/PackageResolver.java @@ -0,0 +1,57 @@ +/** + * 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.packages; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import javax.naming.OperationNotSupportedException; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.module.PathElement; +import org.pkl.core.packages.PackageResolvers.DiskCachedPackageResolver; +import org.pkl.core.packages.PackageResolvers.InMemoryPackageResolver; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; + +public interface PackageResolver extends Closeable { + + static PackageResolver getInstance(SecurityManager securityManager, @Nullable Path cachedDir) { + return cachedDir == null + ? new InMemoryPackageResolver(securityManager) + : new DiskCachedPackageResolver(securityManager, cachedDir); + } + + DependencyMetadata getDependencyMetadata(PackageUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException; + + Pair getDependencyMetadataAndComputeChecksum(PackageUri packageUri) + throws IOException, SecurityManagerException; + + void downloadPackage(PackageUri uri, @Nullable Checksums checksums, boolean noTransitive) + throws OperationNotSupportedException, IOException, SecurityManagerException; + + /** Reads the byte contents of the resource within a package. */ + byte[] getBytes(PackageAssetUri uri, boolean allowDirectories, @Nullable Checksums checksums) + throws IOException, SecurityManagerException; + + List listElements(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException; + + boolean hasElement(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException; +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/PackageResolvers.java b/pkl-core/src/main/java/org/pkl/core/packages/PackageResolvers.java new file mode 100644 index 00000000..73a7cc3e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/PackageResolvers.java @@ -0,0 +1,649 @@ +/** + * 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.packages; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import java.util.zip.ZipInputStream; +import javax.annotation.concurrent.GuardedBy; +import javax.net.ssl.HttpsURLConnection; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.Release; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.module.FileResolver; +import org.pkl.core.module.PathElement; +import org.pkl.core.module.PathElement.TreePathElement; +import org.pkl.core.runtime.FileSystemManager; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.util.ByteArrayUtils; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; +import org.pkl.core.util.json.Json.JsonParseException; + +class PackageResolvers { + abstract static class AbstractPackageResolver implements PackageResolver { + private static final String USER_AGENT; + + static { + var release = Release.current(); + USER_AGENT = "Pkl/" + release.version() + " (" + release.os() + " " + release.flavor() + ")"; + } + + @GuardedBy("lock") + private final EconomicMap cachedDependencyMetadata; + + private final SecurityManager securityManager; + + private boolean isClosed = false; + + protected final Object lock = new Object(); + + protected AbstractPackageResolver(SecurityManager securityManager) { + this.securityManager = securityManager; + cachedDependencyMetadata = EconomicMaps.create(); + } + + /** Retrieves a dependency's metadata file. */ + public DependencyMetadata getDependencyMetadata(PackageUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + if (isClosed) { + throw new IllegalStateException(); + } + synchronized (lock) { + var metadata = cachedDependencyMetadata.get(uri); + if (metadata == null) { + metadata = doGetDependencyMetadata(uri, checksums); + cachedDependencyMetadata.put(uri, metadata); + } + return metadata; + } + } + + @Override + public Pair getDependencyMetadataAndComputeChecksum( + PackageUri packageUri) throws IOException, SecurityManagerException { + var requestUri = packageUri.getMetadataRequestUri(); + var inputStream = openExternalUri(requestUri); + return readDependencyMetadataAndComputeChecksum(packageUri, inputStream); + } + + protected Pair readDependencyMetadataAndComputeChecksum( + PackageUri packageUri, InputStream inputStream) throws IOException { + try (var in = newDigestInputStream(inputStream)) { + var bytes = in.readAllBytes(); + var dependencyMetadata = + DependencyMetadata.parse(new String(bytes, StandardCharsets.UTF_8)); + var checksums = new Checksums(ByteArrayUtils.toHex(in.getMessageDigest().digest())); + return Pair.of(dependencyMetadata, checksums); + } catch (JsonParseException e) { + throw new PackageLoadError( + e, + "invalidDependencyMetadata", + packageUri.getDisplayName(), + packageUri.getMetadataRequestUri(), + e.getMessage()); + } + } + + @Override + public List listElements(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + if (isClosed) { + throw new IllegalStateException(); + } + return doListElements(uri, checksums); + } + + @Override + public boolean hasElement(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + if (isClosed) { + throw new IllegalStateException(); + } + return doHasElement(uri, checksums); + } + + @Override + public void close() throws IOException { + synchronized (lock) { + if (isClosed) { + return; + } + cachedDependencyMetadata.clear(); + isClosed = true; + } + } + + protected DigestInputStream newDigestInputStream(InputStream in) { + try { + var md = MessageDigest.getInstance("SHA-256"); + return new DigestInputStream(in, md); + } catch (NoSuchAlgorithmException e) { + // All JDK's ship with SHA-256 + throw new VmExceptionBuilder().unreachableCode().build(); + } + } + + protected void verifyPackageZipBytes( + PackageUri packageUri, DependencyMetadata dependencyMetadata, byte[] computedChecksum) { + var checksum = ByteArrayUtils.toHex(computedChecksum); + var expectedChecksum = dependencyMetadata.getPackageZipChecksums().getSha256(); + if (!checksum.equals(expectedChecksum)) { + throw new PackageLoadError( + "invalidPackageZipChecksum", + packageUri.getDisplayName(), + checksum, + expectedChecksum, + dependencyMetadata.getPackageZipUrl()); + } + } + + protected void verifyPackageMetadataBytes( + PackageUri packageUri, URI requestUri, Checksums checksums, byte[] computedChecksum) { + var expectedChecksum = checksums.getSha256(); + var checksum = ByteArrayUtils.toHex(computedChecksum); + // Qualify of life improvement: we have a lot of projects in our language snippet tests. + // To avoid having to update checksum values in their PklProject.deps.json files, every time + // a package changes, we set their checksum value to "$skipChecksumVerification". + // We keep two tests that do test checksum verification. + if (IoUtils.isTestMode() && expectedChecksum.equals("$skipChecksumVerification")) { + return; + } + if (!checksum.equals(expectedChecksum)) { + throw new PackageLoadError( + "invalidPackageMetadataChecksum", + packageUri.getDisplayName(), + checksum, + expectedChecksum, + requestUri); + } + } + + protected InputStream openExternalUri(URI uri) throws SecurityManagerException, IOException { + // treat package assets as resources instead of modules + securityManager.checkReadResource(uri); + var connection = (HttpsURLConnection) uri.toURL().openConnection(); + connection.setRequestProperty("User-Agent", USER_AGENT); + int responseCode; + try { + responseCode = connection.getResponseCode(); + if (responseCode != 200) { + throw new PackageLoadError("badHttpStatusCode", responseCode, uri); + } + } catch (IOException e) { + throw new PackageLoadError(e, "ioErrorMakingHttpGet", uri, e.getMessage()); + } + return connection.getInputStream(); + } + + protected IOException fileIsADirectory() { + // Sync with error message from `Files#readString(Path)` + return new IOException("Is a directory"); + } + + protected abstract DependencyMetadata doGetDependencyMetadata( + PackageUri packageUri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException; + + protected abstract List doListElements( + PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException; + + protected abstract boolean doHasElement(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException; + + protected PackageLoadError invalidPackageZipUrl( + PackageUri packageUri, DependencyMetadata dependencyMetadata) { + return new PackageLoadError( + "invalidPackageZipUrl", + packageUri.getDisplayName(), + dependencyMetadata.getPackageZipUrl()); + } + } + + /** + * A package resolver that holds entire package contents in memory. + * + *

This gets used when the cache dir is not set. + */ + static class InMemoryPackageResolver extends AbstractPackageResolver { + @GuardedBy("lock") + private final EconomicMap> cachedEntries = + EconomicMaps.create(); + + @GuardedBy("lock") + private final EconomicMap cachedTreePathElementRoots = + EconomicMaps.create(); + + InMemoryPackageResolver(SecurityManager securityManager) { + super(securityManager); + } + + private byte[] getPackageBytes(PackageUri packageUri, DependencyMetadata metadata) + throws IOException, SecurityManagerException { + var httpInputStream = openExternalUri(metadata.getPackageZipUrl()); + try (var digestInputStream = newDigestInputStream(httpInputStream)) { + var packageBytes = digestInputStream.readAllBytes(); + var checksumBytes = digestInputStream.getMessageDigest().digest(); + verifyPackageZipBytes(packageUri, metadata, checksumBytes); + return packageBytes; + } + } + + private void ensurePackageDownloaded(PackageUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + synchronized (lock) { + if (cachedEntries.containsKey(uri)) { + return; + } + var metadata = getDependencyMetadata(uri, checksums); + var cachedEntrySet = EconomicMaps.create(); + var packageBytes = getPackageBytes(uri, metadata); + try (var zipInputStream = new ZipInputStream(new ByteArrayInputStream(packageBytes))) { + var rootPathElement = new TreePathElement("", true); + cachedTreePathElementRoots.put(uri, rootPathElement); + for (var entry = zipInputStream.getNextEntry(); + entry != null; + entry = zipInputStream.getNextEntry()) { + var pathElement = rootPathElement; + var nameParts = entry.getName().split("/"); + var nameCount = nameParts.length; + for (var i = 0; i < nameCount; i++) { + var name = nameParts[i]; + var isDirectory = entry.isDirectory() || i < (nameCount - 1); + pathElement = pathElement.putIfAbsent(name, new TreePathElement(name, isDirectory)); + } + if (!entry.isDirectory()) { + var entryBytes = zipInputStream.readAllBytes(); + cachedEntrySet.put("/" + entry.getName(), ByteBuffer.wrap(entryBytes)); + } + } + } + cachedEntries.put(uri, cachedEntrySet); + } + } + + // No sense in supporting this in the in-memory package resolver, because it cannot write + // anything to disk. + @Override + public void downloadPackage( + PackageUri uri, @Nullable Checksums checksums, boolean noTransitive) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getBytes( + PackageAssetUri uri, boolean allowDirectories, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var packageUri = uri.getPackageUri(); + ensurePackageDownloaded(packageUri, checksums); + var elem = cachedTreePathElementRoots.get(uri.getPackageUri()).getElement(uri.getAssetPath()); + if (elem == null) { + throw new FileNotFoundException(); + } else if (elem.isDirectory()) { + if (allowDirectories) { + var text = + StreamSupport.stream(elem.getChildren().getKeys().spliterator(), false) + .sorted() + .collect(Collectors.joining("\n")) + + "\n"; + return text.getBytes(StandardCharsets.UTF_8); + } + throw fileIsADirectory(); + } + var entries = cachedEntries.get(packageUri); + // need to normalize here but not in `doListElments` nor `doHasElement` because + // `TreePathElement.getElement` does normalization already. + var path = uri.getAssetPath().normalize().toString(); + assert path.startsWith("/"); + return entries.get(path).array(); + } + + @Override + public List doListElements(PackageAssetUri uri, Checksums checksums) + throws IOException, SecurityManagerException { + var packageUri = uri.getPackageUri(); + ensurePackageDownloaded(packageUri, checksums); + var element = cachedTreePathElementRoots.get(packageUri).getElement(uri.getAssetPath()); + if (element == null) { + return Collections.emptyList(); + } + return element.getChildrenValues(); + } + + @Override + public boolean doHasElement(PackageAssetUri uri, Checksums checksums) + throws IOException, SecurityManagerException { + var packageUri = uri.getPackageUri(); + ensurePackageDownloaded(packageUri, checksums); + var element = cachedTreePathElementRoots.get(packageUri).getElement(uri.getAssetPath()); + return element != null; + } + + @Override + protected DependencyMetadata doGetDependencyMetadata( + PackageUri packageUri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var requestUri = packageUri.getMetadataRequestUri(); + var inputStream = openExternalUri(requestUri); + if (checksums != null) { + inputStream = newDigestInputStream(inputStream); + } + try (var in = inputStream) { + var metadataStr = IoUtils.readString(in); + if (checksums != null) { + var digestInputStream = (DigestInputStream) in; + var checksumBytes = digestInputStream.getMessageDigest().digest(); + verifyPackageMetadataBytes(packageUri, requestUri, checksums, checksumBytes); + } + var metadata = DependencyMetadata.parse(metadataStr); + if (!metadata.getPackageZipUrl().getScheme().equalsIgnoreCase("https")) { + throw invalidPackageZipUrl(packageUri, metadata); + } + return metadata; + } catch (JsonParseException e) { + throw new PackageLoadError( + e, + "invalidDependencyMetadata", + packageUri.getDisplayName(), + requestUri, + e.getMessage()); + } + } + + @Override + public void close() throws IOException { + super.close(); + synchronized (lock) { + cachedEntries.clear(); + cachedTreePathElementRoots.clear(); + } + } + } + + /** + * Resolves packages, cacheing them to disk. + * + *

Uses the built-in zip file system in {@link jdk.nio.zipfs} for reading files from the zip + * archive. + */ + static class DiskCachedPackageResolver extends AbstractPackageResolver { + private final Path cacheDir; + + private final Path tmpDir; + + private static final String CACHE_DIR_PREFIX = "package-1"; + + @GuardedBy("lock") + private final EconomicMap fileSystems = EconomicMaps.create(); + + private static final Set FILE_PERMISSIONS = + EnumSet.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ); + + public DiskCachedPackageResolver(SecurityManager securityManager, Path cacheDir) { + super(securityManager); + this.cacheDir = cacheDir; + this.tmpDir = cacheDir.resolve("tmp"); + } + + private String getEffectivePackageUriPath(PackageUri packageUri) { + var path = packageUri.getUri().getPath(); + assert path != null; + if (packageUri.getChecksums() == null) { + return path; + } + var checksumIdx = path.lastIndexOf("::"); + return path.substring(0, checksumIdx); + } + + private Path getRelativePath(PackageUri uri) { + return Path.of( + CACHE_DIR_PREFIX, uri.getUri().getAuthority(), getEffectivePackageUriPath(uri)); + } + + private String getLastSegmentName(PackageUri packageUri) { + var path = getEffectivePackageUriPath(packageUri); + var lastSep = path.lastIndexOf("/"); + return path.substring(lastSep + 1); + } + + private byte[] downloadUriToPathAndComputeChecksum(URI downloadUri, Path relativePath) + throws IOException, SecurityManagerException { + var tmpPath = tmpDir.resolve(relativePath); + Files.createDirectories(tmpPath.getParent()); + var inputStream = openExternalUri(downloadUri); + try (var digestInputStream = newDigestInputStream(inputStream)) { + Files.copy(digestInputStream, tmpPath); + return digestInputStream.getMessageDigest().digest(); + } + } + + private void downloadMetadata( + PackageUri packageUri, URI downloadUri, Path path, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var inputStream = openExternalUri(downloadUri); + if (checksums != null) { + inputStream = newDigestInputStream(inputStream); + } + Files.createDirectories(path.getParent()); + try (var in = inputStream) { + Files.copy(in, path); + if (checksums != null) { + var digestInputStream = (DigestInputStream) inputStream; + var checksumBytes = digestInputStream.getMessageDigest().digest(); + verifyPackageMetadataBytes(packageUri, downloadUri, checksums, checksumBytes); + } + } + } + + private Path getMetadataPath( + PackageUri packageUri, URI requestUri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var metadataFileName = getLastSegmentName(packageUri) + ".json"; + var metdataRelativePath = getRelativePath(packageUri).resolve(metadataFileName); + var cachePath = cacheDir.resolve(metdataRelativePath); + if (Files.exists(cachePath)) { + return cachePath; + } + var tmpPath = tmpDir.resolve(metdataRelativePath); + try { + downloadMetadata(packageUri, requestUri, tmpPath, checksums); + Files.createDirectories(cachePath.getParent()); + Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE); + Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS); + return cachePath; + } finally { + Files.deleteIfExists(tmpPath); + } + } + + /** Retrieves a dependency's metadata file. */ + @Override + protected DependencyMetadata doGetDependencyMetadata( + PackageUri packageUri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var requestUri = packageUri.getMetadataRequestUri(); + var metadataPath = getMetadataPath(packageUri, requestUri, checksums); + var metadataStr = Files.readString(metadataPath, StandardCharsets.UTF_8); + DependencyMetadata metadata; + try { + metadata = DependencyMetadata.parse(metadataStr); + } catch (JsonParseException e) { + Files.deleteIfExists(metadataPath); + throw new PackageLoadError( + e, + "invalidDependencyMetadata", + packageUri.getDisplayName(), + requestUri, + e.getMessage()); + } + if (!metadata.getPackageZipUrl().getScheme().equalsIgnoreCase("https")) { + Files.deleteIfExists(metadataPath); + throw invalidPackageZipUrl(packageUri, metadata); + } + return metadata; + } + + private Path getZipFilePath(PackageUri packageUri, DependencyMetadata dependencyMetadata) + throws IOException, SecurityManagerException { + var packageZipName = getLastSegmentName(packageUri) + ".zip"; + var relativePath = getRelativePath(packageUri).resolve(packageZipName); + var cachePath = cacheDir.resolve(relativePath); + if (Files.exists(cachePath)) { + return cachePath; + } + var tmpPath = tmpDir.resolve(relativePath); + try { + var checksumBytes = + downloadUriToPathAndComputeChecksum(dependencyMetadata.getPackageZipUrl(), tmpPath); + verifyPackageZipBytes(packageUri, dependencyMetadata, checksumBytes); + Files.createDirectories(cachePath.getParent()); + Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE); + Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS); + return cachePath; + } finally { + Files.deleteIfExists(tmpPath); + } + } + + /** + * Returns a file system that backs the zip archive for a package. + * + *

Downloads the package if not available within {@link DiskCachedPackageResolver#cacheDir}. + */ + private FileSystem getZipFileSystem(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var packageUri = uri.getPackageUri(); + synchronized (fileSystems) { + var fs = fileSystems.get(packageUri); + if (fs == null) { + var metadata = getDependencyMetadata(packageUri, checksums); + var zipFilePath = getZipFilePath(packageUri, metadata); + var jarFileUri = URI.create("jar:" + zipFilePath.toUri()); + fs = FileSystemManager.getFileSystem(jarFileUri); + fileSystems.put(packageUri, fs); + } + return fs; + } + } + + @Override + public void downloadPackage(PackageUri uri, @Nullable Checksums checksums, boolean noTransitive) + throws IOException, SecurityManagerException { + var metadata = getDependencyMetadata(uri, checksums); + getZipFilePath(uri, metadata); + if (noTransitive) { + return; + } + for (var dependency : metadata.getDependencies().values()) { + downloadPackage(dependency.getPackageUri(), dependency.getChecksums(), false); + } + } + + @Override + public Pair getDependencyMetadataAndComputeChecksum( + PackageUri packageUri) throws IOException, SecurityManagerException { + var packageDir = cacheDir.resolve(getRelativePath(packageUri)); + var cacheFile = packageDir.resolve(packageDir.getFileName() + ".json"); + if (Files.exists(cacheFile)) { + return readDependencyMetadataAndComputeChecksum( + packageUri, new FileInputStream(cacheFile.toFile())); + } + return super.getDependencyMetadataAndComputeChecksum(packageUri); + } + + /** Reads the byte contents of the resource within a package. */ + @Override + public byte[] getBytes( + PackageAssetUri uri, boolean allowDirectories, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var path = getZipFileSystem(uri, checksums).getPath(uri.getUri().getFragment()); + if (Files.isDirectory(path)) { + if (allowDirectories) { + // mimick the format that we get when reading a `file:` directory + try (var paths = Files.list(path)) { + var text = + paths + .map(it -> it.getFileName().toString()) + .sorted() + .collect(Collectors.joining("\n")) + + "\n"; + return text.getBytes(StandardCharsets.UTF_8); + } + } + throw fileIsADirectory(); + } + try { + return Files.readAllBytes(path); + } catch (NoSuchFileException e) { + throw new FileNotFoundException(); + } + } + + @Override + protected List doListElements(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var path = getZipFileSystem(uri, checksums).getPath(uri.getUri().getFragment()); + return FileResolver.listElements(path); + } + + @Override + public boolean doHasElement(PackageAssetUri uri, @Nullable Checksums checksums) + throws IOException, SecurityManagerException { + var path = getZipFileSystem(uri, checksums).getPath(uri.getUri().getFragment()); + return FileResolver.hasElement(path); + } + + @Override + public void close() throws IOException { + super.close(); + synchronized (lock) { + var cursor = fileSystems.getEntries(); + while (cursor.advance()) { + cursor.getValue().close(); + } + fileSystems.clear(); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/PackageUri.java b/pkl-core/src/main/java/org/pkl/core/packages/PackageUri.java new file mode 100644 index 00000000..7b61b30e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/PackageUri.java @@ -0,0 +1,198 @@ +/** + * 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.packages; + +import java.net.URI; +import java.net.URISyntaxException; +import org.pkl.core.PklBugException; +import org.pkl.core.Version; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; + +public class PackageUri { + private final URI uri; + private final Version version; + private final String pathWithoutVersion; + private @Nullable Checksums checksums; + + public static PackageUri create(String baseUri) { + try { + return new PackageUri(baseUri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public PackageUri(String baseUri) throws URISyntaxException { + this(new URI(baseUri)); + } + + public PackageUri(URI uri) throws URISyntaxException { + if (uri.isOpaque()) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("invalidModuleUriMissingSlash", uri, "package")); + } + var scheme = uri.getScheme(); + if (scheme == null || !(scheme.equals("package") || scheme.equals("projectpackage"))) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("invalidSchemeInPackageUri", scheme)); + } + var authority = uri.getAuthority(); + if (authority == null || authority.isEmpty()) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("missingAuthorityInPackageUri", uri)); + } + var path = uri.getPath(); + if (path == null || path.isEmpty()) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("missingPathInPackageUri", uri)); + } + var versionIdx = path.lastIndexOf('@'); + if (versionIdx == -1) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("missingVersionInPackageUri", path)); + } + this.uri = IoUtils.stripFragment(uri); + this.pathWithoutVersion = path.substring(0, versionIdx); + var checksumIdx = path.indexOf("::"); + var versionPart = path.substring(versionIdx + 1); + if (checksumIdx > versionIdx) { + var checksumPart = path.substring(checksumIdx + 2); + versionPart = path.substring(versionIdx + 1, checksumIdx); + this.checksums = parseChecksumPart(checksumPart); + } + try { + this.version = Version.parse(versionPart); + } catch (IllegalArgumentException e) { + throw new URISyntaxException(uri.toString(), e.getMessage()); + } + } + + public URI getUri() { + return uri; + } + + public URI getMetadataRequestUri() { + if (this.checksums != null) { + var schemeSpecificPart = uri.getSchemeSpecificPart(); + var checksumIdx = schemeSpecificPart.lastIndexOf("::"); + var effectiveSchemeSpecificPart = schemeSpecificPart.substring(0, checksumIdx); + return URI.create("https:" + effectiveSchemeSpecificPart); + } + return URI.create("https:" + uri.getSchemeSpecificPart()); + } + + public Version getVersion() { + return version; + } + + @Override + public String toString() { + return uri.toString(); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PackageUri that = (PackageUri) o; + return getUri().equals(that.getUri()); + } + + public PackageAssetUri toPackageAssetUri(String path) { + return new PackageAssetUri(this, path); + } + + public String getDisplayName() { + var str = toExternalPackageUri().toString(); + if (checksums != null) { + var checksumsIdx = str.lastIndexOf("::"); + return str.substring(0, checksumsIdx); + } + return str; + } + + public PackageUri toExternalPackageUri() { + if (uri.getScheme().equals("package")) { + return this; + } + try { + return new PackageUri( + new URI( + "package", + uri.getUserInfo(), + uri.getHost(), + uri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment())); + } catch (URISyntaxException e) { + throw PklBugException.unreachableCode(); + } + } + + public PackageUri toProjectPackageUri() { + if (uri.getScheme().equals("projectpackage")) { + return this; + } + try { + return new PackageUri( + new URI( + "projectpackage", + uri.getUserInfo(), + uri.getHost(), + uri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment())); + } catch (URISyntaxException e) { + throw PklBugException.unreachableCode(); + } + } + + public String getPathWithoutVersion() { + return pathWithoutVersion; + } + + public @Nullable Checksums getChecksums() { + return checksums; + } + + private Checksums parseChecksumPart(String checksumPart) throws URISyntaxException { + var parts = checksumPart.split(":"); + if (parts.length != 2) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("invalidPackageUriChecksum", checksumPart)); + } + var algorithm = parts[0]; + var checksum = parts[1]; + if (!algorithm.equals("sha256")) { + throw new URISyntaxException( + uri.toString(), ErrorMessages.create("unknownChecksumAlgorithm", algorithm)); + } + return new Checksums(checksum); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/PackageUtils.java b/pkl-core/src/main/java/org/pkl/core/packages/PackageUtils.java new file mode 100644 index 00000000..797905df --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/PackageUtils.java @@ -0,0 +1,40 @@ +/** + * 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.packages; + +import java.net.URISyntaxException; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.json.Json.FormatException; +import org.pkl.core.util.json.Json.JsonParseException; + +public class PackageUtils { + public static PackageUri parsePackageUriWithoutChecksums(Object obj) + throws JsonParseException, URISyntaxException { + if (!(obj instanceof String)) { + throw new FormatException("string", obj.getClass()); + } + var packageUri = new PackageUri((String) obj); + checkHasNoChecksumComponent(packageUri); + return packageUri; + } + + public static void checkHasNoChecksumComponent(PackageUri packageUri) throws URISyntaxException { + if (packageUri.getChecksums() != null) { + throw new URISyntaxException( + packageUri.toString(), ErrorMessages.create("unexpectedChecksumInPackageUri")); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/package-info.java b/pkl-core/src/main/java/org/pkl/core/packages/package-info.java new file mode 100644 index 00000000..69482949 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/packages/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.packages; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/parser/ErrorStrategy.java b/pkl-core/src/main/java/org/pkl/core/parser/ErrorStrategy.java new file mode 100644 index 00000000..7503f55a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/parser/ErrorStrategy.java @@ -0,0 +1,120 @@ +/** + * 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.parser; + +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.misc.IntervalSet; +import org.pkl.core.parser.antlr.PklParser; + +class ErrorStrategy extends DefaultErrorStrategy { + @Override + protected void reportNoViableAlternative(Parser parser, NoViableAltException e) { + var builder = new StringBuilder(); + var offendingToken = e.getOffendingToken(); + + if (Lexer.isKeyword(offendingToken)) { + appendKeywordNotAllowedMessage(builder, e.getOffendingToken(), e.getExpectedTokens()); + } else { + builder.append("No viable alternative at input "); + var tokens = parser.getInputStream(); + if (e.getStartToken().getType() == Token.EOF) { + builder.append(""); + } else { + builder.append(escapeWSAndQuote(tokens.getText(e.getStartToken(), offendingToken))); + } + } + + parser.notifyErrorListeners(offendingToken, builder.toString(), e); + } + + @Override + protected void reportInputMismatch(Parser parser, InputMismatchException e) { + var builder = new StringBuilder(); + var offendingToken = e.getOffendingToken(); + + if (Lexer.isKeyword(offendingToken)) { + appendKeywordNotAllowedMessage(builder, e.getOffendingToken(), e.getExpectedTokens()); + } else { + // improve formatting compared to DefaultErrorStrategy + builder + .append("Mismatched input: ") + .append(getTokenErrorDisplay(offendingToken)) + .append(". "); + appendExpectedTokensMessage(builder, parser); + } + + parser.notifyErrorListeners(offendingToken, builder.toString(), e); + } + + // improve formatting compared to DefaultErrorStrategy + @Override + protected void reportUnwantedToken(Parser parser) { + if (inErrorRecoveryMode(parser)) return; + + beginErrorCondition(parser); + var builder = new StringBuilder(); + var currentToken = parser.getCurrentToken(); + + if (Lexer.isKeyword(currentToken)) { + appendKeywordNotAllowedMessage(builder, currentToken, parser.getExpectedTokens()); + } else { + builder.append("Extraneous input: ").append(getTokenErrorDisplay(currentToken)).append(". "); + appendExpectedTokensMessage(builder, parser); + } + + parser.notifyErrorListeners(currentToken, builder.toString(), null); + } + + // improve formatting compared to DefaultErrorStrategy + protected void reportMissingToken(Parser parser) { + if (inErrorRecoveryMode(parser)) return; + + beginErrorCondition(parser); + var builder = new StringBuilder(); + var currentToken = parser.getCurrentToken(); + + var expecting = getExpectedTokens(parser); + builder + .append("Missing ") + .append(expecting.toString(parser.getVocabulary())) + .append(" at ") + .append(getTokenErrorDisplay(currentToken)) + .append(". "); + + parser.notifyErrorListeners(currentToken, builder.toString(), null); + } + + private void appendExpectedTokensMessage(StringBuilder builder, Parser parser) { + var expectedTokens = parser.getExpectedTokens(); + var size = expectedTokens.size(); + if (size == 0) return; + + builder.append(size == 1 ? "Expected: " : "Expected one of: "); + var msg = expectedTokens.toString(parser.getVocabulary()); + if (msg.startsWith("{")) msg = msg.substring(1); + if (msg.endsWith("}")) msg = msg.substring(0, msg.length() - 1); + builder.append(msg); + } + + private void appendKeywordNotAllowedMessage( + StringBuilder builder, Token offendingToken, IntervalSet expectedTokens) { + builder.append("Keyword `").append(offendingToken.getText()).append("` is not allowed here."); + if (expectedTokens.contains(PklParser.Identifier)) { + builder.append(" (If you must use this name as identifier, enclose it in backticks.)"); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/parser/LexParseException.java b/pkl-core/src/main/java/org/pkl/core/parser/LexParseException.java new file mode 100644 index 00000000..159c3684 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/parser/LexParseException.java @@ -0,0 +1,95 @@ +/** + * 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.parser; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; + +public abstract class LexParseException extends RuntimeException { + // line of the error's start position, 1-based + private final int line; + + // column of the error's start position, 1-based + private final int column; + + // number of characters, starting from line/column, belonging to the offending token + private final int length; + + private final int relevance; + + private @Nullable ParserRuleContext partialParseResult; + + public LexParseException(String message, int line, int column, int length, int relevance) { + super(format(message)); + this.line = line; + this.column = column; + this.length = length; + this.relevance = relevance; + partialParseResult = null; + } + + public int getLine() { + return line; + } + + public int getColumn() { + return column; + } + + public int getLength() { + return length; + } + + public int getRelevance() { + return relevance; + } + + public @Nullable ParserRuleContext getPartialParseResult() { + return partialParseResult; + } + + public LexParseException withPartialParseResult(ParserRuleContext partialParseResult) { + this.partialParseResult = partialParseResult; + return this; + } + + public static class LexError extends LexParseException { + public LexError(String message, int line, int column, int length) { + super(message, line, column, length, Integer.MAX_VALUE); + } + } + + public static class ParseError extends LexParseException { + public ParseError(String message, int line, int column, int length, int relevance) { + super(message, line, column, length, relevance); + } + } + + public static class IncompleteInput extends ParseError { + public IncompleteInput(String message, int line, int column, int length) { + super(message, line, column, length, Integer.MAX_VALUE - 1); + } + } + + // format ANTLR error messages like Pkl's own error messages + private static String format(String msg) { + var result = IoUtils.capitalize(msg); + result = result.replace("'", "`"); + if (!result.contains(":") && !result.endsWith(")") && !result.endsWith(".")) result += "."; + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/parser/Lexer.java b/pkl-core/src/main/java/org/pkl/core/parser/Lexer.java new file mode 100644 index 00000000..e9196810 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/parser/Lexer.java @@ -0,0 +1,98 @@ +/** + * 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.parser; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import org.antlr.v4.runtime.*; +import org.pkl.core.parser.antlr.PklLexer; +import org.pkl.core.util.Nullable; + +public class Lexer { + public static final Set KEYWORD_TYPES; + public static final Set KEYWORD_NAMES; + + static { + var keywordTypes = new HashSet(); + var keywordNames = new HashSet(); + var vocabulary = PklLexer.VOCABULARY; + + for (var i = 0; i <= vocabulary.getMaxTokenType(); i++) { + var literal = vocabulary.getLiteralName(i); + if (literal == null) continue; + // remove leading and trailing quotes + literal = literal.substring(1, literal.length() - 1); + if (Character.isLetter(literal.charAt(0)) || literal.equals("_")) { + keywordTypes.add(i); + keywordNames.add(literal); + } + } + + KEYWORD_TYPES = Collections.unmodifiableSet(keywordTypes); + KEYWORD_NAMES = Collections.unmodifiableSet(keywordNames); + } + + @TruffleBoundary + public static PklLexer createLexer(CharStream source) { + var lexer = new PklLexer(source); + lexer.removeErrorListeners(); + lexer.addErrorListener( + new ANTLRErrorListener<>() { + @Override + public void syntaxError( + Recognizer recognizer, + T offendingSymbol, + int line, + int charPositionInLine, + String msg, + RecognitionException e) { + var lexer = ((org.antlr.v4.runtime.Lexer) recognizer); + throw new LexParseException.LexError( + msg, + line, + charPositionInLine + 1, + lexer._input.index() - lexer._tokenStartCharIndex); + } + }); + return lexer; + } + + @TruffleBoundary + public static boolean isKeyword(@Nullable Token token) { + return token != null && KEYWORD_TYPES.contains(token.getType()); + } + + @TruffleBoundary + public static boolean isRegularIdentifier(String identifier) { + if (identifier.isEmpty()) return false; + + if (KEYWORD_NAMES.contains(identifier)) return false; + + var firstCp = identifier.codePointAt(0); + return (firstCp == '$' || firstCp == '_' || Character.isUnicodeIdentifierStart(firstCp)) + && identifier + .codePoints() + .skip(1) + .allMatch(cp -> cp == '$' || Character.isUnicodeIdentifierPart(cp)); + } + + @TruffleBoundary + public static String maybeQuoteIdentifier(String identifier) { + return isRegularIdentifier(identifier) ? identifier : "`" + identifier + "`"; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/parser/Parser.java b/pkl-core/src/main/java/org/pkl/core/parser/Parser.java new file mode 100644 index 00000000..9a477666 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/parser/Parser.java @@ -0,0 +1,232 @@ +/** + * 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.parser; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.atn.PredictionMode; +import org.antlr.v4.runtime.tree.ParseTree; +import org.pkl.core.parser.LexParseException.IncompleteInput; +import org.pkl.core.parser.antlr.PklLexer; +import org.pkl.core.parser.antlr.PklParser; +import org.pkl.core.parser.antlr.PklParser.*; +import org.pkl.core.util.Nullable; + +public class Parser { + @TruffleBoundary + public PklParser createParser( + TokenStream stream, @Nullable List errorCollector) { + var parser = new PklParser(stream); + parser.setErrorHandler(new ErrorStrategy()); + registerErrorListener(parser, errorCollector); + return parser; + } + + @TruffleBoundary + public ModuleContext parseModule(CharStream source) throws LexParseException { + return parseProduction(source, PklParser::module); + } + + @TruffleBoundary + public ModuleContext parseModule(String source) throws LexParseException { + return parseModule(toCharStream(source)); + } + + @TruffleBoundary + public ReplInputContext parseReplInput(CharStream source) throws LexParseException { + var ctx = parseProduction(source, PklParser::replInput); + checkIsCompleteInput(ctx); + return ctx; + } + + @TruffleBoundary + public ReplInputContext parseReplInput(String source) throws LexParseException { + return parseReplInput(toCharStream(source)); + } + + @TruffleBoundary + public ExprInputContext parseExpressionInput(CharStream source) throws LexParseException { + var ctx = parseProduction(source, PklParser::exprInput); + checkIsCompleteInput(ctx); + return ctx; + } + + /** + * Two-step parse as recommended in chapter "Maximizing Parser Speed" of "The Definitive ANTLR 4 + * Reference, 2nd Ed". + */ + @TruffleBoundary + public T parseProduction( + CharStream source, Function production) throws LexParseException { + var lexer = Lexer.createLexer(source); + var errorCollector = new ArrayList(); + var parser = createParser(new CommonTokenStream(lexer), errorCollector); + // TODO: investigate why SLL is often not enough to parse Pkl code + parser.getInterpreter().setPredictionMode(PredictionMode.SLL); + + var result = production.apply(parser); + + // TODO: only necessary to retry for parse (vs. lex) errors? + if (!errorCollector.isEmpty()) { + errorCollector.clear(); + parser.reset(); + parser.getInterpreter().setPredictionMode(PredictionMode.LL); + result = production.apply(parser); + } + + var mostRelevant = + errorCollector.stream().max(Comparator.comparingInt(LexParseException::getRelevance)); + if (mostRelevant.isPresent()) { + throw mostRelevant.get().withPartialParseResult(result); + } + + return result; + } + + @TruffleBoundary + public ExprInputContext parseExpressionInput(String source) throws LexParseException { + return parseExpressionInput(toCharStream(source)); + } + + @SuppressWarnings("deprecation") + private CharStream toCharStream(String source) { + // `ANTLRInputStream` has been deprecated and should be replaced with `CharStreams.ofString()`. + // It seems that the bugs we formerly encountered with `CharStreams.ofString()` are fixed in + // 4.7.2. + // However, switching to `CharStreams.ofString()` means that ANTLR's column numbers are measured + // in number of code points, + // which makes them incompatible with Truffle's `SourceSection` (which uses number of code + // units). + return new ANTLRInputStream(source); + } + + // To improve error reporting, missing closing delimiters + // are tolerated by the grammar and only caught in AstBuilder. + // This method compensates by flagging a missing closing delimiter. + private void checkIsCompleteInput(ParserRuleContext ctx) { + if (ctx.getChildCount() == 1) return; // EOF + + var curr = ctx.getChild(ctx.getChildCount() - 2); // last child before EOF + while (curr.getChildCount() > 0) { + if (curr instanceof ClassBodyContext) { + if (((ClassBodyContext) curr).err == null) throw incompleteInput(curr, "}"); + else return; + } + if (curr instanceof ParameterListContext) { + if (((ParameterListContext) curr).err == null) throw incompleteInput(curr, ")"); + else return; + } + if (curr instanceof ArgumentListContext) { + if (((ArgumentListContext) curr).err == null) throw incompleteInput(curr, ")"); + else return; + } + if (curr instanceof TypeParameterListContext) { + if (((TypeParameterListContext) curr).err == null) throw incompleteInput(curr, ">"); + else return; + } + if (curr instanceof TypeArgumentListContext) { + if (((TypeArgumentListContext) curr).err == null) throw incompleteInput(curr, ">"); + else return; + } + if (curr instanceof ParenthesizedTypeContext) { + if (((ParenthesizedTypeContext) curr).err == null) throw incompleteInput(curr, ")"); + else return; + } + if (curr instanceof ConstrainedTypeContext) { + if (((ConstrainedTypeContext) curr).err == null) throw incompleteInput(curr, ")"); + else return; + } + if (curr instanceof ParenthesizedExprContext) { + if (((ParenthesizedExprContext) curr).err == null) throw incompleteInput(curr, ")"); + else return; + } + if (curr instanceof SuperSubscriptExprContext) { + if (((SuperSubscriptExprContext) curr).err == null) throw incompleteInput(curr, "]"); + else return; + } + if (curr instanceof SubscriptExprContext) { + if (((SubscriptExprContext) curr).err == null) throw incompleteInput(curr, "]"); + else return; + } + if (curr instanceof ObjectBodyContext) { + if (((ObjectBodyContext) curr).err == null) throw incompleteInput(curr, "}"); + else return; + } + curr = curr.getChild(curr.getChildCount() - 1); + } + } + + private void registerErrorListener( + PklParser parser, @Nullable List errorCollector) { + parser.removeErrorListeners(); + parser.addErrorListener( + new BaseErrorListener() { + @Override + public void syntaxError( + Recognizer recognizer, + T offendingToken, + int line, + int charPositionInLine, + String msg, + @Nullable RecognitionException e) { + assert charPositionInLine == offendingToken.getCharPositionInLine(); + var length = offendingToken.getStopIndex() - offendingToken.getStartIndex() + 1; + + LexParseException exception; + // For incomplete input similar to `foo { bar {`, e can (at least) be null, + // NoViableAltException, or InputMismatchException. Therefore, just check for EOF. + if (offendingToken.getType() == PklLexer.EOF) { + exception = + new LexParseException.IncompleteInput(msg, line, charPositionInLine + 1, length); + } else { + exception = + new LexParseException.ParseError( + msg, line, charPositionInLine + 1, length, getAstDepth(e)); + } + + if (errorCollector != null) { + errorCollector.add(exception); + } else { + throw exception; + } + } + }); + } + + private LexParseException incompleteInput(ParseTree tree, String missingDelimiter) { + var ctx = (ParserRuleContext) tree; + return new IncompleteInput( + "Missing closing delimiter `" + missingDelimiter + "`.", + ctx.stop.getLine(), + ctx.stop.getCharPositionInLine() + 1, + ctx.stop.getStopIndex() - ctx.stop.getStartIndex() + 1); + } + + private static int getAstDepth(@Nullable RecognitionException e) { + if (e == null) return 0; + + var depth = 0; + for (var context = e.getContext(); context != null; context = context.getParent()) { + depth += 1; + } + + return depth; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/parser/package-info.java b/pkl-core/src/main/java/org/pkl/core/parser/package-info.java new file mode 100644 index 00000000..e2521ac4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/parser/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.parser; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/project/CanonicalPackageUri.java b/pkl-core/src/main/java/org/pkl/core/project/CanonicalPackageUri.java new file mode 100644 index 00000000..669f6ca7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/CanonicalPackageUri.java @@ -0,0 +1,110 @@ +/** + * 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.project; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; +import org.pkl.core.PklBugException; +import org.pkl.core.packages.PackageUri; +import org.pkl.core.util.ErrorMessages; + +/** + * The canonical name of a package dependency within a project. + * + *

Equivalent to the package's base URI, and the major version number, i.e. {@code + * package://example.com/foo/bar@0}. Does not include a URI's userinfo, query params or fragment + * segments. + */ +public class CanonicalPackageUri { + private final URI baseUri; + private final int majorVersion; + + public static CanonicalPackageUri fromPackageUri(PackageUri packageUri) { + var uri = packageUri.getUri(); + URI baseUri; + try { + baseUri = + new URI( + // make sure scheme is always "package" + "package", + // userinfo isn't considered part of the package identifier. + null, + uri.getHost(), + uri.getPort(), + packageUri.getPathWithoutVersion(), + // query params aren't considered part of the package identifier. + null, + null); + } catch (URISyntaxException e) { + throw PklBugException.unreachableCode(); + } + return new CanonicalPackageUri(baseUri, packageUri.getVersion().getMajor()); + } + + public static CanonicalPackageUri of(String uriStr) throws URISyntaxException { + var versionIdx = uriStr.lastIndexOf('@'); + if (versionIdx == -1) { + throw new URISyntaxException( + uriStr, ErrorMessages.create("missingVersionInPackageUri", uriStr)); + } + int majorVersion; + try { + majorVersion = Integer.parseInt(uriStr.substring(versionIdx + 1)); + } catch (NumberFormatException e) { + throw new URISyntaxException(uriStr, ErrorMessages.create("")); + } + var baseUri = new URI(uriStr.substring(0, versionIdx)); + return new CanonicalPackageUri(baseUri, majorVersion); + } + + public CanonicalPackageUri(URI baseUri, int majorVersion) { + this.baseUri = baseUri; + this.majorVersion = majorVersion; + } + + @SuppressWarnings("unused") + public int getMajorVersion() { + return majorVersion; + } + + @SuppressWarnings("unused") + public URI getBaseUri() { + return baseUri; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CanonicalPackageUri that = (CanonicalPackageUri) o; + return majorVersion == that.majorVersion && baseUri.equals(that.baseUri); + } + + @Override + public int hashCode() { + return Objects.hash(baseUri, majorVersion); + } + + @Override + public String toString() { + return baseUri + "@" + majorVersion; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/project/DeclaredDependencies.java b/pkl-core/src/main/java/org/pkl/core/project/DeclaredDependencies.java new file mode 100644 index 00000000..702994d5 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/DeclaredDependencies.java @@ -0,0 +1,56 @@ +/** + * 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.project; + +import java.net.URI; +import java.util.Map; +import org.pkl.core.packages.Dependency.RemoteDependency; +import org.pkl.core.packages.PackageUri; +import org.pkl.core.util.Nullable; + +public class DeclaredDependencies { + private final Map remoteDependencies; + private final Map localDependencies; + private final URI projectFileUri; + private final @Nullable PackageUri myPackageUri; + + public DeclaredDependencies( + Map remoteDependencies, + Map localDependencies, + URI projectFileUri, + @Nullable PackageUri myPackageUri) { + this.remoteDependencies = remoteDependencies; + this.localDependencies = localDependencies; + this.projectFileUri = projectFileUri; + this.myPackageUri = myPackageUri; + } + + public Map getLocalDependencies() { + return localDependencies; + } + + public Map getRemoteDependencies() { + return remoteDependencies; + } + + public URI getProjectFileUri() { + return projectFileUri; + } + + public @Nullable PackageUri getMyPackageUri() { + return myPackageUri; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/project/Package.java b/pkl-core/src/main/java/org/pkl/core/project/Package.java new file mode 100644 index 00000000..dfb5ad5d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/Package.java @@ -0,0 +1,226 @@ +/** + * 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.project; + +import java.net.URI; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import org.pkl.core.Version; +import org.pkl.core.packages.PackageUri; +import org.pkl.core.util.Nullable; + +/** Java representation of class {@code pkl.Project#Package} */ +@SuppressWarnings("unused") +public final class Package { + + private final String name; + private final PackageUri uri; + private final Version version; + private final URI packageZipUrl; + private final @Nullable String description; + private final List authors; + private final @Nullable URI website; + private final @Nullable URI documentation; + private final @Nullable URI sourceCode; + private final @Nullable String sourceCodeUrlScheme; + private final @Nullable String license; + private final @Nullable String licenseText; + private final @Nullable URI issueTracker; + private final List apiTests; + private final List exclude; + + public Package( + String name, + PackageUri uri, + Version version, + URI packageZipUrl, + @Nullable String description, + List authors, + @Nullable URI website, + @Nullable URI documentation, + @Nullable URI sourceCode, + @Nullable String sourceCodeUrlScheme, + @Nullable String license, + @Nullable String licenseText, + @Nullable URI issueTracker, + List apiTests, + List exclude) { + this.name = name; + this.uri = uri; + this.version = version; + this.packageZipUrl = packageZipUrl; + this.description = description; + this.authors = authors; + this.website = website; + this.documentation = documentation; + this.sourceCode = sourceCode; + this.sourceCodeUrlScheme = sourceCodeUrlScheme; + this.license = license; + this.licenseText = licenseText; + this.issueTracker = issueTracker; + this.apiTests = apiTests; + this.exclude = exclude; + } + + public String getName() { + return name; + } + + public PackageUri getUri() { + return uri; + } + + public Version getVersion() { + return version; + } + + public URI getPackageZipUrl() { + return packageZipUrl; + } + + public @Nullable String getDescription() { + return description; + } + + public List getAuthors() { + return authors; + } + + public @Nullable URI getWebsite() { + return website; + } + + public @Nullable URI getDocumentation() { + return documentation; + } + + public @Nullable URI getSourceCode() { + return sourceCode; + } + + public @Nullable String getSourceCodeUrlScheme() { + return sourceCodeUrlScheme; + } + + public @Nullable String getLicenseText() { + return licenseText; + } + + public @Nullable String getLicense() { + return license; + } + + public @Nullable URI getIssueTracker() { + return issueTracker; + } + + public List getApiTests() { + return apiTests; + } + + public List getExclude() { + return exclude; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Package aPackage = (Package) o; + return name.equals(aPackage.name) + && uri.equals(aPackage.uri) + && version.equals(aPackage.version) + && Objects.equals(description, aPackage.description) + && authors.equals(aPackage.authors) + && Objects.equals(website, aPackage.website) + && Objects.equals(documentation, aPackage.documentation) + && Objects.equals(sourceCode, aPackage.sourceCode) + && Objects.equals(sourceCodeUrlScheme, aPackage.sourceCodeUrlScheme) + && Objects.equals(license, aPackage.license) + && Objects.equals(licenseText, aPackage.licenseText) + && Objects.equals(issueTracker, aPackage.issueTracker) + && Objects.equals(apiTests, aPackage.apiTests) + && exclude.equals(aPackage.exclude); + } + + @Override + public int hashCode() { + return Objects.hash( + name, + uri, + version, + description, + authors, + website, + documentation, + sourceCode, + sourceCodeUrlScheme, + license, + licenseText, + issueTracker, + apiTests, + exclude); + } + + @Override + public String toString() { + return "Package{" + + "name=" + + name + + ", uri=" + + uri + + ", version='" + + version + + '\'' + + ", description='" + + description + + '\'' + + ", authors=" + + authors + + ", website='" + + website + + ", documentation='" + + documentation + + '\'' + + ", sourceCode='" + + sourceCode + + '\'' + + ", sourceCodeUrlScheme='" + + sourceCodeUrlScheme + + '\'' + + ", license='" + + license + + '\'' + + ", licenseText='" + + licenseText + + '\'' + + ", issueTracker='" + + issueTracker + + '\'' + + ", apiTests='" + + apiTests + + '\'' + + ", exclude='" + + exclude + + '\'' + + '}'; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/project/Project.java b/pkl-core/src/main/java/org/pkl/core/project/Project.java new file mode 100644 index 00000000..3a311a22 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/Project.java @@ -0,0 +1,525 @@ +/** + * 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.project; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.pkl.core.Composite; +import org.pkl.core.Duration; +import org.pkl.core.EvaluatorBuilder; +import org.pkl.core.ModuleSource; +import org.pkl.core.PClassInfo; +import org.pkl.core.PNull; +import org.pkl.core.PObject; +import org.pkl.core.PklException; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagers; +import org.pkl.core.StackFrameTransformer; +import org.pkl.core.StackFrameTransformers; +import org.pkl.core.Version; +import org.pkl.core.module.ModuleKeyFactories; +import org.pkl.core.packages.Checksums; +import org.pkl.core.packages.Dependency.RemoteDependency; +import org.pkl.core.packages.PackageUri; +import org.pkl.core.packages.PackageUtils; +import org.pkl.core.resource.ResourceReaders; +import org.pkl.core.util.Nullable; + +/** Java representation of module {@code pkl.Project}. */ +public final class Project { + private final @Nullable Package pkg; + private final DeclaredDependencies dependencies; + private final EvaluatorSettings evaluatorSettings; + private final URI projectFileUri; + private final Path projectDir; + private final List tests; + private final Map localProjectDependencies; + + /** + * Loads Project data from the given {@link Path}. + * + *

Evaluates a module's {@code output.value} to allow for embedding a project within a + * template. + * + * @throws PklException if an error occured while evaluating the project file. + */ + public static Project loadFromPath( + Path path, + SecurityManager securityManager, + java.time.@Nullable Duration timeout, + StackFrameTransformer stackFrameTransformer, + Map envVars) { + try (var evaluator = + EvaluatorBuilder.unconfigured() + .setSecurityManager(securityManager) + .setStackFrameTransformer(stackFrameTransformer) + .addModuleKeyFactory(ModuleKeyFactories.standardLibrary) + .addModuleKeyFactory(ModuleKeyFactories.file) + .addResourceReader(ResourceReaders.environmentVariable()) + .addResourceReader(ResourceReaders.file()) + .addEnvironmentVariables(envVars) + .setTimeout(timeout) + .build()) { + var output = evaluator.evaluateOutputValueAs(ModuleSource.path(path), PClassInfo.Project); + return Project.parseProject(output); + } catch (URISyntaxException e) { + throw new PklException(e.getMessage(), e); + } + } + + /** Convenience method to load a project with the default stack frame transformer. */ + public static Project loadFromPath( + Path path, SecurityManager securityManager, java.time.@Nullable Duration timeout) { + return loadFromPath( + path, securityManager, timeout, StackFrameTransformers.defaultTransformer, System.getenv()); + } + + /** + * Convenience method to load a project with the default security manager, no timeout, and the + * default stack frame transformer. + */ + public static Project loadFromPath(Path path) { + return loadFromPath(path, SecurityManagers.defaultManager, null); + } + + private static DeclaredDependencies parseDependencies( + PObject module, URI projectFileUri, @Nullable PackageUri packageUri) + throws URISyntaxException { + var remoteDependencies = new HashMap(); + var localDependencies = new HashMap(); + //noinspection unchecked + var dependencies = (Map) module.getProperty("dependencies"); + for (var entry : dependencies.entrySet()) { + var value = entry.getValue(); + if (value.getClassInfo().equals(PClassInfo.Project)) { + var localProjectFileUri = URI.create((String) value.getProperty("projectFileUri")); + var localPkgUri = + PackageUri.create((String) ((PObject) value.getProperty("package")).getProperty("uri")); + localDependencies.put( + entry.getKey(), parseDependencies(value, localProjectFileUri, localPkgUri)); + } else { + remoteDependencies.put(entry.getKey(), parseRemoteDependency(value)); + } + } + return new DeclaredDependencies( + remoteDependencies, localDependencies, projectFileUri, packageUri); + } + + private static RemoteDependency parseRemoteDependency(PObject object) throws URISyntaxException { + var packageUri = new PackageUri((String) object.getProperty("uri")); + PackageUtils.checkHasNoChecksumComponent(packageUri); + var objChecksum = object.getProperty("checksums"); + Checksums checksums = null; + if (objChecksum instanceof PObject) { + var sha256 = (String) ((PObject) objChecksum).get("sha256"); + assert sha256 != null; + checksums = new Checksums(sha256); + } + return new RemoteDependency(packageUri, checksums); + } + + public static Project parseProject(PObject module) throws URISyntaxException { + var pkgObj = getNullableProperty(module, "package"); + var projectFileUri = URI.create((String) module.getProperty("projectFileUri")); + var dependencies = parseDependencies(module, projectFileUri, null); + var projectDir = Path.of(projectFileUri).getParent(); + Package pkg = null; + if (pkgObj != null) { + pkg = parsePackage((PObject) pkgObj); + } + var evaluatorSettings = + getProperty( + module, + "evaluatorSettings", + (settings) -> parseEvaluatorSettings(settings, projectDir)); + @SuppressWarnings("unchecked") + var testPathStrs = (List) getProperty(module, "tests"); + var tests = + testPathStrs.stream() + .map((it) -> projectDir.resolve(it).normalize()) + .collect(Collectors.toList()); + var localProjectDependencies = parseLocalProjectDependencies(module); + return new Project( + pkg, + dependencies, + evaluatorSettings, + projectFileUri, + projectDir, + tests, + localProjectDependencies); + } + + private static Map parseLocalProjectDependencies(PObject module) + throws URISyntaxException { + //noinspection unchecked + var dependencies = (Map) module.getProperty("dependencies"); + var result = new HashMap(); + for (var entry : dependencies.entrySet()) { + var value = entry.getValue(); + if (value.getClassInfo().equals(PClassInfo.Project)) { + result.put(entry.getKey(), parseProject(entry.getValue())); + } + } + return result; + } + + @SuppressWarnings("unchecked") + private static EvaluatorSettings parseEvaluatorSettings(Object settings, Path projectDir) { + var pSettings = (PObject) settings; + var externalProperties = getNullableProperty(pSettings, "externalProperties", Project::asMap); + var env = getNullableProperty(pSettings, "env", Project::asMap); + var allowedModules = getNullableProperty(pSettings, "allowedModules", Project::asPatternList); + var allowedResources = + getNullableProperty(pSettings, "allowedResources", Project::asPatternList); + var noCache = (Boolean) getNullableProperty(pSettings, "noCache"); + var modulePathStrs = (List) getNullableProperty(pSettings, "modulePath"); + List modulePath = null; + if (modulePathStrs != null) { + modulePath = + modulePathStrs.stream() + .map((it) -> projectDir.resolve(it).normalize()) + .collect(Collectors.toList()); + } + var timeout = (Duration) getNullableProperty(pSettings, "timeout"); + var moduleCacheDir = getNullablePath(pSettings, "moduleCacheDir", projectDir); + var rootDir = getNullablePath(pSettings, "rootDir", projectDir); + return new EvaluatorSettings( + externalProperties, + env, + allowedModules, + allowedResources, + noCache, + moduleCacheDir, + modulePath, + timeout, + rootDir); + } + + @SuppressWarnings("unchecked") + private static Map asMap(Object t) { + assert t instanceof Map; + return (Map) t; + } + + @SuppressWarnings("unchecked") + private static List asPatternList(Object t) { + return ((List) t).stream().map(Pattern::compile).collect(Collectors.toList()); + } + + private static Object getProperty(PObject settings, String propertyName) { + return settings.getProperty(propertyName); + } + + private static T getProperty(PObject settings, String propertyName, Function f) { + return Objects.requireNonNull(getNullableProperty(settings, propertyName, f)); + } + + private static @Nullable Object getNullableProperty(Composite object, String propertyName) { + var result = object.getPropertyOrNull(propertyName); + if (result instanceof PNull || result == null) { + return null; + } + return result; + } + + private static @Nullable T getNullableProperty( + Composite object, String propertyName, Function f) { + var value = object.getPropertyOrNull(propertyName); + if (value instanceof PNull || value == null) { + return null; + } + return f.apply(value); + } + + private static @Nullable URI getNullableURI(Composite object, String propertyName) + throws URISyntaxException { + var value = object.getPropertyOrNull(propertyName); + if (value instanceof PNull || value == null) { + return null; + } + return new URI((String) value); + } + + private static @Nullable Path getNullablePath( + Composite object, String propertyName, Path projectDir) { + return getNullableProperty( + object, propertyName, (obj) -> projectDir.resolve((String) obj).normalize()); + } + + @SuppressWarnings("unchecked") + private static Package parsePackage(PObject pObj) throws URISyntaxException { + var name = (String) pObj.getProperty("name"); + var uri = new PackageUri((String) pObj.getProperty("uri")); + var version = Version.parse((String) getProperty(pObj, "version")); + var packageZipUrl = new URI((String) getProperty(pObj, "packageZipUrl")); + var description = (String) getNullableProperty(pObj, "description"); + var authors = (List) getProperty(pObj, "authors"); + var website = getNullableURI(pObj, "website"); + var documentation = getNullableURI(pObj, "documentation"); + var sourceCode = getNullableURI(pObj, "sourceCode"); + var sourceCodeUrlScheme = (String) getNullableProperty(pObj, "sourceCodeUrlScheme"); + var license = (String) getNullableProperty(pObj, "license"); + var licenseText = (String) getNullableProperty(pObj, "licenseText"); + var issueTracker = (URI) getNullableURI(pObj, "issueTracker"); + var apiTestStrs = (List) getProperty(pObj, "apiTests"); + var apiTests = apiTestStrs.stream().map(Path::of).collect(Collectors.toList()); + var exclude = (List) getProperty(pObj, "exclude"); + + return new Package( + name, + uri, + version, + packageZipUrl, + description, + authors, + website, + documentation, + sourceCode, + sourceCodeUrlScheme, + license, + licenseText, + issueTracker, + apiTests, + exclude); + } + + private Project( + @Nullable Package pkg, + DeclaredDependencies dependencies, + EvaluatorSettings evaluatorSettings, + URI projectFileUri, + Path projectDir, + List tests, + Map localProjectDependencies) { + this.pkg = pkg; + this.dependencies = dependencies; + this.evaluatorSettings = evaluatorSettings; + this.projectFileUri = projectFileUri; + this.projectDir = projectDir; + this.tests = tests; + this.localProjectDependencies = localProjectDependencies; + } + + public @Nullable Package getPackage() { + return pkg; + } + + public EvaluatorSettings getSettings() { + return evaluatorSettings; + } + + public URI getProjectFileUri() { + return projectFileUri; + } + + public List getTests() { + return tests; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Project project = (Project) o; + return Objects.equals(pkg, project.pkg) + && dependencies.equals(project.dependencies) + && evaluatorSettings.equals(project.evaluatorSettings) + && projectFileUri.equals(project.projectFileUri) + && tests.equals(project.tests); + } + + @Override + public int hashCode() { + return Objects.hash(pkg, dependencies, evaluatorSettings, projectFileUri, tests); + } + + public DeclaredDependencies getDependencies() { + return dependencies; + } + + public Map getLocalProjectDependencies() { + return localProjectDependencies; + } + + public Path getProjectDir() { + return projectDir; + } + + public static class EvaluatorSettings { + private final @Nullable Map externalProperties; + private final @Nullable Map env; + private final @Nullable List allowedModules; + private final @Nullable List allowedResources; + private final @Nullable Boolean noCache; + private final @Nullable Path moduleCacheDir; + private final @Nullable List modulePath; + private final @Nullable Duration timeout; + private final @Nullable Path rootDir; + + public EvaluatorSettings( + @Nullable Map externalProperties, + @Nullable Map env, + @Nullable List allowedModules, + @Nullable List allowedResources, + @Nullable Boolean noCache, + @Nullable Path moduleCacheDir, + @Nullable List modulePath, + @Nullable Duration timeout, + @Nullable Path rootDir) { + this.externalProperties = externalProperties; + this.env = env; + this.allowedModules = allowedModules; + this.allowedResources = allowedResources; + this.noCache = noCache; + this.moduleCacheDir = moduleCacheDir; + this.modulePath = modulePath; + this.timeout = timeout; + this.rootDir = rootDir; + } + + public @Nullable Map getExternalProperties() { + return externalProperties; + } + + public @Nullable Map getEnv() { + return env; + } + + public @Nullable List getAllowedModules() { + return allowedModules; + } + + public @Nullable List getAllowedResources() { + return allowedResources; + } + + public @Nullable Boolean isNoCache() { + return noCache; + } + + public @Nullable List getModulePath() { + return modulePath; + } + + public @Nullable Duration getTimeout() { + return timeout; + } + + public @Nullable Path getModuleCacheDir() { + return moduleCacheDir; + } + + public @Nullable Path getRootDir() { + return rootDir; + } + + private boolean arePatternsEqual( + @Nullable List myPattern, @Nullable List thatPattern) { + if (myPattern == null) { + return thatPattern == null; + } + if (thatPattern == null) { + return false; + } + if (myPattern.size() != thatPattern.size()) { + return false; + } + for (var i = 0; i < myPattern.size(); i++) { + if (!myPattern.get(i).pattern().equals(thatPattern.get(i).pattern())) { + return false; + } + } + return true; + } + + private int hashPatterns(@Nullable List patterns) { + if (patterns == null) { + return 0; + } + var ret = 1; + for (var pattern : patterns) { + ret = 31 * ret + pattern.pattern().hashCode(); + } + return ret; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EvaluatorSettings that = (EvaluatorSettings) o; + return Objects.equals(externalProperties, that.externalProperties) + && Objects.equals(env, that.env) + && arePatternsEqual(allowedModules, that.allowedModules) + && arePatternsEqual(allowedResources, that.allowedResources) + && Objects.equals(noCache, that.noCache) + && Objects.equals(moduleCacheDir, that.moduleCacheDir) + && Objects.equals(modulePath, that.modulePath) + && Objects.equals(timeout, that.timeout) + && Objects.equals(rootDir, that.rootDir); + } + + @Override + public int hashCode() { + var result = + Objects.hash( + externalProperties, env, noCache, moduleCacheDir, modulePath, timeout, rootDir); + result = 31 * result + hashPatterns(allowedModules); + result = 31 * result + hashPatterns(allowedResources); + return result; + } + + @Override + public String toString() { + return "EvaluatorSettings{" + + "externalProperties=" + + externalProperties + + ", env=" + + env + + ", allowedModules=" + + allowedModules + + ", allowedResources=" + + allowedResources + + ", noCache=" + + noCache + + ", moduleCacheDir=" + + moduleCacheDir + + ", modulePath=" + + modulePath + + ", timeout=" + + timeout + + ", rootDir=" + + rootDir + + '}'; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/project/ProjectDependenciesResolver.java b/pkl-core/src/main/java/org/pkl/core/project/ProjectDependenciesResolver.java new file mode 100644 index 00000000..55489940 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/ProjectDependenciesResolver.java @@ -0,0 +1,147 @@ +/** + * 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.project; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.file.Path; +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.EconomicSet; +import org.pkl.core.PklException; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.packages.Checksums; +import org.pkl.core.packages.Dependency; +import org.pkl.core.packages.Dependency.LocalDependency; +import org.pkl.core.packages.Dependency.RemoteDependency; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.packages.PackageResolver; +import org.pkl.core.packages.PackageUri; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.EconomicSets; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.Nullable; + +/** + * Given a project's dependencies, build the dependency list. + * + *

Resolves all dependencies using the Construct Build List algorithm. + * + *

Resolved dependencies have URI `projectpackage` to indicate that they should be project-local. + */ +public class ProjectDependenciesResolver { + private final Project project; + private final PackageResolver packageResolver; + private final Writer logWriter; + private final EconomicMap resolvedDependencies = + EconomicMaps.create(); + + private final EconomicSet alreadyHandledDependencies = EconomicSets.create(); + + public ProjectDependenciesResolver( + Project project, PackageResolver packageResolver, Writer logWriter) { + this.project = project; + this.packageResolver = packageResolver; + this.logWriter = logWriter; + } + + public ProjectDeps resolve() { + buildResolvedDependencies(project.getDependencies()); + for (var localProject : project.getDependencies().getLocalDependencies().values()) { + var packageUri = localProject.getMyPackageUri(); + assert packageUri != null; + var canonicalUri = CanonicalPackageUri.fromPackageUri(packageUri); + var resolvedDependency = resolvedDependencies.get(canonicalUri); + if (!(resolvedDependencies.get(canonicalUri) instanceof LocalDependency)) { + log( + String.format( + "WARN: local dependency `%s` was overridden to remote dependency `%s`.", + packageUri.getDisplayName(), resolvedDependency.getPackageUri().getDisplayName())); + } + } + return new ProjectDeps(resolvedDependencies); + } + + private void log(String message) { + try { + logWriter.write(message + "\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void buildResolvedDependencies(DeclaredDependencies declaredDependencies) { + for (var dependency : declaredDependencies.getRemoteDependencies().values()) { + resolveDependenciesOfPackageUri( + dependency.getPackageUri().toProjectPackageUri(), dependency.getChecksums()); + } + for (var localDeclaredDependencies : declaredDependencies.getLocalDependencies().values()) { + resolveDependencies(localDeclaredDependencies); + } + } + + private void resolveDependenciesOfPackageUri( + PackageUri packageUri, @Nullable Checksums expectedChecksums) { + try { + if (alreadyHandledDependencies.contains(packageUri)) { + return; + } + var pair = packageResolver.getDependencyMetadataAndComputeChecksum(packageUri); + var metadata = pair.first; + var computedChecksums = pair.second; + if (expectedChecksums != null) { + if (!expectedChecksums.getSha256().equals(computedChecksums.getSha256())) { + throw new PklException( + ErrorMessages.create( + "invalidDeclaredChecksum", + packageUri.getDisplayName(), + computedChecksums.getSha256(), + expectedChecksums.getSha256())); + } + } + var dependencyWithChecksum = new RemoteDependency(packageUri, computedChecksums); + updateDependency(dependencyWithChecksum); + EconomicSets.add(alreadyHandledDependencies, packageUri); + for (var transitiveDependency : metadata.getDependencies().values()) { + resolveDependenciesOfPackageUri( + transitiveDependency.getPackageUri().toProjectPackageUri(), + transitiveDependency.getChecksums()); + } + } catch (IOException | SecurityManagerException | PackageLoadError e) { + throw new PklException(e.getMessage(), e); + } + } + + private void resolveDependencies(DeclaredDependencies declaredDependencies) { + var packageUri = declaredDependencies.getMyPackageUri(); + assert packageUri != null; + var projectDir = Path.of(declaredDependencies.getProjectFileUri()).getParent(); + var relativePath = this.project.getProjectDir().relativize(projectDir); + var localDependency = new LocalDependency(packageUri.toProjectPackageUri(), relativePath); + updateDependency(localDependency); + buildResolvedDependencies(declaredDependencies); + } + + private void updateDependency(Dependency dependency) { + var canonicalPackageUri = CanonicalPackageUri.fromPackageUri(dependency.getPackageUri()); + var currentDependency = resolvedDependencies.get(canonicalPackageUri); + if (currentDependency == null + || currentDependency.getVersion().compareTo(dependency.getVersion()) < 0) { + EconomicMaps.put(resolvedDependencies, canonicalPackageUri, dependency); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/project/ProjectDeps.java b/pkl-core/src/main/java/org/pkl/core/project/ProjectDeps.java new file mode 100644 index 00000000..939f25f6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/ProjectDeps.java @@ -0,0 +1,225 @@ +/** + * 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.project; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.packages.Checksums; +import org.pkl.core.packages.Dependency; +import org.pkl.core.packages.Dependency.LocalDependency; +import org.pkl.core.packages.Dependency.RemoteDependency; +import org.pkl.core.packages.DependencyMetadata; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.packages.PackageUtils; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.json.Json; +import org.pkl.core.util.json.Json.FormatException; +import org.pkl.core.util.json.Json.JsObject; +import org.pkl.core.util.json.Json.JsonParseException; +import org.pkl.core.util.json.JsonWriter; + +/** + * The Java representation of a project's resolved dependency list. Resolved dependencies are stored + * as JSON as a sibling file to PklProject. Each key in the JSON file records an entry for each + * dependecy via its base URI, and the major version number. + * + *

A resolved dependency can either be local or remote. A remote dependency will have its + * checksums recorded, while a local dependency will point to the relative path of the project + * holding the dependency. + * + *

Sample structure: + * + *

+ * 
+ * {
+ *   "schemaVersion": 1,
+ *   "resolvedDependencies": {
+ *     "package://example.com/my/package@0": {
+ *       "type": "remote",
+ *       "uri": "projectpackage://example.com/my/package@0.5.0",
+ *       "checksums": {
+ *         "sha256": "abc123"
+ *       }
+ *     },
+ *     "package://example.com/other/package@1": {
+ *       "type": "local",
+ *       "uri": "projectpackage://example.com/other/package@1.5.0",
+ *       "path": "../sibling"
+ *     }
+ *   }
+ * }
+ * 
+ * + */ +public class ProjectDeps { + private static final Set supportedSchemaVersions = Set.of(1); + + private final EconomicMap resolvedDependencies; + + public static ProjectDeps parse(Path path) + throws IOException, URISyntaxException, JsonParseException { + var input = Files.readString(path); + return parse(input); + } + + public static ProjectDeps parse(String input) throws JsonParseException, URISyntaxException { + var parsed = Json.parseObject(input); + var schemaVersion = parsed.getInt("schemaVersion"); + if (!supportedSchemaVersions.contains(schemaVersion)) { + throw new PackageLoadError("unsupportedProjectDepsVersion", schemaVersion); + } + var resolvedDependencies = + parsed.get("resolvedDependencies", ProjectDeps::parseResolvedDependencies); + return new ProjectDeps(resolvedDependencies); + } + + private static EconomicMap parseResolvedDependencies( + Object object) throws JsonParseException, URISyntaxException { + if (!(object instanceof JsObject)) { + throw new FormatException("resolvedDendencies", "object", object.getClass()); + } + var jsObj = (JsObject) object; + var ret = EconomicMaps.create(jsObj.size()); + for (var entry : jsObj.entrySet()) { + Dependency resolvedDependency = parseResolvedDependency(entry); + var canonicalPackageUri = CanonicalPackageUri.of(entry.getKey()); + ret.put(canonicalPackageUri, resolvedDependency); + } + return ret; + } + + private static Dependency parseResolvedDependency(Entry entry) + throws JsonParseException { + var input = entry.getValue(); + if (!(input instanceof JsObject)) { + throw new VmExceptionBuilder().evalError("invalid object").build(); + } + var obj = (JsObject) input; + var type = obj.getString("type"); + var uri = obj.get("uri", PackageUtils::parsePackageUriWithoutChecksums); + if (type.equals("remote")) { + var checksums = DependencyMetadata.parseChecksums(obj.getObject("checksums")); + return new RemoteDependency(uri, checksums); + } else { + assert type.equals("local"); + var pathStr = obj.getString("path"); + return new Dependency.LocalDependency(uri, Path.of(pathStr)); + } + } + + public ProjectDeps(EconomicMap resolvedDependencies) { + this.resolvedDependencies = resolvedDependencies; + } + + /** Given a declared dependency, return the resolved dependency. */ + public @Nullable Dependency get(CanonicalPackageUri canonicalPackageUri) { + return resolvedDependencies.get(canonicalPackageUri); + } + + /** Serializes project dependencies to JSON, and writes it to the provided output stream. */ + public void writeTo(OutputStream out) throws IOException { + new ProjectDepsWriter(out, resolvedDependencies).write(); + } + + @Override + public String toString() { + return "ProjectDeps {" + resolvedDependencies + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProjectDeps that = (ProjectDeps) o; + return EconomicMaps.equals(resolvedDependencies, that.resolvedDependencies); + } + + @Override + public int hashCode() { + return Objects.hash(resolvedDependencies); + } + + private static final class ProjectDepsWriter { + private final EconomicMap projectDeps; + private final JsonWriter jsonWriter; + + private ProjectDepsWriter( + OutputStream out, EconomicMap projectDeps) { + jsonWriter = new JsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); + jsonWriter.setIndent(" "); + this.projectDeps = projectDeps; + } + + private void writeChecksums(Checksums checksums) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("sha256").value(checksums.getSha256()); + jsonWriter.endObject(); + } + + private void writeRemoteDependency(RemoteDependency remoteDependency) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("type").value("remote"); + jsonWriter.name("uri").value(remoteDependency.getPackageUri().toString()); + jsonWriter.name("checksums"); + assert remoteDependency.getChecksums() != null; + writeChecksums(remoteDependency.getChecksums()); + jsonWriter.endObject(); + } + + private void writeLocalDependency(LocalDependency localDependency) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("type").value("local"); + jsonWriter.name("uri").value(localDependency.getPackageUri().toString()); + jsonWriter.name("path").value(localDependency.getPath().toString()); + jsonWriter.endObject(); + } + + private void write() throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("schemaVersion").value(1); + jsonWriter.name("resolvedDependencies"); + jsonWriter.beginObject(); + var cursor = projectDeps.getEntries(); + while (cursor.advance()) { + jsonWriter.name(cursor.getKey().toString()); + var dependency = cursor.getValue(); + if (dependency instanceof LocalDependency) { + writeLocalDependency((LocalDependency) dependency); + } else { + writeRemoteDependency((RemoteDependency) dependency); + } + } + jsonWriter.endObject(); + jsonWriter.endObject(); + jsonWriter.close(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java new file mode 100644 index 00000000..72dbe1a6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java @@ -0,0 +1,469 @@ +/** + * 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.project; + +import com.oracle.truffle.api.source.SourceSection; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.PklBugException; +import org.pkl.core.PklException; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.StackFrameTransformer; +import org.pkl.core.ast.builder.ImportsAndReadsParser; +import org.pkl.core.module.ModuleKeys; +import org.pkl.core.module.ProjectDependenciesManager; +import org.pkl.core.module.ResolvedModuleKeys; +import org.pkl.core.packages.Checksums; +import org.pkl.core.packages.Dependency.LocalDependency; +import org.pkl.core.packages.Dependency.RemoteDependency; +import org.pkl.core.packages.DependencyMetadata; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.packages.PackageResolver; +import org.pkl.core.packages.PackageUri; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.util.ByteArrayUtils; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.GlobResolver; +import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; + +/** + * Given a list of project directories, prepares artifacts to be published as a package. + * + *

Validates that relative imports of all included Pkl modules resolve to locations within the + * package. + * + *

Given a package URI {@code package://example.com/thepackage@1.0.0}, the following files get + * created: + * + *

    + *
  • thepackage@1.0.0 - the metadata JSON file + *
  • thepackage@1.0.0.sha256 - the SHA-256 checksum of the metadata file + *
  • thepackage@1.0.0.zip - the zip archive containing the contents of the package + *
  • thepackage@1.0.0.zip.sha256 - the SHA-256 checksum of the zip archive + *
+ */ +public class ProjectPackager { + /** + * Modification time value for all zip entries in a package, to ensure that archives are + * reproducible. + * + *

Date is 1980 February 1st CET (value taken from {@code + * org.gradle.api.internal.file.archive.ZipCopyAction}). + */ + private static final long ZIP_ENTRY_MTIME = + new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0).getTimeInMillis(); + + private final EconomicMap packageResults = EconomicMap.create(); + + private final List projects; + private final Path workingDir; + private final String outputPathPattern; + private final StackFrameTransformer stackFrameTransformer; + private final PackageResolver packageResolver; + private final boolean skipPublishCheck; + private final Writer outputWriter; + + public ProjectPackager( + List projects, + Path workingDir, + String outputPathPattern, + StackFrameTransformer stackFrameTransformer, + SecurityManager securityManager, + boolean skipPublishCheck, + Writer outputWriter) { + this.projects = projects; + this.workingDir = workingDir; + this.outputPathPattern = outputPathPattern; + this.stackFrameTransformer = stackFrameTransformer; + // intentionally use InMemoryPackageResolver + this.packageResolver = PackageResolver.getInstance(securityManager, null); + this.skipPublishCheck = skipPublishCheck; + this.outputWriter = outputWriter; + } + + public void createPackages() throws IOException { + for (var project : projects) { + var packageResult = doPackage(project); + outputWriter.write(workingDir.relativize(packageResult.getMetadataFile()) + "\n"); + outputWriter.write(workingDir.relativize(packageResult.getMetadataChecksumFile()) + "\n"); + outputWriter.write(workingDir.relativize(packageResult.getZipFile()) + "\n"); + outputWriter.write(workingDir.relativize(packageResult.getZipChecksumFile()) + "\n"); + outputWriter.flush(); + } + } + + private Path resolveOutputDirectory(Package pkg) { + var substituted = + outputPathPattern + .replace("%{name}", pkg.getName()) + .replace("%{version}", pkg.getVersion().toString()); + return workingDir.resolve(substituted); + } + + public PackageResult doPackage(Project project) throws IOException { + var pkg = project.getPackage(); + if (pkg == null) { + throw new PklException( + ErrorMessages.create("noPackageDefinedByProject", project.getProjectFileUri())); + } + if (packageResults.containsKey(pkg.getUri())) { + return packageResults.get(pkg.getUri()); + } + var files = collectPackageElements(project, pkg); + validatePklImportsAndReads(project, files); + var outputDir = resolveOutputDirectory(pkg); + var metadataFileName = IoUtils.takeLastSegment(pkg.getUri().getUri().getPath(), '/'); + var metadataFile = outputDir.resolve(metadataFileName); + var metadataChecksumFile = outputDir.resolve(metadataFileName + ".sha256"); + var zipFile = outputDir.resolve(metadataFileName + ".zip"); + var zipChecksumFile = outputDir.resolve(metadataFileName + ".zip.sha256"); + var zipFileChecksum = createPackageZipAndComputeChecksum(project, files, zipFile); + var metadataFileChecksum = + createDependencyMetadataAndComputeChecksum(project, pkg, metadataFile, zipFileChecksum); + Files.writeString(zipChecksumFile, zipFileChecksum); + Files.writeString(metadataChecksumFile, metadataFileChecksum); + if (!skipPublishCheck) { + checkAlreadyPublishedPackage(pkg, metadataFileChecksum); + } + var result = + new PackageResult( + metadataFile, metadataChecksumFile, zipFile, zipChecksumFile, metadataFileChecksum); + packageResults.put(pkg.getUri(), result); + return result; + } + + private void checkAlreadyPublishedPackage(Package pkg, String computedChecksum) + throws IOException { + try { + var metadataAndChecksum = + packageResolver.getDependencyMetadataAndComputeChecksum(pkg.getUri()); + var receivedChecksum = metadataAndChecksum.second.getSha256(); + if (!receivedChecksum.equals(computedChecksum)) { + throw new PklException( + ErrorMessages.create( + "packageAlreadyPublishedWithDifferentContents", + pkg.getUri(), + computedChecksum, + receivedChecksum)); + } + } catch (PackageLoadError e) { + if (e.getMessageName().equals("badHttpStatusCode") && (int) e.getArguments()[0] == 404) { + return; + } + throw e; + } catch (SecurityManagerException e) { + throw new PklException(e.getMessage()); + } + } + + private String createDependencyMetadataAndComputeChecksum( + Project project, Package pkg, Path metadataFile, String zipFileChecksum) throws IOException { + var dependencyMetadata = createDependencyMetadata(project, pkg, zipFileChecksum); + try (var fos = newDigestOutputStream(new FileOutputStream(metadataFile.toFile()))) { + dependencyMetadata.writeTo(fos); + return ByteArrayUtils.toHex(fos.getMessageDigest().digest()); + } + } + + /** If the project has a local dependency, package it as well, so we can record its checksum. */ + private Map buildDependencies(Project project) throws IOException { + try { + var ret = + new HashMap( + project.getDependencies().getLocalDependencies().size() + + project.getDependencies().getRemoteDependencies().size()); + var projectDependenciesManager = new ProjectDependenciesManager(project.getDependencies()); + for (var entry : project.getDependencies().getRemoteDependencies().entrySet()) { + var resolved = + (RemoteDependency) + projectDependenciesManager.getResolvedDependency(entry.getValue().getPackageUri()); + ret.put( + entry.getKey(), + new RemoteDependency( + resolved.getPackageUri().toExternalPackageUri(), resolved.getChecksums())); + } + for (var entry : project.getLocalProjectDependencies().entrySet()) { + var localProject = entry.getValue(); + assert localProject.getPackage() != null; + var packageUri = localProject.getPackage().getUri(); + var resolved = projectDependenciesManager.getResolvedDependency(packageUri); + if (resolved instanceof LocalDependency) { + var packageResult = doPackage(localProject); + ret.put( + entry.getKey(), + new RemoteDependency( + packageUri.toExternalPackageUri(), + new Checksums(packageResult.getMetadataChecksum()))); + } else { + var remoteDep = (RemoteDependency) resolved; + ret.put( + entry.getKey(), + new RemoteDependency( + remoteDep.getPackageUri().toExternalPackageUri(), remoteDep.getChecksums())); + } + } + return ret; + } catch (PackageLoadError e) { + throw new PklException( + ErrorMessages.create( + "unexpectedPackageLoadError", project.getProjectFileUri(), e.getMessage()), + e); + } + } + + private DependencyMetadata createDependencyMetadata( + Project project, Package pkg, String packageZipChecksum) throws IOException { + return new DependencyMetadata( + pkg.getName(), + pkg.getUri(), + pkg.getVersion(), + pkg.getPackageZipUrl(), + new Checksums(packageZipChecksum), + buildDependencies(project), + pkg.getSourceCodeUrlScheme(), + pkg.getSourceCode(), + pkg.getDocumentation(), + pkg.getLicense(), + pkg.getLicenseText(), + pkg.getAuthors(), + pkg.getIssueTracker(), + pkg.getDescription()); + } + + private DigestOutputStream newDigestOutputStream(OutputStream outputStream) { + try { + var md = MessageDigest.getInstance("SHA-256"); + return new DigestOutputStream(outputStream, md); + } catch (NoSuchAlgorithmException e) { + // SHA-256 is available in all JVM implementations + throw PklBugException.unreachableCode(); + } + } + + private String ensureEndsWithSlash(Path file) { + if (file.endsWith("/")) { + return file.toString(); + } + return file + "/"; + } + + /** + * Sets mtime to 0 so package creation is idempotent. Running the packager multiple times produces + * the same output. + */ + private String createPackageZipAndComputeChecksum( + Project project, List files, Path outputZipFile) { + DigestOutputStream digestOutputStream; + try { + var outputFile = outputZipFile.toFile(); + Files.createDirectories(outputZipFile.getParent()); + digestOutputStream = newDigestOutputStream(new FileOutputStream(outputFile)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + try (var zos = new ZipOutputStream(digestOutputStream)) { + for (var file : files) { + var relativePath = project.getProjectDir().relativize(file); + if (Files.isDirectory(file)) { + var zipEntry = new ZipEntry(ensureEndsWithSlash(relativePath)); + zipEntry.setTime(ZIP_ENTRY_MTIME); + zos.putNextEntry(zipEntry); + } else { + var zipEntry = new ZipEntry(relativePath.toString()); + zipEntry.setTime(ZIP_ENTRY_MTIME); + zos.putNextEntry(zipEntry); + Files.copy(file, zos); + } + zos.closeEntry(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return ByteArrayUtils.toHex(digestOutputStream.getMessageDigest().digest()); + } + + private void validatePklImportsAndReads(Project project, List files) { + for (var file : files) { + if (file.toString().endsWith(".pkl")) { + validateImportsAndReads(project, file); + } + } + } + + private List getExcludePatterns(Package pkg) { + var excludePatterns = new ArrayList(); + for (String s : pkg.getExclude()) { + try { + excludePatterns.add(GlobResolver.toRegexPattern(s)); + } catch (InvalidGlobPatternException e) { + throw new PklException(e.getMessage(), e); + } + } + return excludePatterns; + } + + private List collectPackageElements(Project project, Package pkg) { + var excludePatterns = getExcludePatterns(pkg); + try (var stream = Files.walk(project.getProjectDir())) { + return stream + .filter( + (it) -> { + var fileNameRelativeToProjectRoot = + project.getProjectDir().relativize(it).toString(); + for (var pattern : excludePatterns) { + if (pattern.matcher(it.getFileName().toString()).matches()) { + return false; + } + if (pattern.matcher(fileNameRelativeToProjectRoot).matches()) { + return false; + } + } + return true; + }) + // Have consistent sort order independent of different file systems + .sorted() + .collect(Collectors.toList()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private boolean isAbsoluteImport(String importStr) { + return importStr.matches("\\w:.*") || importStr.startsWith("@"); + } + + /** + * Parse a Pkl module, and verify that its {@code import}s and {@code read}s resolve to locations + * within the package directory. + * + *

Note that these might be glob expressions, so these paths might not actually exist. For + * example, an import might look like {@code "foo/*.pkl"}, which would resolve as path {@code + * `/some/dir/foo/*.pkl`}, which is not a real file. This is just a sanity check to ensure that + * the paths can reasonably resolve to a location within the package directory. + */ + public void validateImportsAndReads(Project project, Path pklModulePath) { + var imports = getImportsAndReads(pklModulePath); + if (imports == null) { + return; + } + for (var importContext : imports) { + var importStr = importContext.first; + var sourceSection = importContext.second; + if (isAbsoluteImport(importStr)) { + continue; + } + var importPath = Path.of(importStr); + if (importPath.isAbsolute() && !project.getProjectDir().toString().equals("/")) { + throw new VmExceptionBuilder() + .evalError("invalidRelativeProjectImport", importStr) + .withSourceSection(sourceSection) + .build() + .toPklException(stackFrameTransformer); + } + var currentPath = pklModulePath.getParent(); + // It's not good enough to just check the normalized path to see whether it exists within the + // root dir. + // It's possible that the import path resolves to a path outside the project dir, + // and then back inside the project dir. + for (var i = 0; i < importPath.getNameCount(); i++) { + var segment = importPath.getName(i); + currentPath = currentPath.resolve(segment); + var normalized = currentPath.normalize(); + if (!normalized.startsWith(project.getProjectDir())) { + throw new VmExceptionBuilder() + .evalError("invalidRelativeProjectImport", importStr) + .withSourceSection(sourceSection) + .build() + .toPklException(stackFrameTransformer); + } + } + } + } + + private @Nullable List> getImportsAndReads(Path pklModulePath) { + try { + var moduleKey = ModuleKeys.file(pklModulePath.toUri(), pklModulePath); + var resolvedModuleKey = ResolvedModuleKeys.file(moduleKey, moduleKey.getUri(), pklModulePath); + return ImportsAndReadsParser.parse(moduleKey, resolvedModuleKey); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static class PackageResult { + private final Path zipFile; + private final Path zipChecksumFile; + private final Path metadataFile; + private final Path metadataChecksumFile; + private final String metadataChecksum; + + public PackageResult( + Path zipFile, + Path zipChecksumFile, + Path metadataFile, + Path metadataChecksumFile, + String metadataChecksum) { + this.zipFile = zipFile; + this.zipChecksumFile = zipChecksumFile; + this.metadataFile = metadataFile; + this.metadataChecksumFile = metadataChecksumFile; + this.metadataChecksum = metadataChecksum; + } + + public Path getZipFile() { + return zipFile; + } + + public Path getZipChecksumFile() { + return zipChecksumFile; + } + + public Path getMetadataFile() { + return metadataFile; + } + + public Path getMetadataChecksumFile() { + return metadataChecksumFile; + } + + public String getMetadataChecksum() { + return metadataChecksum; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/project/package-info.java b/pkl-core/src/main/java/org/pkl/core/project/package-info.java new file mode 100644 index 00000000..d5f3ee0b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/project/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.project; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/repl/ReplRequest.java b/pkl-core/src/main/java/org/pkl/core/repl/ReplRequest.java new file mode 100644 index 00000000..a38ff141 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/repl/ReplRequest.java @@ -0,0 +1,86 @@ +/** + * 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.repl; + +import java.net.URI; + +public abstract class ReplRequest { + public final String id; + + private ReplRequest(String id) { + if (id.isEmpty()) { + throw new IllegalArgumentException("Request ID must be empty."); + } + this.id = id; + } + + /** Requests evaluation of REPL input. */ + public static final class Eval extends ReplRequest { + public final String text; + public final boolean evalDefinitions; + public final boolean forceResults; + + public Eval(String id, String text, boolean evalDefinitions, boolean forceResults) { + super(id); + this.text = text; + this.evalDefinitions = evalDefinitions; + this.forceResults = forceResults; + } + + public String toString() { + return String.format( + "%s(text=%s,evalDefinitions=%s,forceResults=%s)", + getClass().getSimpleName(), text, evalDefinitions, forceResults); + } + } + + /** Requests loading of a module. */ + public static final class Load extends ReplRequest { + public final URI uri; + + public Load(String id, URI uri) { + super(id); + this.uri = uri; + } + + public String toString() { + return String.format("%s(url=%s)", getClass().getSimpleName(), uri); + } + } + + public static final class Completion extends ReplRequest { + public final String text; + + public Completion(String id, String text) { + super(id); + this.text = text; + } + + public String toString() { + return String.format("%s(text=%s)", getClass().getSimpleName(), text); + } + } + + public static final class Reset extends ReplRequest { + public Reset(String id) { + super(id); + } + + public String toString() { + return String.format("%s()", getClass().getSimpleName()); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/repl/ReplResponse.java b/pkl-core/src/main/java/org/pkl/core/repl/ReplResponse.java new file mode 100644 index 00000000..4d7342d6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/repl/ReplResponse.java @@ -0,0 +1,106 @@ +/** + * 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.repl; + +import java.util.*; + +public abstract class ReplResponse { + private final String message; + + private ReplResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public static final class Completion extends ReplResponse { + public static final Completion EMPTY = new Completion(List.of()); + + public final Collection members; + + public Completion(Collection members) { + super(""); + this.members = members; + } + + public String toString() { + return String.format("%s(members=%s)", getClass().getSimpleName(), members); + } + } + + public static final class EvalSuccess extends ReplResponse { + public EvalSuccess(String message) { + super(message); + } + + public String getResult() { + return getMessage(); + } + + public String toString() { + return String.format("%s(result=%s)", getClass().getSimpleName(), getResult()); + } + } + + public static final class EvalError extends ReplResponse { + public EvalError(String message) { + super(message); + } + + public String toString() { + return String.format("%s(message=%s)", getClass().getSimpleName(), getMessage()); + } + } + + public static final class IncompleteInput extends ReplResponse { + public IncompleteInput(String message) { + super(message); + } + + public String toString() { + return String.format("%s(message=%s)", getClass().getSimpleName(), getMessage()); + } + } + + public static final class InvalidRequest extends ReplResponse { + public InvalidRequest(String message) { + super(message); + } + + public String toString() { + return String.format("%s(message=%s)", getClass().getSimpleName(), getMessage()); + } + } + + public static final class InternalError extends ReplResponse { + private final Throwable cause; + + public InternalError(Throwable cause) { + super(cause.toString()); + this.cause = cause; + } + + public Throwable getCause() { + return cause; + } + + public String toString() { + return String.format("%s(cause=%s)", getClass().getSimpleName(), cause); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java new file mode 100644 index 00000000..c7069921 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java @@ -0,0 +1,455 @@ +/** + * 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.repl; + +import com.oracle.truffle.api.Truffle; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.nodes.IndirectCallNode; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.graalvm.polyglot.Context; +import org.pkl.core.*; +import org.pkl.core.SecurityManager; +import org.pkl.core.ast.*; +import org.pkl.core.ast.builder.AstBuilder; +import org.pkl.core.ast.member.*; +import org.pkl.core.ast.repl.ResolveClassMemberNode; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.module.*; +import org.pkl.core.packages.PackageResolver; +import org.pkl.core.parser.LexParseException; +import org.pkl.core.parser.Parser; +import org.pkl.core.parser.antlr.PklParser; +import org.pkl.core.parser.antlr.PklParser.*; +import org.pkl.core.project.DeclaredDependencies; +import org.pkl.core.repl.ReplRequest.Eval; +import org.pkl.core.repl.ReplRequest.Load; +import org.pkl.core.repl.ReplRequest.Reset; +import org.pkl.core.repl.ReplResponse.EvalError; +import org.pkl.core.repl.ReplResponse.EvalSuccess; +import org.pkl.core.repl.ReplResponse.InvalidRequest; +import org.pkl.core.resource.ResourceReader; +import org.pkl.core.runtime.*; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.MutableReference; +import org.pkl.core.util.Nullable; + +public class ReplServer implements AutoCloseable { + private final IndirectCallNode callNode = Truffle.getRuntime().createIndirectCallNode(); + private final Context polyglotContext; + private final VmLanguage language; + private final ReplState replState; + private final Path workingDir; + private final SecurityManager securityManager; + private final ModuleResolver moduleResolver; + private final VmExceptionRenderer errorRenderer; + private final PackageResolver packageResolver; + private final @Nullable ProjectDependenciesManager projectDependenciesManager; + + public ReplServer( + SecurityManager securityManager, + Logger logger, + Collection moduleKeyFactories, + Collection resourceReaders, + Map environmentVariables, + Map externalProperties, + @Nullable Path moduleCacheDir, + @Nullable DeclaredDependencies projectDependencies, + @Nullable String outputFormat, + Path workingDir, + StackFrameTransformer frameTransformer) { + + this.workingDir = workingDir; + this.securityManager = securityManager; + this.moduleResolver = new ModuleResolver(moduleKeyFactories); + this.errorRenderer = new VmExceptionRenderer(new StackTraceRenderer(frameTransformer)); + replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype())); + + var languageRef = new MutableReference(null); + packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir); + projectDependenciesManager = + projectDependencies == null ? null : new ProjectDependenciesManager(projectDependencies); + polyglotContext = + VmUtils.createContext( + () -> { + languageRef.set(VmLanguage.get(null)); + var vmContext = VmContext.get(null); + vmContext.initialize( + new VmContext.Holder( + frameTransformer, + securityManager, + moduleResolver, + new ResourceManager(securityManager, resourceReaders), + logger, + environmentVariables, + externalProperties, + moduleCacheDir, + outputFormat, + packageResolver, + projectDependenciesManager)); + }); + language = languageRef.get(); + } + + public List handleRequest(ReplRequest request) { + polyglotContext.enter(); + try { + if (request instanceof Eval) { + return handleEval((Eval) request); + } + + if (request instanceof Load) { + return handleLoad((Load) request); + } + + if (request instanceof ReplRequest.Completion) { + return handleCompletion((ReplRequest.Completion) request); + } + + if (request instanceof Reset) { + return handleReset(); + } + + return List.of( + new InvalidRequest("Unsupported request type: " + request.getClass().getSimpleName())); + } catch (Exception e) { + return List.of(new ReplResponse.InternalError(e)); + } finally { + polyglotContext.leave(); + } + } + + @Override + public void close() { + polyglotContext.close(true); + try { + packageResolver.close(); + } catch (IOException ignored) { + } + } + + private List handleEval(Eval request) { + var results = + evaluate( + replState, request.id, request.text, request.evalDefinitions, request.forceResults); + return results.stream() + .map( + result -> + result instanceof ReplResponse + ? (ReplResponse) result + : new EvalSuccess(render(result))) + .collect(Collectors.toList()); + } + + @SuppressWarnings("StatementWithEmptyBody") + private List evaluate( + ReplState replState, + String requestId, + String text, + boolean evalDefinitions, + boolean forceResults) { + var parser = new Parser(); + PklParser.ReplInputContext replInputContext; + var uri = URI.create("repl:" + requestId); + + try { + replInputContext = parser.parseReplInput(text); + } catch (LexParseException.IncompleteInput e) { + return List.of(new ReplResponse.IncompleteInput(e.getMessage())); + } catch (LexParseException e) { + var exception = VmUtils.toVmException(e, text, uri, uri.toString()); + var errorMessage = errorRenderer.render(exception); + return List.of(new EvalError(errorMessage)); + } + + var results = new ArrayList<>(); + var module = ModuleKeys.synthetic(uri, workingDir.toUri(), uri, text, false); + ResolvedModuleKey resolved; + try { + resolved = module.resolve(securityManager); + } catch (SecurityManagerException e) { + throw new VmExceptionBuilder().withCause(e).build(); + } catch (IOException e) { + // resolving a synthetic module should never cause IOException + throw new AssertionError(e); + } + + var builder = + new AstBuilder( + VmUtils.loadSource(resolved), + language, + replState.module.getModuleInfo(), + moduleResolver); + var childrenExceptEof = + replInputContext.children.subList(0, replInputContext.children.size() - 1); + + for (var tree : childrenExceptEof) { + try { + if (tree instanceof ExprContext) { + var exprNode = (ExpressionNode) tree.accept(builder); + evaluateExpr(replState, exprNode, forceResults, results); + } else if (tree instanceof ImportClauseContext) { + addStaticModuleProperty(builder.visitImportClause((ImportClauseContext) tree)); + } else if (tree instanceof ClassPropertyContext) { + var propertyNode = builder.visitClassProperty((ClassPropertyContext) tree); + var property = addModuleProperty(propertyNode); + if (evalDefinitions) { + evaluateMemberDef(replState, property, forceResults, results); + } + } else if (tree instanceof ClazzContext) { + addStaticModuleProperty(builder.visitClazz((ClazzContext) tree)); + } else if (tree instanceof TypeAliasContext) { + addStaticModuleProperty(builder.visitTypeAlias((TypeAliasContext) tree)); + } else if (tree instanceof ClassMethodContext) { + addModuleMethodDef(builder.visitClassMethod((ClassMethodContext) tree)); + } else if (tree instanceof ModuleDeclContext) { + // do nothing for now + } else if (tree instanceof TerminalNode && tree.toString().equals(",")) { + // do nothing + } else { + results.add( + new ReplResponse.InternalError(new IllegalStateException("Unexpected parse result"))); + } + } catch (VmException e) { + // TODO: patch stack trace for constants + results.add(new EvalError(errorRenderer.render(e))); + } + } + + return results; + } + + private void addStaticModuleProperty(ObjectMember property) { + replState.module.getPrototype().addProperty(property); + } + + private ObjectMember addModuleProperty(UnresolvedPropertyNode propertyNode) { + var needToCreateNewModuleToEnforceLateBinding = + !propertyNode.isLocal() && replState.module.hasMember(propertyNode.getName()); + + if (needToCreateNewModuleToEnforceLateBinding) { + replState.module = createEmptyReplModule(replState.module); + } + + var resolveNode = + new ResolveClassMemberNode( + language, new FrameDescriptor(), propertyNode, replState.module.getVmClass()); + + var property = + (ClassProperty) + callNode.call(resolveNode.getCallTarget(), replState.module, replState.module); + + replState.module.getVmClass().addProperty(property); + return property.getInitializer(); + } + + private void addModuleMethodDef(UnresolvedMethodNode methodNode) { + var needToCreateNewModuleToEnforceLateBinding = + !methodNode.isLocal() + && replState.module.getVmClass().hasDeclaredMethod(methodNode.getName()); + + if (needToCreateNewModuleToEnforceLateBinding) { + replState.module = createEmptyReplModule(replState.module); + } + + var resolveNode = + new ResolveClassMemberNode( + language, new FrameDescriptor(), methodNode, replState.module.getVmClass()); + + var method = + (ClassMethod) + callNode.call(resolveNode.getCallTarget(), replState.module, replState.module); + + replState.module.getVmClass().addMethod(method); + } + + private void evaluateExpr( + ReplState replState, ExpressionNode exprNode, boolean forceResults, List results) { + + var rootNode = + new SimpleRootNode( + language, new FrameDescriptor(), exprNode.getSourceSection(), "", exprNode); + + var result = callNode.call(rootNode.getCallTarget(), replState.module, replState.module); + + if (forceResults) VmValue.force(result, false); + results.add(result); + } + + private void evaluateMemberDef( + ReplState replState, ObjectMember memberDef, boolean forceResults, List results) { + + var result = + memberDef.getConstantValue() != null + ? memberDef.getConstantValue() + : callNode.call(memberDef.getCallTarget(), replState.module, replState.module); + + if (forceResults) VmValue.force(result, false); + results.add(result); + } + + private List handleLoad(Load request) { + try { + var uri = IoUtils.resolve(workingDir.toUri(), request.uri); + var moduleToLoad = moduleResolver.resolve(uri); + var loadedModule = language.loadModule(moduleToLoad); + replState.module = + createReplModule( + loadedModule.getVmClass().getDeclaredProperties(), + loadedModule.getVmClass().getDeclaredMethods(), + loadedModule.getMembers(), + loadedModule.getParent()); + return List.of(); + } catch (VmException e) { + return List.of(new EvalError(errorRenderer.render(e))); + } + } + + private List handleCompletion(ReplRequest.Completion request) { + var members = new HashSet(); + + if (IoUtils.isWhitespace(request.text)) { + collectMembers(members, BaseModule.getModule()); + collectMembers(members, replState.module); + return List.of(new ReplResponse.Completion(members)); + } + + List results; + + // make sure completion request never affects repl state + var tempModule = + new ReplState( + createReplModule( + replState.module.getVmClass().getDeclaredProperties(), + replState.module.getVmClass().getDeclaredMethods(), + replState.module.getMembers(), + replState.module.getParent())); + results = evaluate(tempModule, request.id, request.text, false, false); + + if (results.isEmpty()) { + return List.of(ReplResponse.Completion.EMPTY); + } + + var lastResult = results.get(results.size() - 1); + if (lastResult instanceof EvalError || lastResult instanceof ReplResponse.IncompleteInput) { + return List.of(ReplResponse.Completion.EMPTY); + } + + assert !(lastResult instanceof ReplResponse); + + VmObjectLike composite; + if (lastResult instanceof VmObjectLike) { + composite = (VmObjectLike) lastResult; + } else { + composite = VmUtils.getClass(lastResult).getPrototype(); + } + + collectMembers(members, composite); + return List.of(new ReplResponse.Completion(members)); + } + + private void collectMembers(Set members, VmObjectLike composite) { + composite.iterateMembers( + (key, prop) -> { + if (key instanceof Identifier) { + members.add(key.toString()); + } + return true; + }); + composite + .getVmClass() + .visitMethodDefsTopDown( + fun -> members.add(fun.getName() + (fun.getParameterCount() == 0 ? "()" : "("))); + } + + private List handleReset() { + replState.module = createEmptyReplModule(BaseModule.getModuleClass().getPrototype()); + return List.of(); + } + + private VmTyped createEmptyReplModule(@Nullable VmTyped parent) { + return createReplModule(List.of(), List.of(), EconomicMaps.create(), parent); + } + + private VmTyped createReplModule( + Iterable propertyDefs, + Iterable methodDefs, + UnmodifiableEconomicMap moduleMembers, + @Nullable VmTyped parent) { + var uri = URI.create("repl:repl"); + var classInfo = PClassInfo.get("repl", "module", uri); + var moduleKey = ModuleKeys.synthetic(uri, workingDir.toUri(), uri, "", false); + ResolvedModuleKey resolvedModuleKey; + try { + resolvedModuleKey = moduleKey.resolve(securityManager); + } catch (IOException | SecurityManagerException e) { + // should never happen + throw new RuntimeException(e); + } + var moduleInfo = + new ModuleInfo( + VmUtils.unavailableSourceSection(), + VmUtils.unavailableSourceSection(), + null, + "repl", + moduleKey, + resolvedModuleKey, + false); + var module = + new VmTyped( + VmUtils.createEmptyMaterializedFrame(), + null, // set by initSuperclass() + null, + moduleMembers); + module.setExtraStorage(moduleInfo); + var clazz = + new VmClass( + VmUtils.unavailableSourceSection(), + VmUtils.unavailableSourceSection(), + null, + List.of(), + VmModifier.NONE, + classInfo, + List.of(), + module); + if (parent != null) { + var superclass = parent.getVmClass(); + var supertypeNode = TypeNode.forClass(VmUtils.unavailableSourceSection(), superclass); + clazz.initSupertype(supertypeNode, superclass); + } + clazz.addProperties(propertyDefs); + clazz.addMethods(methodDefs); + return module; + } + + private String render(Object value) { + return VmValueRenderer.multiLine(Integer.MAX_VALUE).render(value); + } + + private static class ReplState { + VmTyped module; + + public ReplState(VmTyped module) { + this.module = module; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/repl/package-info.java b/pkl-core/src/main/java/org/pkl/core/repl/package-info.java new file mode 100644 index 00000000..6cb3081e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/repl/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.repl; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/resource/Resource.java b/pkl-core/src/main/java/org/pkl/core/resource/Resource.java new file mode 100644 index 00000000..76feb31f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/resource/Resource.java @@ -0,0 +1,52 @@ +/** + * 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.resource; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** An external (file, HTTP, etc.) resource. */ +public final class Resource { + private final URI uri; + + private final byte[] bytes; + + /** Constructs a resource. */ + public Resource(URI uri, byte[] bytes) { + this.uri = uri; + this.bytes = bytes; + } + + /** Returns the URI of this resource. */ + public URI getUri() { + return uri; + } + + public byte[] getBytes() { + return bytes; + } + + /** Returns the text content of this resource. */ + public String getText() { + return new String(bytes, StandardCharsets.UTF_8); + } + + /** Returns the content of this resource in Base64. */ + public String getBase64() { + return Base64.getEncoder().encodeToString(bytes); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/resource/ResourceReader.java b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReader.java new file mode 100644 index 00000000..cb2024f3 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReader.java @@ -0,0 +1,58 @@ +/** + * 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.resource; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.runtime.ReaderBase; + +/** + * SPI for reading external resources from Pkl. Once a resource reader has been registered, Pkl code + * can read its resources with {@code read("")}, provided that resource URIs match an entry in + * the resource allowlist ({@code --allowed-resources}). + * + *

See {@link ResourceReaders} for predefined resource readers. + */ +public interface ResourceReader extends ReaderBase { + /** The URI scheme associated with resources read by this resource reader. */ + String getUriScheme(); + + /** + * Reads the resource with the given URI. Returns {@code Optional.empty()} if a resource with the + * given URI cannot be found. + * + *

Supported resource types are: + * + *

    + *
  • {@link String} + *
  • {@link Resource} + *
+ * + * Throws: + * + *
    + *
  • {@link IOException} — if an error occurred while reading the resource, outside of the + * resource not being found. + *
  • {@link URISyntaxException} — if the URI format is invalid for the resource. + *
  • {@link SecurityManagerException} — If the resource read is invalid per the security + * manager. + *
+ */ + Optional read(URI uri) throws IOException, URISyntaxException, SecurityManagerException; +} diff --git a/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java new file mode 100644 index 00000000..bc3e61a2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java @@ -0,0 +1,577 @@ +/** + * 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.resource; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.module.FileResolver; +import org.pkl.core.module.ModulePathResolver; +import org.pkl.core.module.PathElement; +import org.pkl.core.module.ProjectDependenciesManager; +import org.pkl.core.packages.Dependency; +import org.pkl.core.packages.Dependency.LocalDependency; +import org.pkl.core.packages.PackageAssetUri; +import org.pkl.core.packages.PackageResolver; +import org.pkl.core.runtime.VmContext; +import org.pkl.core.util.ErrorMessages; +import org.pkl.core.util.IoUtils; +import org.pkl.core.util.Nullable; + +/** Predefined resource readers for OS environment variables and external properties. */ +public final class ResourceReaders { + private ResourceReaders() {} + + /** + * A resource reader for OS environment variables. If this resource reader is present, Pkl code + * can read environment variable {@code FOO_BAR} with {@code read("env:FOO_BAR")}, provided that + * resource URI {@code env:FOO_BAR} matches an entry in the resource allowlist ({@code + * --allowed-resources}). + */ + public static ResourceReader environmentVariable() { + return EnvironmentVariable.INSTANCE; + } + + /** + * A resource reader for external properties. If this resource reader is present, Pkl code can + * read external property {@code foo.bar} with {@code read("prop:foo.bar")}, provided that + * resource URI {@code prop:foo.bar} matches an entry in the resource allowlist ({@code + * --allowed-resources}). + */ + public static ResourceReader externalProperty() { + return ExternalProperty.INSTANCE; + } + + public static ResourceReader file() { + return FileResource.INSTANCE; + } + + /** + * A resource reader for HTTP resources. If this resource reader is present, Pkl code can read + * HTTP resource {@code http://apple.com/foo/bar.txt} with {@code + * read("http://apple.com/foo/bar.txt")}, provided that resource URI {@code + * "http://apple.com/foo/bar.txt"} matches an entry in the resource allowlist ({@code + * --allowed-resources}). + */ + public static ResourceReader http() { + return HttpResource.INSTANCE; + } + + /** + * A resource reader for HTTPS resources. If this resource reader is present, Pkl code can read + * HTTPS resource {@code https://apple.com/foo/bar.txt} with {@code + * read("https://apple.com/foo/bar.txt")}, provided that resource URI {@code + * "https://apple.com/foo/bar.txt"} matches an entry in the resource allowlist ({@code + * --allowed-resources}). + */ + public static ResourceReader https() { + return HttpsResource.INSTANCE; + } + + /** + * A resource reader for JVM class path resources. If this resource reader is present, Pkl code + * can read class path resource {@code /foo/bar.txt} with {@code read("modulepath:foo/bar.txt")}, + * provided that resource URI {@code "modulepath:foo/bar.txt"} matches an entry in the resource + * allowlist ({@code --allowed-resources}). + */ + public static ResourceReader classPath(ClassLoader classLoader) { + return new ClassPathResource(classLoader); + } + + /** + * A resource reader for Pkl module path ({@code --module-path}) resources. If this resource + * reader is present, Pkl code can read module path resource {@code /foo/bar.txt} with {@code + * read("modulepath:foo/bar.txt")}, provided that resource URI {@code "modulepath:foo/bar.txt"} + * matches an entry in the resource allowlist ({@code --allowed-resources}). + */ + public static ResourceReader modulePath(ModulePathResolver resolver) { + return new ModulePathResource(resolver); + } + + /** + * A resource reader for {@code package:} resources. If this resource reader is present, Pkl code + * can read resources from within packages with {@code + * read("package://example.com/foo@1.0.0#/foo.txt")}, or using dependency notation with {@code + * read("@foo/foo.txt")} assuming that the Pkl module is within a project that declares a + * dependency named {@code foo}. + */ + public static ResourceReader pkg() { + return PackageResource.INSTANCE; + } + + public static ResourceReader projectpackage() { + return ProjectPackageResource.INSTANCE; + } + + /** + * Returns resource readers registered as {@link ServiceLoader service providers} of type {@code + * org.pkl.core.resource.ResourceReader}. + */ + public static List fromServiceProviders() { + return FromServiceProviders.INSTANCE; + } + + private static final class EnvironmentVariable implements ResourceReader { + static final ResourceReader INSTANCE = new EnvironmentVariable(); + + @Override + public String getUriScheme() { + return "env"; + } + + @Override + public Optional read(URI uri) { + assert uri.getScheme().equals("env"); + + var context = VmContext.get(null); + var value = context.getEnvironmentVariables().get(uri.getSchemeSpecificPart()); + return Optional.ofNullable(value); + } + + @Override + public boolean hasHierarchicalUris() { + return false; + } + + @Override + public boolean isGlobbable() { + return true; + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws SecurityManagerException { + securityManager.checkResolveResource(baseUri); + var context = VmContext.get(null); + var ret = new ArrayList(); + for (var envVarName : context.getEnvironmentVariables().keySet()) { + ret.add(PathElement.opaque(envVarName)); + } + return ret; + } + } + + // to clearly separate the capability to read properties + // from their definition/storage, this class doesn't store + // properties but looks them up from the language context + private static final class ExternalProperty implements ResourceReader { + static final ResourceReader INSTANCE = new ExternalProperty(); + + @Override + public String getUriScheme() { + return "prop"; + } + + @Override + public Optional read(URI uri) { + assert uri.getScheme().equals("prop"); + + var context = VmContext.get(null); + var value = context.getExternalProperties().get(uri.getSchemeSpecificPart()); + return Optional.ofNullable(value); + } + + @Override + public boolean hasHierarchicalUris() { + return false; + } + + @Override + public boolean isGlobbable() { + return true; + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws SecurityManagerException { + securityManager.checkResolveResource(baseUri); + var context = VmContext.get(null); + var ret = new ArrayList(); + for (var propName : context.getExternalProperties().keySet()) { + ret.add(PathElement.opaque(propName)); + } + return ret; + } + } + + private static final class FileResource extends UrlResource { + static final ResourceReader INSTANCE = new FileResource(); + + @Override + public String getUriScheme() { + return "file"; + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return true; + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI elementUri) + throws SecurityManagerException { + securityManager.checkResolveResource(elementUri); + return FileResolver.hasElement(elementUri); + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveResource(baseUri); + return FileResolver.listElements(baseUri); + } + } + + private static final class HttpResource extends UrlResource { + static final ResourceReader INSTANCE = new HttpResource(); + + @Override + public String getUriScheme() { + return "http"; + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + } + + private static final class HttpsResource extends UrlResource { + static final ResourceReader INSTANCE = new HttpsResource(); + + @Override + public String getUriScheme() { + return "https"; + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + } + + private abstract static class UrlResource implements ResourceReader { + @Override + public Optional read(URI uri) throws IOException { + try { + var url = IoUtils.toUrl(uri); + var content = IoUtils.readBytes(url); + return Optional.of(new Resource(uri, content)); + } catch (FileNotFoundException e) { + return Optional.empty(); + } + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + } + + private static final class ClassPathResource implements ResourceReader { + private final ClassLoader classLoader; + + public ClassPathResource(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public String getUriScheme() { + return "modulepath"; + } + + @Override + public Optional read(URI uri) throws URISyntaxException { + assert uri.getScheme().equals("modulepath"); + if (uri.getPath() == null) { + throw new URISyntaxException( + uri.toString(), + ErrorMessages.create("invalidModuleUriMissingSlash", uri, "modulepath")); + } + var path = getResourcePath(uri); + try (var stream = classLoader.getResourceAsStream(path)) { + return stream == null + ? Optional.empty() + : Optional.of(new Resource(uri, stream.readAllBytes())); + } catch (IOException e) { + return Optional.empty(); + } + } + + private static String getResourcePath(URI uri) { + var path = uri.getPath(); + assert path.charAt(0) == '/'; + return path.substring(1); + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + + @Override + public boolean hasElement(SecurityManager manager, URI uri) throws SecurityManagerException { + manager.checkResolveResource(uri); + var uriPath = uri.getPath(); + assert uriPath.charAt(0) == '/'; + return classLoader.getResource(uriPath.substring(1)) != null; + } + } + + private static final class ModulePathResource implements ResourceReader { + private final ModulePathResolver resolver; + + public ModulePathResource(ModulePathResolver resolver) { + this.resolver = resolver; + } + + @Override + public String getUriScheme() { + return "modulepath"; + } + + @Override + public Optional read(URI uri) throws IOException, URISyntaxException { + assert uri.getScheme().equals("modulepath"); + + if (uri.getPath() == null) { + throw new URISyntaxException( + uri.toString(), + ErrorMessages.create("invalidModuleUriMissingSlash", uri, "modulepath")); + } + + try { + var path = resolver.resolve(uri); + var content = Files.readAllBytes(path); + return Optional.of(new Resource(uri, content)); + } catch (FileNotFoundException e) { + return Optional.empty(); + } + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return false; + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI elementUri) + throws SecurityManagerException { + securityManager.checkResolveResource(elementUri); + return resolver.hasElement(elementUri); + } + } + + /** Handler for {@code package} schemes */ + private static final class PackageResource implements ResourceReader { + static final PackageResource INSTANCE = new PackageResource(); + + @Override + public String getUriScheme() { + return "package"; + } + + @Override + public Optional read(URI uri) + throws IOException, URISyntaxException, SecurityManagerException { + var assetUri = new PackageAssetUri(uri); + var bytes = getPackageResolver().getBytes(assetUri, true, null); + return Optional.of(new Resource(uri, bytes)); + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return true; + } + + @Override + public boolean hasFragmentPaths() { + return true; + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveResource(baseUri); + var packageAssetUri = PackageAssetUri.create(baseUri); + return getPackageResolver() + .listElements(packageAssetUri, packageAssetUri.getPackageUri().getChecksums()); + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI elementUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveResource(elementUri); + var packageAssetUri = PackageAssetUri.create(elementUri); + return getPackageResolver() + .hasElement(packageAssetUri, packageAssetUri.getPackageUri().getChecksums()); + } + + private PackageResolver getPackageResolver() { + var packageResolver = VmContext.get(null).getPackageResolver(); + assert packageResolver != null; + return packageResolver; + } + } + + private static final class ProjectPackageResource implements ResourceReader { + static final ProjectPackageResource INSTANCE = new ProjectPackageResource(); + + @Override + public String getUriScheme() { + return "projectpackage"; + } + + @Override + public Optional read(URI uri) + throws IOException, URISyntaxException, SecurityManagerException { + var assetUri = new PackageAssetUri(uri); + var dependency = getProjectDepsResolver().getResolvedDependency(assetUri.getPackageUri()); + var path = getLocalPath(dependency, assetUri); + if (path != null) { + var url = path.toUri().toURL(); + var bytes = IoUtils.readBytes(url); + return Optional.of(new Resource(uri, bytes)); + } + var remoteDep = (Dependency.RemoteDependency) dependency; + var bytes = getPackageResolver().getBytes(assetUri, true, remoteDep.getChecksums()); + return Optional.of(new Resource(uri, bytes)); + } + + @Override + public boolean hasHierarchicalUris() { + return true; + } + + @Override + public boolean isGlobbable() { + return true; + } + + @Override + public boolean hasFragmentPaths() { + return true; + } + + @Override + public List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveResource(baseUri); + var packageAssetUri = PackageAssetUri.create(baseUri); + var dependency = + getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); + var path = getLocalPath(dependency, packageAssetUri); + if (path != null) { + return FileResolver.listElements(path); + } + var remoteDep = (Dependency.RemoteDependency) dependency; + return getPackageResolver() + .listElements(PackageAssetUri.create(baseUri), remoteDep.getChecksums()); + } + + @Override + public boolean hasElement(SecurityManager securityManager, URI elementUri) + throws IOException, SecurityManagerException { + securityManager.checkResolveResource(elementUri); + var packageAssetUri = PackageAssetUri.create(elementUri); + var dependency = + getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); + var path = getLocalPath(dependency, packageAssetUri); + if (path != null) { + return FileResolver.hasElement(path); + } + var remoteDep = (Dependency.RemoteDependency) dependency; + return getPackageResolver() + .hasElement(PackageAssetUri.create(elementUri), remoteDep.getChecksums()); + } + + private PackageResolver getPackageResolver() { + var packageResolver = VmContext.get(null).getPackageResolver(); + assert packageResolver != null; + return packageResolver; + } + + private ProjectDependenciesManager getProjectDepsResolver() { + var projectDepsManager = VmContext.get(null).getProjectDependenciesManager(); + assert projectDepsManager != null; + return projectDepsManager; + } + + private @Nullable Path getLocalPath(Dependency dependency, PackageAssetUri packageAssetUri) { + if (!(dependency instanceof LocalDependency)) { + return null; + } + return ((LocalDependency) dependency) + .resolveAssetPath(getProjectDepsResolver().getProjectDir(), packageAssetUri); + } + } + + private static class FromServiceProviders { + private static final List INSTANCE; + + static { + var loader = IoUtils.createServiceLoader(ResourceReader.class); + var readers = new ArrayList(); + loader.forEach(readers::add); + INSTANCE = Collections.unmodifiableList(readers); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/resource/package-info.java b/pkl-core/src/main/java/org/pkl/core/resource/package-info.java new file mode 100644 index 00000000..6c1c4baf --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/resource/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.resource; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java new file mode 100644 index 00000000..2d7a1e62 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java @@ -0,0 +1,406 @@ +/** + * 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.runtime; + +import static org.pkl.core.PClassInfo.pklBaseUri; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; + +public final class BaseModule extends StdLibModule { + static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(pklBaseUri, instance); + } + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getAnyClass() { + return AnyClass.instance; + } + + public static VmClass getTypedClass() { + return TypedClass.instance; + } + + public static VmClass getNullClass() { + return NullClass.instance; + } + + public static VmClass getNumberClass() { + return NumberClass.instance; + } + + public static VmClass getIntClass() { + return IntClass.instance; + } + + public static VmClass getFloatClass() { + return FloatClass.instance; + } + + public static VmClass getStringClass() { + return StringClass.instance; + } + + public static VmClass getBooleanClass() { + return BooleanClass.instance; + } + + public static VmClass getDurationClass() { + return DurationClass.instance; + } + + public static VmClass getDataSizeClass() { + return DataSizeClass.instance; + } + + public static VmClass getIntSeqClass() { + return IntSeqClass.instance; + } + + public static VmClass getCollectionClass() { + return CollectionClass.instance; + } + + public static VmClass getListClass() { + return ListClass.instance; + } + + public static VmClass getSetClass() { + return SetClass.instance; + } + + public static VmClass getListingClass() { + return ListingClass.instance; + } + + public static VmClass getMapClass() { + return MapClass.instance; + } + + public static VmClass getMappingClass() { + return MappingClass.instance; + } + + public static VmClass getDynamicClass() { + return DynamicClass.instance; + } + + public static VmClass getRenderDirectiveClass() { + return RenderDirectiveClass.instance; + } + + /** + * Returns class pkl.base#Module. For the module class of pkl.base use {@code + * getModule().getVmClass()}. + */ + public static VmClass getModuleClass() { + return ModuleClass.instance; + } + + public static VmClass getClassClass() { + return ClassClass.instance; + } + + public static VmClass getTypeAliasClass() { + return TypeAliasClass.instance; + } + + public static VmClass getRegexClass() { + return RegexClass.instance; + } + + public static VmClass getRegexMatchClass() { + return RegexMatchClass.instance; + } + + public static VmClass getFunctionClass() { + return FunctionClass.instance; + } + + public static VmClass getFunctionNClass(int paramCount) { + switch (paramCount) { + case 0: + return getFunction0Class(); + case 1: + return getFunction1Class(); + case 2: + return getFunction2Class(); + case 3: + return getFunction3Class(); + case 4: + return getFunction4Class(); + case 5: + return getFunction5Class(); + default: + CompilerDirectives.transferToInterpreter(); + throw new IllegalArgumentException( + String.format("Class `Function%d` does not exist.", paramCount)); + } + } + + public static VmClass getFunction0Class() { + return Function0Class.instance; + } + + public static VmClass getFunction1Class() { + return Function1Class.instance; + } + + public static VmClass getFunction2Class() { + return Function2Class.instance; + } + + public static VmClass getFunction3Class() { + return Function3Class.instance; + } + + public static VmClass getFunction4Class() { + return Function4Class.instance; + } + + public static VmClass getFunction5Class() { + return Function5Class.instance; + } + + public static VmClass getPairClass() { + return PairClass.instance; + } + + public static VmClass getVarArgsClass() { + return VarArgsClass.instance; + } + + public static VmClass getModuleInfoClass() { + return ModuleInfoClass.instance; + } + + public static VmClass getAnnotationClass() { + return AnnotationClass.instance; + } + + public static VmClass getDeprecatedClass() { + return DeprecatedClass.instance; + } + + public static VmClass getResourceClass() { + return ResourceClass.instance; + } + + public static VmTypeAlias getNonNullTypeAlias() { + return NonNullTypeAlias.instance; + } + + public static VmTypeAlias getInt8TypeAlias() { + return Int8TypeAlias.instance; + } + + public static VmTypeAlias getInt16TypeAlias() { + return Int16TypeAlias.instance; + } + + public static VmTypeAlias getInt32TypeAlias() { + return Int32TypeAlias.instance; + } + + public static VmTypeAlias getMixinTypeAlias() { + return MixinTypeAlias.instance; + } + + private static final class AnyClass { + static final VmClass instance = loadClass("Any"); + } + + private static final class TypedClass { + static final VmClass instance = loadClass("Typed"); + } + + private static final class NullClass { + static final VmClass instance = loadClass("Null"); + } + + private static final class NumberClass { + static final VmClass instance = loadClass("Number"); + } + + private static final class IntClass { + static final VmClass instance = loadClass("Int"); + } + + private static final class FloatClass { + static final VmClass instance = loadClass("Float"); + } + + private static final class StringClass { + static final VmClass instance = loadClass("String"); + } + + private static final class BooleanClass { + static final VmClass instance = loadClass("Boolean"); + } + + private static final class DurationClass { + static final VmClass instance = loadClass("Duration"); + } + + private static final class DataSizeClass { + static final VmClass instance = loadClass("DataSize"); + } + + private static final class IntSeqClass { + static final VmClass instance = loadClass("IntSeq"); + } + + private static final class CollectionClass { + static final VmClass instance = loadClass("Collection"); + } + + private static final class ListClass { + static final VmClass instance = loadClass("List"); + } + + private static final class SetClass { + static final VmClass instance = loadClass("Set"); + } + + private static final class ListingClass { + static final VmClass instance = loadClass("Listing"); + } + + private static final class MapClass { + static final VmClass instance = loadClass("Map"); + } + + private static final class MappingClass { + static final VmClass instance = loadClass("Mapping"); + } + + private static final class DynamicClass { + static final VmClass instance = loadClass("Dynamic"); + } + + private static final class ModuleClass { + static final VmClass instance = loadClass("Module"); + } + + private static final class ClassClass { + static final VmClass instance = loadClass("Class"); + } + + private static final class TypeAliasClass { + static final VmClass instance = loadClass("TypeAlias"); + } + + private static final class RegexClass { + static final VmClass instance = loadClass("Regex"); + } + + private static final class RegexMatchClass { + static final VmClass instance = loadClass("RegexMatch"); + } + + private static final class RenderDirectiveClass { + static final VmClass instance = loadClass("RenderDirective"); + } + + private static final class PairClass { + static final VmClass instance = loadClass("Pair"); + } + + private static final class VarArgsClass { + static final VmClass instance = loadClass("VarArgs"); + } + + private static final class ModuleInfoClass { + static final VmClass instance = loadClass("ModuleInfo"); + } + + private static final class AnnotationClass { + static final VmClass instance = loadClass("Annotation"); + } + + private static final class DeprecatedClass { + static final VmClass instance = loadClass("Deprecated"); + } + + private static final class ResourceClass { + static final VmClass instance = loadClass("Resource"); + } + + private static final class FunctionClass { + static final VmClass instance = loadClass("Function"); + } + + private static final class Function0Class { + static final VmClass instance = loadClass("Function0"); + } + + private static final class Function1Class { + static final VmClass instance = loadClass("Function1"); + } + + private static final class Function2Class { + static final VmClass instance = loadClass("Function2"); + } + + private static final class Function3Class { + static final VmClass instance = loadClass("Function3"); + } + + private static final class Function4Class { + static final VmClass instance = loadClass("Function4"); + } + + private static final class Function5Class { + static final VmClass instance = loadClass("Function5"); + } + + private static final class NonNullTypeAlias { + static final VmTypeAlias instance = loadTypeAlias("NonNull"); + } + + private static final class Int8TypeAlias { + static final VmTypeAlias instance = loadTypeAlias("Int8"); + } + + private static final class Int16TypeAlias { + static final VmTypeAlias instance = loadTypeAlias("Int16"); + } + + private static final class Int32TypeAlias { + static final VmTypeAlias instance = loadTypeAlias("Int32"); + } + + private static final class MixinTypeAlias { + static final VmTypeAlias instance = loadTypeAlias("Mixin"); + } + + @TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } + + @TruffleBoundary + private static VmTypeAlias loadTypeAlias(String typeAliasName) { + var theModule = getModule(); + return (VmTypeAlias) VmUtils.readMember(theModule, Identifier.get(typeAliasName)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/BenchmarkModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/BenchmarkModule.java new file mode 100644 index 00000000..77d12c7b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/BenchmarkModule.java @@ -0,0 +1,45 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.net.URI; + +public final class BenchmarkModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:Benchmark"), instance); + } + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getBenchmarkResultClass() { + return BenchmarkResultClass.instance; + } + + private static final class BenchmarkResultClass { + static final VmClass instance = loadClass("BenchmarkResult"); + } + + @TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/CertificateUtils.java b/pkl-core/src/main/java/org/pkl/core/runtime/CertificateUtils.java new file mode 100644 index 00000000..4d7f750a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/CertificateUtils.java @@ -0,0 +1,93 @@ +/** + * 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.runtime; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +public class CertificateUtils { + public static void setupAllX509CertificatesGlobally(List certs) { + try { + var certificates = new ArrayList(certs.size()); + for (var cert : certs) { + try (var input = toInputStream(cert)) { + certificates.addAll(generateCertificates(input)); + } + } + setupX509CertificatesGlobally(certificates); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static InputStream toInputStream(Object cert) throws IOException { + if (cert instanceof Path) { + var pathCert = (Path) cert; + return Files.newInputStream(pathCert); + } + if (cert instanceof InputStream) { + return (InputStream) cert; + } + throw new IllegalArgumentException( + "Unknown class for certificate: " + + cert.getClass() + + ". Valid types: java.nio.Path, java.io.InputStream"); + } + + private static Collection generateCertificates(InputStream inputStream) + throws CertificateException { + //noinspection unchecked + return (Collection) + CertificateFactory.getInstance("X.509").generateCertificates(inputStream); + } + + private static void setupX509CertificatesGlobally(Collection certs) + throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, + KeyManagementException { + System.setProperty("com.sun.net.ssl.checkRevocation", "true"); + Security.setProperty("ocsp.enable", "true"); + var keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(null); + + var count = 1; + for (var cert : certs) { + keystore.setCertificateEntry("Certificate" + count++, cert); + } + var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keystore); + + var sc = SSLContext.getInstance("SSL"); + sc.init(null, tmf.getTrustManagers(), new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/FileSystemManager.java b/pkl-core/src/main/java/org/pkl/core/runtime/FileSystemManager.java new file mode 100644 index 00000000..0b697050 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/FileSystemManager.java @@ -0,0 +1,169 @@ +/** + * 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.runtime; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.WatchService; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.graalvm.collections.EconomicMap; +import org.pkl.core.util.EconomicMaps; + +/** + * Manages file systems, potentially across multiple evaluator instances. + * + *

File systems are only closed when the last usage of it closes. + */ +public class FileSystemManager { + private static final EconomicMap fileSystems = EconomicMaps.create(); + + private static final Map counts = new IdentityHashMap<>(); + + private static final List externalFileSystems = new ArrayList<>(); + + public static synchronized FileSystem getFileSystem(URI uri) throws IOException { + var fs = fileSystems.get(uri); + if (fs != null) { + counts.put(fs, counts.get(fs) + 1); + return fs; + } + try { + fs = new Handle(FileSystems.newFileSystem(uri, new HashMap<>())); + fileSystems.put(uri, fs); + counts.put(fs, 1); + return fs; + } catch (FileSystemAlreadyExistsException e) { + fs = new Handle(FileSystems.getFileSystem(uri)); + // Something other than Pkl is holding onto this file system. + // Mark it as external, so Pkl does not close it. + externalFileSystems.add(fs); + fileSystems.put(uri, fs); + counts.put(fs, 1); + return fs; + } + } + + /** + * Possibily close this file system. Will not close if the file system was initialized externally + * to Pkl. + */ + private static synchronized void close(Handle fs) throws IOException { + var count = counts.get(fs) - 1; + if (count > 0) { + counts.put(fs, count); + return; + } + counts.remove(fs); + var cursor = fileSystems.getEntries(); + while (cursor.advance()) { + var fileSystem = cursor.getValue(); + if (fileSystem.equals(fs)) { + var key = cursor.getKey(); + //noinspection resource + fileSystems.removeKey(key); + break; + } + } + var isExternal = externalFileSystems.contains(fs); + if (isExternal) { + externalFileSystems.remove(fs); + } else { + fs.delegate.close(); + } + } + + private static class Handle extends FileSystem { + + final FileSystem delegate; + + public Handle(FileSystem delegate) { + this.delegate = delegate; + } + + @Override + public FileSystemProvider provider() { + return delegate.provider(); + } + + @Override + public void close() throws IOException { + FileSystemManager.close(this); + } + + @Override + public boolean isOpen() { + return delegate.isOpen(); + } + + @Override + public boolean isReadOnly() { + return delegate.isReadOnly(); + } + + @Override + public String getSeparator() { + return delegate.getSeparator(); + } + + @Override + public Iterable getRootDirectories() { + return delegate.getRootDirectories(); + } + + @Override + public Iterable getFileStores() { + return delegate.getFileStores(); + } + + @Override + public Set supportedFileAttributeViews() { + return delegate.supportedFileAttributeViews(); + } + + @Override + public Path getPath(String first, String... more) { + return delegate.getPath(first, more); + } + + @Override + public PathMatcher getPathMatcher(String syntaxAndPattern) { + return delegate.getPathMatcher(syntaxAndPattern); + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + return delegate.getUserPrincipalLookupService(); + } + + @Override + public WatchService newWatchService() throws IOException { + return delegate.newWatchService(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java new file mode 100644 index 00000000..9644e87e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java @@ -0,0 +1,225 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** Note: this class has a natural ordering that is inconsistent with equals. */ +public final class Identifier implements Comparable { + /** Pool for non-local members. */ + private static final Map pool = new ConcurrentHashMap<>(); + + /** Pool for local properties. Also used for frame slots of `for` expression variables. */ + private static final Map localPropertyPool = new ConcurrentHashMap<>(); + + /** Pool for local methods. */ + private static final Map localMethodPool = new ConcurrentHashMap<>(); + + // collection literals + public static final Identifier LIST = get("List"); + public static final Identifier SET = get("Set"); + public static final Identifier MAP = get("Map"); + + // members of pkl.base + public static final Identifier ANY = get("Any"); + public static final Identifier TYPED = get("Typed"); + public static final Identifier MODULE = get("Module"); + public static final Identifier MODULE_INFO = get("ModuleInfo"); + + // members of pkl.base#Any + public static final Identifier TO_STRING = get("toString"); + + // members of pkl.base#Annotation and its children + public static final Identifier MESSAGE = get("message"); + + // members of pkl.base#Listing and pkl.base#Mapping + public static final Identifier DEFAULT = get("default"); + + // members of pkl.base#ValueRenderer subclasses + public static final Identifier MODE = get("mode"); + public static final Identifier INDENT = get("indent"); + public static final Identifier INDENT_WIDTH = get("indentWidth"); + public static final Identifier OMIT_NULL_PROPERTIES = get("omitNullProperties"); + public static final Identifier USE_CUSTOM_STRING_DELIMITERS = get("useCustomStringDelimiters"); + public static final Identifier IS_STREAM = get("isStream"); + public static final Identifier RESTRICT_CHARSET = get("restrictCharset"); + public static final Identifier XML_VERSION = get("xmlVersion"); + public static final Identifier ROOT_ELEMENT_NAME = get("rootElementName"); + public static final Identifier ROOT_ELEMENT_ATTRIBUTES = get("rootElementAttributes"); + public static final Identifier CONVERTERS = get("converters"); + public static final Identifier USE_MAPPING = get("useMapping"); + + // members of pkl.base#IntSeq, pkl.base#RegexMatch + public static final Identifier START = get("start"); + public static final Identifier END = get("end"); + + // members of pkl.base#RegexMatch + public static final Identifier VALUE = get("value"); + public static final Identifier GROUPS = get("groups"); + + // members of pkl.base#Module + public static final Identifier OUTPUT = get("output"); + public static final Identifier FILES = get("files"); + + // members of pkl.base#{ModuleOutput, Resource, RenderDirective, PcfRenderDirective, XmlComment, + // XmlCData} + public static final Identifier TEXT = get("text"); + + // members of pkl.base#ModuleOutput, pkl.base#Resource, pkl.base#String + public static final Identifier BASE64 = get("base64"); + + // members of pkl.base#Resource + public static final Identifier URI = get("uri"); + + // members of pkl.base#ModuleInfo + public static final Identifier MIN_PKL_VERSION = get("minPklVersion"); + + // members of pkl.base#Duration + public static final Identifier NS = get("ns"); + public static final Identifier US = get("us"); + public static final Identifier MS = get("ms"); + public static final Identifier S = get("s"); + public static final Identifier MIN = get("min"); + public static final Identifier H = get("h"); + public static final Identifier D = get("d"); + + // members of pkl.base#DataSize + public static final Identifier B = get("b"); + public static final Identifier KB = get("kb"); + public static final Identifier KIB = get("kib"); + public static final Identifier MB = get("mb"); + public static final Identifier MIB = get("mib"); + public static final Identifier GB = get("gb"); + public static final Identifier GIB = get("gib"); + public static final Identifier TB = get("tb"); + public static final Identifier TIB = get("tib"); + public static final Identifier PB = get("pb"); + public static final Identifier PIB = get("pib"); + + // members of pkl.base#Pair + public static final Identifier FIRST = get("first"); + public static final Identifier SECOND = get("second"); + + // members of pkl.base#Function(1-5) + public static final Identifier APPLY = get("apply"); + + // members of pkl.base#PcfRenderDirective + public static final Identifier BEFORE = get("before"); + public static final Identifier AFTER = get("after"); + + // members of pkl.base#XmlElement, pkl.jsonnet#ExtVar + public static final Identifier IS_XML_ELEMENT = get("_isXmlElement"); + public static final Identifier NAME = get("name"); + public static final Identifier ATTRIBUTES = get("attributes"); + public static final Identifier IS_BLOCK_FORMAT = get("isBlockFormat"); + + // members of pkl.jsonnet#ImportStr + public static final Identifier PATH = get("path"); + + // members of pkl.test + public static final Identifier FACTS = get("facts"); + public static final Identifier EXAMPLES = get("examples"); + + // members of pkl.benchmark + public static final Identifier ITERATIONS = get("iterations"); + public static final Identifier ITERATION_TIME = get("iterationTime"); + public static final Identifier IS_VERBOSE = get("isVerbose"); + public static final Identifier EXPRESSION = get("expression"); + public static final Identifier SOURCE_MODULE = get("sourceModule"); + public static final Identifier SOURCE_TEXT = get("sourceText"); + public static final Identifier SOURCE_URI = get("sourceUri"); + + // members of pkl.yaml + public static final Identifier MAX_COLLECTION_ALIASES = get("maxCollectionAliases"); + + public static final Identifier DEPENDENCIES = get("dependencies"); + + // common in lambdas etc + public static final Identifier IT = get("it"); + + private final String name; + + private Identifier(String name) { + this.name = name; + } + + @TruffleBoundary + public static Identifier get(String name) { + return pool.computeIfAbsent(name, Identifier::new); + } + + @TruffleBoundary + public static Identifier localProperty(String name) { + return localPropertyPool.computeIfAbsent(name, Identifier::new); + } + + @TruffleBoundary + public static Identifier localMethod(String name) { + return localMethodPool.computeIfAbsent(name, Identifier::new); + } + + @TruffleBoundary + public static Identifier property(String name, boolean isLocal) { + return isLocal ? localProperty(name) : get(name); + } + + @TruffleBoundary + public static Identifier method(String name, boolean isLocal) { + return isLocal ? localMethod(name) : get(name); + } + + public Identifier toLocalProperty() { + return localProperty(name); + } + + public Identifier toRegular() { + return get(name); + } + + public Identifier toLocalMethod() { + return localMethod(name); + } + + public boolean isRegular() { + return get(name) == this; + } + + // not named isLocalProperty() to work around https://bugs.openjdk.java.net/browse/JDK-8185424 + // (which is apparently related to `Xdoclint:none` option) + public boolean isLocalProp() { + return localProperty(name) == this; + } + + public boolean isLocalMethod() { + return localMethod(name) == this; + } + + @Override + @TruffleBoundary + public int compareTo(Identifier other) { + return name.compareTo(other.name); + } + + // equals and hashCode intentionally inherited from Object + + @Override + @TruffleBoundary + public String toString() { + return name; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/Iterators.java b/pkl-core/src/main/java/org/pkl/core/runtime/Iterators.java new file mode 100644 index 00000000..b4105c77 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/Iterators.java @@ -0,0 +1,150 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.*; +import java.util.function.Consumer; + +public final class Iterators { + private Iterators() {} + + @SuppressWarnings("unchecked") + public static Iterator emptyTruffleIterator() { + return EMPTY_TRUFFLE_ITERATOR; + } + + /** An empty iterator that performs all work behind Truffle boundaries. */ + @SuppressWarnings("rawtypes") + private static final Iterator EMPTY_TRUFFLE_ITERATOR = + new Iterator() { + @TruffleBoundary + @Override + public boolean hasNext() { + return false; + } + + @TruffleBoundary + @Override + public Object next() { + throw new NoSuchElementException(); + } + + @TruffleBoundary + @Override + public void remove() { + throw new IllegalStateException(); + } + + @TruffleBoundary + @Override + public void forEachRemaining(Consumer action) { + throw new UnsupportedOperationException("forEachRemaining"); + } + }; + + /** An iterator for iterables that performs all work behind Truffle boundaries. */ + public static final class TruffleIterator implements Iterator { + private final Iterator delegate; + + // accepting Iterable instead of Iterator puts Iterable.iterator() behind a Truffle boundary + @TruffleBoundary + public TruffleIterator(Iterable iterable) { + delegate = iterable.iterator(); + } + + @Override + @TruffleBoundary + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + @TruffleBoundary + public T next() { + return delegate.next(); + } + + @Override + @TruffleBoundary + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + @TruffleBoundary + public void forEachRemaining(Consumer action) { + throw new UnsupportedOperationException("forEachRemaining"); + } + } + + /** A reverse iterator for lists that performs all work behind Truffle boundaries. */ + public static final class ReverseTruffleIterator implements Iterator { + private final ListIterator delegate; + + // accepting List instead of ListIterator puts List.listIterator() behind a Truffle boundary + @TruffleBoundary + public ReverseTruffleIterator(List list) { + delegate = list.listIterator(list.size()); + } + + @Override + @TruffleBoundary + public boolean hasNext() { + return delegate.hasPrevious(); + } + + @Override + @TruffleBoundary + public T next() { + return delegate.previous(); + } + + @Override + @TruffleBoundary + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + @TruffleBoundary + public void forEachRemaining(Consumer action) { + throw new UnsupportedOperationException("forEachRemaining"); + } + } + + /** A reverse iterator for arrays. */ + public static final class ReverseArrayIterator implements Iterator { + private final Object[] array; + + private int nextIndex; + + public ReverseArrayIterator(Object[] array) { + this.array = array; + nextIndex = array.length - 1; + } + + @Override + public boolean hasNext() { + return nextIndex >= 0; + } + + @Override + public Object next() { + return array[nextIndex--]; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/JsonnetModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/JsonnetModule.java new file mode 100644 index 00000000..9bf9821f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/JsonnetModule.java @@ -0,0 +1,53 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.net.URI; + +public final class JsonnetModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:jsonnet"), instance); + } + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getImportStrClass() { + return ImportStrClass.instance; + } + + public static VmClass getExtVarClass() { + return ExtVarClass.instance; + } + + private static final class ImportStrClass { + static final VmClass instance = loadClass("ImportStr"); + } + + private static final class ExtVarClass { + static final VmClass instance = loadClass("ExtVar"); + } + + @TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/KeyLookupSuggestions.java b/pkl-core/src/main/java/org/pkl/core/runtime/KeyLookupSuggestions.java new file mode 100644 index 00000000..85402abd --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/KeyLookupSuggestions.java @@ -0,0 +1,96 @@ +/** + * 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.runtime; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.pkl.core.ValueFormatter; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.StringSimilarity; + +public class KeyLookupSuggestions { + private static final StringSimilarity STRING_SIMILARITY = new StringSimilarity(); + // 0.77 is just about low enough to consider two three-character + // keys that differ in their first character similar + private static final double SIMILARITY_THRESHOLD = 0.77; + + public static List forMap(VmMap map, String key) { + var candidates = new ArrayList(); + + map.forEach( + entry -> { + if (!(entry.getKey() instanceof String)) return; + var entryKey = (String) entry.getKey(); + var similarity = STRING_SIMILARITY.similarity(entryKey, key); + if (similarity >= SIMILARITY_THRESHOLD) { + candidates.add(new Candidate(entryKey, similarity)); + } + }); + + candidates.sort(Comparator.naturalOrder()); + return candidates; + } + + public static List forObject(VmObjectLike object, String key) { + var candidates = new ArrayList(); + + object.iterateMemberValues( + (memberKey, member, value) -> { + if (!(memberKey instanceof String)) return true; + var stringKey = (String) memberKey; + var similarity = STRING_SIMILARITY.similarity(stringKey, key); + if (similarity >= SIMILARITY_THRESHOLD) { + candidates.add(new Candidate(stringKey, similarity)); + } + return true; + }); + + candidates.sort(Comparator.naturalOrder()); + return candidates; + } + + public static final class Candidate implements Comparable { + private final String key; + private final double similarity; + + public Candidate(String key, double similarity) { + this.key = key; + this.similarity = similarity; + } + + // note: not consistent with equals + @Override + public int compareTo(Candidate other) { + return Double.compare(other.similarity, similarity); + } + + @Override + public boolean equals(@Nullable Object obj) { + return (obj instanceof Candidate && ((Candidate) obj).key.equals(key)); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public String toString() { + return ValueFormatter.basic().formatStringValue(key, ""); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/LoggerImpl.java b/pkl-core/src/main/java/org/pkl/core/runtime/LoggerImpl.java new file mode 100644 index 00000000..412badc8 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/LoggerImpl.java @@ -0,0 +1,41 @@ +/** + * 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.runtime; + +import org.pkl.core.Logger; +import org.pkl.core.StackFrame; +import org.pkl.core.StackFrameTransformer; + +public final class LoggerImpl implements Logger { + + private final Logger delegate; + private final StackFrameTransformer transformer; + + public LoggerImpl(Logger delegate, StackFrameTransformer transformer) { + this.delegate = delegate; + this.transformer = transformer; + } + + @Override + public void trace(String message, StackFrame frame) { + delegate.trace(message, transformer.apply(frame)); + } + + @Override + public void warn(String message, StackFrame frame) { + delegate.warn(message, transformer.apply(frame)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/MathModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/MathModule.java new file mode 100644 index 00000000..17576aaa --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/MathModule.java @@ -0,0 +1,39 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives; +import java.net.URI; + +public final class MathModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:math"), instance); + } + + private MathModule() {} + + public static VmTyped getModule() { + return instance; + } + + @CompilerDirectives.TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/MemberLookupSuggestions.java b/pkl-core/src/main/java/org/pkl/core/runtime/MemberLookupSuggestions.java new file mode 100644 index 00000000..3331ff27 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/MemberLookupSuggestions.java @@ -0,0 +1,169 @@ +/** + * 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.runtime; + +import java.util.*; +import java.util.stream.Collectors; +import org.pkl.core.ast.member.ClassMethod; +import org.pkl.core.ast.member.Member; +import org.pkl.core.runtime.MemberLookupSuggestions.Candidate.Kind; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.StringSimilarity; + +public class MemberLookupSuggestions { + private static final StringSimilarity STRING_SIMILARITY = new StringSimilarity(); + // 0.77 is just about low enough to consider two three-character + // names that differ in their first character similar + private static final double SIMILARITY_THRESHOLD = 0.77; + + private final VmObjectLike composite; + private final Object memberName; + // -1 for property, #arguments for function + private final int memberArity; + + private final Set memberKinds; + private final Set candidates = new LinkedHashSet<>(); + + public MemberLookupSuggestions( + VmObjectLike composite, Object memberName, int memberArity, Set memberKinds) { + this.composite = composite; + this.memberName = memberName; + this.memberArity = memberArity; + this.memberKinds = memberKinds; + } + + // use same search order as in member lookup, + // so that in case of members with same name, + // (only) the correct one is suggested + public List find(boolean isImplicitReceiver) { + candidates.clear(); + + if (isImplicitReceiver) { + for (var curr = composite; curr != null; curr = curr.getEnclosingOwner()) { + addPropertyCandidates(curr, true); + if (curr.isPrototype()) { + addMethodCandidates(curr.getVmClass().getDeclaredMethods(), true); + } + } + addPropertyCandidates(BaseModule.getModule(), false); + addMethodCandidates(BaseModule.getModule().getVmClass().getMethods(), false); + } + + for (var curr = composite; curr != null; curr = curr.getParent()) { + addPropertyCandidates(curr, false); + } + addMethodCandidates(composite.getVmClass().getMethods(), false); + + return candidates.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList()); + } + + private void addPropertyCandidates(VmObjectLike object, boolean includeLocal) { + if (!memberKinds.contains(Kind.PROPERTY)) return; + + for (var member : EconomicMaps.getValues(object.getMembers())) { + addIfSimilar(member, Candidate.Kind.PROPERTY, -1, includeLocal); + } + } + + private void addMethodCandidates(Iterable methods, boolean includeLocal) { + if (!memberKinds.contains(Kind.METHOD)) return; + + for (var method : methods) { + addIfSimilar(method, Candidate.Kind.METHOD, method.getParameterCount(), includeLocal); + } + } + + private void addIfSimilar(Member member, Candidate.Kind kind, int arity, boolean includeLocal) { + + var memberName = member.getNameOrNull(); + if (memberName == null) return; + + if (includeLocal || !member.isLocal()) { + var nameSimilarity = + STRING_SIMILARITY.similarity(memberName.toString(), this.memberName.toString()); + if (nameSimilarity >= SIMILARITY_THRESHOLD) { + var arityDifference = Math.abs(arity - memberArity); + var signature = member.getCallSignature(); + assert signature != null; + if (nameSimilarity < 1 || memberArity == 0) { + candidates.add( + new Candidate( + kind, memberName.toString(), signature, nameSimilarity, arityDifference)); + } else if (nameSimilarity == 1 && memberArity >= 0) { + candidates.add( + new Candidate( + kind, + memberName.toString(), + signature + ".apply(...)", + nameSimilarity, + arityDifference)); + } + } + } + } + + public static final class Candidate implements Comparable { + private final Kind kind; + private final String name; + private final String callSignature; + private final double nameSimilarity; + private final int arityDifference; + + public Candidate( + Kind kind, String name, String callSignature, double nameSimilarity, int arityDifference) { + this.kind = kind; + this.name = name; + this.callSignature = callSignature; + this.nameSimilarity = nameSimilarity; + this.arityDifference = arityDifference; + } + + // note: not consistent with equals (hence cannot use TreeSet) + @Override + public int compareTo(Candidate other) { + if (nameSimilarity == other.nameSimilarity) { + return Integer.compare(arityDifference, other.arityDifference); + } + return Double.compare(other.nameSimilarity, nameSimilarity); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof Candidate)) return false; + + var other = (Candidate) obj; + // member lookup is name rather than signature based (but distinguishes kind) + return kind == other.kind && name.equals(other.name); + } + + @Override + public int hashCode() { + return kind.hashCode() * 31 + name.hashCode(); + } + + @Override + public String toString() { + return callSignature; + } + + public enum Kind { + PROPERTY, + METHOD + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/MinPklVersionChecker.java b/pkl-core/src/main/java/org/pkl/core/runtime/MinPklVersionChecker.java new file mode 100644 index 00000000..4e7c77e0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/MinPklVersionChecker.java @@ -0,0 +1,107 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.nodes.Node; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RuleContext; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.pkl.core.Release; +import org.pkl.core.Version; +import org.pkl.core.parser.antlr.PklParser.*; +import org.pkl.core.util.Nullable; + +final class MinPklVersionChecker { + private static final Version currentVersion = Release.current().version(); + + // only use major/minor/patch for version check to ease working with dev versions + private static final Version currentMajorMinorPatchVersion = currentVersion.toNormal(); + + static void check(VmTyped module, @Nullable Node importNode) { + assert module.isModuleObject(); + + for (var ann : module.getModuleInfo().getAnnotations()) { + if (ann.getVmClass() != BaseModule.getModuleInfoClass()) continue; + + // parsing should never fail due to pkl.base#ModuleInfo.minPklVersion's type constraint + var requiredVersion = + Version.parse((String) VmUtils.readMember(ann, Identifier.MIN_PKL_VERSION)); + doCheck(module.getModuleInfo().getModuleName(), requiredVersion, importNode); + return; + } + } + + static void check(String moduleName, @Nullable ParserRuleContext ctx, @Nullable Node importNode) { + if (!(ctx instanceof ModuleContext)) return; + + var moduleDeclCtx = ((ModuleContext) ctx).moduleDecl(); + if (moduleDeclCtx == null) return; + + for (var annCtx : moduleDeclCtx.annotation()) { + if (!Identifier.MODULE_INFO.toString().equals(getLastIdText(annCtx.type()))) continue; + + var objectBodyCtx = annCtx.objectBody(); + if (objectBodyCtx == null) continue; + + for (var memberCtx : objectBodyCtx.objectMember()) { + if (!(memberCtx instanceof ObjectPropertyContext)) continue; + + var propertyCtx = (ObjectPropertyContext) memberCtx; + if (!Identifier.MIN_PKL_VERSION.toString().equals(getText(propertyCtx.Identifier()))) + continue; + + var versionText = getText(propertyCtx.expr()); + if (versionText == null) continue; + + Version version; + try { + version = Version.parse(versionText.substring(1, versionText.length() - 1)); + } catch (IllegalArgumentException e) { + return; + } + + doCheck(moduleName, version, importNode); + return; + } + } + } + + private static @Nullable String getText(@Nullable RuleContext ruleCtx) { + return ruleCtx == null ? null : ruleCtx.getText(); + } + + private static @Nullable String getLastIdText(@Nullable TypeContext typeCtx) { + if (!(typeCtx instanceof DeclaredTypeContext)) return null; + var declCtx = (DeclaredTypeContext) typeCtx; + var token = declCtx.qualifiedIdentifier().Identifier; + return token == null ? null : token.getText(); + } + + private static @Nullable String getText(@Nullable TerminalNode idCtx) { + return idCtx == null ? null : idCtx.getText(); + } + + private static void doCheck( + String moduleName, @Nullable Version requiredVersion, @Nullable Node importNode) { + if (requiredVersion == null || currentMajorMinorPatchVersion.compareTo(requiredVersion) >= 0) + return; + + throw new VmExceptionBuilder() + .withOptionalLocation(importNode) + .evalError("incompatiblePklVersion", moduleName, requiredVersion, currentVersion) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/MirrorFactories.java b/pkl-core/src/main/java/org/pkl/core/runtime/MirrorFactories.java new file mode 100644 index 00000000..36e08b51 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/MirrorFactories.java @@ -0,0 +1,277 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.member.ClassMethod; +import org.pkl.core.ast.member.ClassProperty; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.ast.type.TypeNode.*; +import org.pkl.core.stdlib.VmObjectFactory; +import org.pkl.core.stdlib.VmObjectFactory.Property; +import org.pkl.core.util.Pair; + +public final class MirrorFactories { + private MirrorFactories() {} + + public static final VmObjectFactory moduleFactory = + new VmObjectFactory<>(ReflectModule::getModuleClass); + + public static final VmObjectFactory classFactory = + new VmObjectFactory<>(ReflectModule::getClassClass); + + public static final VmObjectFactory typeAliasFactory = + new VmObjectFactory<>(ReflectModule::getTypeAliasClass); + + public static final VmObjectFactory propertyFactory = + new VmObjectFactory<>(ReflectModule::getPropertyClass); + + public static final VmObjectFactory methodFactory = + new VmObjectFactory<>(ReflectModule::getMethodClass); + + public static final VmObjectFactory> methodParameterFactory = + new VmObjectFactory<>(ReflectModule::getMethodParameterClass); + + public static final VmObjectFactory typeParameterFactory = + new VmObjectFactory<>(ReflectModule::getTypeParameterClass); + + public static final VmObjectFactory classTypeFactory = + new VmObjectFactory<>(ReflectModule::getDeclaredTypeClass); + + public static final VmObjectFactory typeAliasTypeFactory = + new VmObjectFactory<>(ReflectModule::getDeclaredTypeClass); + + public static final VmObjectFactory> declaredTypeFactory = + new VmObjectFactory<>(ReflectModule::getDeclaredTypeClass); + + public static final VmObjectFactory stringLiteralTypeFactory = + new VmObjectFactory<>(ReflectModule::getStringLiteralTypeClass); + + public static final VmObjectFactory stringLiteralTypeFactory2 = + new VmObjectFactory<>(ReflectModule::getStringLiteralTypeClass); + + public static final VmObjectFactory unionTypeFactory = + new VmObjectFactory<>(ReflectModule::getUnionTypeClass); + + public static final VmObjectFactory unionTypeFactory2 = + new VmObjectFactory<>(ReflectModule::getUnionTypeClass); + + public static final VmObjectFactory + unionOfStringLiteralsTypeFactory = new VmObjectFactory<>(ReflectModule::getUnionTypeClass); + + public static final VmObjectFactory unionOfStringLiteralsTypeFactory2 = + new VmObjectFactory<>(ReflectModule::getUnionTypeClass); + + public static final VmObjectFactory nullableTypeFactory = + new VmObjectFactory<>(ReflectModule::getNullableTypeClass); + + public static final VmObjectFactory nullableTypeFactory2 = + new VmObjectFactory<>(ReflectModule::getNullableTypeClass); + + public static final VmObjectFactory functionTypeFactory = + new VmObjectFactory<>(ReflectModule::getFunctionTypeClass); + + public static final VmObjectFactory> functionTypeFactory2 = + new VmObjectFactory<>(ReflectModule::getFunctionTypeClass); + + public static final VmObjectFactory typeVariableFactory = + new VmObjectFactory<>(ReflectModule::getTypeVariableClass); + + public static final VmObjectFactory typeVariableFactory2 = + new VmObjectFactory<>(ReflectModule::getTypeVariableClass); + + public static final VmObjectFactory moduleTypeFactory = + new VmObjectFactory<>(ReflectModule::getModuleTypeClass); + + public static final VmObjectFactory unknownTypeFactory = + new VmObjectFactory<>(ReflectModule::getUnknownTypeClass); + + public static final VmObjectFactory nothingTypeFactory = + new VmObjectFactory<>(ReflectModule::getNothingTypeClass); + + public static final VmObjectFactory sourceLocationFactory = + new VmObjectFactory<>(ReflectModule::getSourceLocationClass); + + static { + moduleFactory + .addTypedProperty( + "location", + module -> sourceLocationFactory.create(module.getModuleInfo().getHeaderSection())) + .addProperty( + "docComment", + module -> VmNull.lift(VmUtils.exportDocComment(module.getModuleInfo().getDocComment()))) + .addListProperty( + "annotations", module -> VmList.create(module.getModuleInfo().getAnnotations())) + .addSetProperty( + "modifiers", + module -> + module.getModuleInfo().isAmend() + ? VmSet.EMPTY + : module.getVmClass().getModifierMirrors()) + .addStringProperty("name", module -> module.getModuleInfo().getModuleName()) + .addStringProperty( + "uri", module -> module.getModuleInfo().getModuleKey().getUri().toString()) + .addTypedProperty("reflectee", Property.identity()) + .addTypedProperty("moduleClass", module -> module.getVmClass().getMirror()) + .addProperty("supermodule", VmTyped::getSupermoduleMirror) + .addBooleanProperty("isAmend", module -> module.getModuleInfo().isAmend()) + .addMapProperty("imports", VmTyped::getImports) + .addMapProperty("classes", VmTyped::getClassMirrors) + .addMapProperty("typeAliases", VmTyped::getTypeAliasMirrors); + + classFactory + .addTypedProperty( + "location", clazz -> sourceLocationFactory.create(clazz.getHeaderSection())) + .addProperty( + "docComment", clazz -> VmNull.lift(VmUtils.exportDocComment(clazz.getDocComment()))) + .addListProperty("annotations", clazz -> VmList.create(clazz.getAnnotations())) + .addSetProperty("modifiers", VmClass::getModifierMirrors) + .addStringProperty("name", VmClass::getSimpleName) + .addClassProperty("reflectee", Property.identity()) + .addListProperty("typeParameters", VmClass::getTypeParameterMirrors) + .addProperty("superclass", VmClass::getSuperclassMirror) + .addProperty("supertype", VmClass::getSupertypeMirror) + .addMapProperty("properties", VmClass::getPropertyMirrors) + .addTypedProperty("enclosingDeclaration", VmClass::getModuleMirror) + .addMapProperty("methods", VmClass::getMethodMirrors); + + typeAliasFactory + .addTypedProperty( + "location", alias -> sourceLocationFactory.create(alias.getHeaderSection())) + .addProperty( + "docComment", alias -> VmNull.lift(VmUtils.exportDocComment(alias.getDocComment()))) + .addListProperty("annotations", alias -> VmList.create(alias.getAnnotations())) + .addSetProperty("modifiers", VmTypeAlias::getModifierMirrors) + .addStringProperty("name", VmTypeAlias::getSimpleName) + .addProperty("reflectee", Property.identity()) + .addListProperty("typeParameters", VmTypeAlias::getTypeParameterMirrors) + .addTypedProperty("enclosingDeclaration", VmTypeAlias::getModuleMirror) + .addTypedProperty("referent", VmTypeAlias::getTypeMirror); + + propertyFactory + .addTypedProperty( + "location", property -> sourceLocationFactory.create(property.getHeaderSection())) + .addProperty( + "docComment", + property -> VmNull.lift(VmUtils.exportDocComment(property.getDocComment()))) + .addListProperty("annotations", property -> VmList.create(property.getAnnotations())) + .addSetProperty("modifiers", ClassProperty::getModifierMirrors) + .addStringProperty("name", property -> property.getName().toString()) + .addTypedProperty("type", ClassProperty::getTypeMirror) + .addProperty( + "defaultValue", + property -> + property.isAbstract() + || property.isExternal() + || property.getInitializer().isUndefined() + ? VmNull.withoutDefault() + : + // get default from prototype because it's cached there + VmUtils.readMember(property.getOwner(), property.getName())); + + methodFactory + .addTypedProperty( + "location", method -> sourceLocationFactory.create(method.getHeaderSection())) + .addProperty( + "docComment", method -> VmNull.lift(VmUtils.exportDocComment(method.getDocComment()))) + .addListProperty("annotations", method -> VmList.create(method.getAnnotations())) + .addSetProperty("modifiers", ClassMethod::getModifierMirrors) + .addListProperty("typeParameters", ClassMethod::getTypeParameterMirrors) + .addStringProperty("name", method -> method.getName().toString()) + .addMapProperty("parameters", ClassMethod::getParameterMirrors) + .addTypedProperty("returnType", ClassMethod::getReturnTypeMirror); + + methodParameterFactory + .addStringProperty("name", Pair::getFirst) + .addTypedProperty("type", Pair::getSecond); + + typeParameterFactory + .addStringProperty("name", TypeParameter::getName) + .addProperty( + "variance", + typeParameter -> { + switch (typeParameter.getVariance()) { + case COVARIANT: + return "out"; + case CONTRAVARIANT: + return "in"; + default: + return VmNull.withoutDefault(); + } + }); + + classTypeFactory + .addTypedProperty( + "referent", + typeNode -> { + var clazz = typeNode.getVmClass(); + assert clazz != null; + return clazz.getMirror(); + }) + .addListProperty("typeArguments", TypeNode::getTypeArgumentMirrors); + + typeAliasTypeFactory + .addTypedProperty( + "referent", + typeNode -> { + var alias = typeNode.getVmTypeAlias(); + assert alias != null; + return alias.getMirror(); + }) + .addListProperty("typeArguments", TypeNode::getTypeArgumentMirrors); + + declaredTypeFactory + .addTypedProperty("referent", Pair::getFirst) + .addListProperty("typeArguments", Pair::getSecond); + + functionTypeFactory + .addListProperty("parameterTypes", FunctionTypeNode::getParameterTypeMirrors) + .addTypedProperty("returnType", FunctionTypeNode::getReturnTypeMirror); + + functionTypeFactory2 + .addListProperty("parameterTypes", Pair::getFirst) + .addTypedProperty("returnType", Pair::getSecond); + + stringLiteralTypeFactory.addStringProperty("value", StringLiteralTypeNode::getLiteral); + + stringLiteralTypeFactory2.addStringProperty("value", Property.identity()); + + unionTypeFactory.addListProperty("members", UnionTypeNode::getElementTypeMirrors); + + unionTypeFactory2.addListProperty("members", Property.identity()); + + unionOfStringLiteralsTypeFactory.addListProperty( + "members", UnionOfStringLiteralsTypeNode::getElementTypeMirrors); + + unionOfStringLiteralsTypeFactory2.addListProperty("members", Property.identity()); + + nullableTypeFactory.addTypedProperty("member", NullableTypeNode::getElementTypeMirror); + + nullableTypeFactory2.addTypedProperty("member", Property.identity()); + + typeVariableFactory.addTypedProperty("referent", TypeVariableNode::getTypeParameterMirror); + + typeVariableFactory2.addTypedProperty("referent", Property.identity()); + + sourceLocationFactory + .addIntProperty("line", SourceSection::getStartLine) + .addIntProperty("column", SourceSection::getStartColumn) + .addStringProperty( + "displayUri", + section -> VmUtils.getDisplayUri(section, VmContext.get(null).getFrameTransformer())); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java new file mode 100644 index 00000000..4befb038 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java @@ -0,0 +1,210 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.Source; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.file.NoSuchFileException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.pkl.core.Release; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.ast.expression.unary.ImportNode; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.module.ModuleKeys; +import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.util.Nullable; + +/** + * Caches modules by the URI originally specified in the importing module, and also by the resolved + * URI from which the module was eventually loaded. Caching by original URI avoids any overhead + * incurred by resolving a module multiple times. Caching by resolved URI avoids any overhead + * incurred by evaluating a module multiple times, and also avoids any inconsistencies caused by + * module contents changing during evaluation. + */ +public final class ModuleCache { + private static final Set STDLIB_MODULE_URIS = + Release.current().standardLibrary().modules().stream() + .map(URI::create) + .collect(Collectors.toSet()); + + public ModuleCache() {} + + public interface ModuleInitializer { + void initialize( + ModuleKey moduleKey, + ResolvedModuleKey resolvedModuleKey, + ModuleResolver moduleResolver, + Source source, + VmTyped emptyModule, + @Nullable Node importNode); + } + + // due to eager initialization of (module) classes, + // loading of module A may be triggered while A is being loaded + // this is why we can't use (Concurrent)Map.computeIfAbsent() for caching, + // (duplicate modules wouldn't work correctly because modules and their classes have identity) + // value type is VmTyped|RuntimeException + private final Map modulesByOriginalUri = new HashMap<>(); + private final Map modulesByResolvedUri = new HashMap<>(); + + @TruffleBoundary + public synchronized VmTyped getOrLoad( + ModuleKey moduleKey, + SecurityManager securityManager, + ModuleResolver moduleResolver, + Supplier moduleInstantiator, + ModuleInitializer moduleInitializer, + @Nullable ImportNode importNode) { + + if (ModuleKeys.isStdLibModule(moduleKey)) { + var moduleName = moduleKey.getUri().getSchemeSpecificPart(); + + // some standard library modules are cached as static singletons + // and hence aren't parsed/initialized anew for every evaluator + switch (moduleName) { + case "base": + // always needed + return BaseModule.getModule(); + case "Benchmark": + return BenchmarkModule.getModule(); + case "jsonnet": + return JsonnetModule.getModule(); + case "math": + return MathModule.getModule(); + case "platform": + return PlatformModule.getModule(); + case "project": + return ProjectModule.getModule(); + case "reflect": + return ReflectModule.getModule(); + case "release": + return ReleaseModule.getModule(); + case "semver": + return SemVerModule.getModule(); + case "settings": + // always needed if ~/.pkl/settings.pkl is present + return SettingsModule.getModule(); + case "test": + return TestModule.getModule(); + case "xml": + return XmlModule.getModule(); + default: + if (!STDLIB_MODULE_URIS.contains(moduleKey.getUri())) { + var stdlibModules = String.join("\n", Release.current().standardLibrary().modules()); + throw new VmExceptionBuilder() + .withOptionalLocation(importNode) + .evalError("cannotFindStdLibModule", moduleName, stdlibModules) + .build(); + } + } + } + + if (!moduleKey.isCached()) { + var resolvedKey = resolve(moduleKey, securityManager, importNode); + return doLoad( + moduleKey, + resolvedKey, + moduleResolver, + moduleInstantiator, + moduleInitializer, + importNode); + } + + var module1 = modulesByOriginalUri.get(moduleKey.getUri()); + if (module1 != null) { + if (module1 instanceof VmTyped) return (VmTyped) module1; + + assert module1 instanceof RuntimeException; + // would be more accurate/safe to throw a clone with adapted Pkl stack trace + throw (RuntimeException) module1; + } + + var resolvedKey = resolve(moduleKey, securityManager, importNode); + var module2 = modulesByResolvedUri.get(resolvedKey.getUri()); + if (module2 != null) { + if (module2 instanceof VmTyped) return (VmTyped) module2; + + assert module2 instanceof RuntimeException; + // would be more accurate/safe to throw a clone with adapted Pkl stack trace + throw (RuntimeException) module2; + } + + return doLoad( + moduleKey, resolvedKey, moduleResolver, moduleInstantiator, moduleInitializer, importNode); + } + + private VmTyped doLoad( + ModuleKey moduleKey, + ResolvedModuleKey resolvedKey, + ModuleResolver moduleResolver, + Supplier moduleInstantiator, + ModuleInitializer moduleInitializer, + @Nullable ImportNode importNode) { + + VmTyped module = moduleInstantiator.get(); + + try { + var result = VmUtils.loadSource(resolvedKey); + + // cache module before initializing it to handle recursive module dependencies (cf. ClassNode) + modulesByOriginalUri.put(moduleKey.getUri(), module); + modulesByResolvedUri.put(resolvedKey.getUri(), module); + + moduleInitializer.initialize( + moduleKey, resolvedKey, moduleResolver, result, module, importNode); + } catch (Exception e) { + // handle error deterministically by caching it and rethrowing it when the module is loaded + // again + // (shouldn't try to load a module multiple times within the scope of an + // Evaluator/ModuleCache) + modulesByOriginalUri.put(moduleKey.getUri(), e); + modulesByResolvedUri.put(resolvedKey.getUri(), e); + throw e; + } + + return module; + } + + private ResolvedModuleKey resolve( + ModuleKey module, SecurityManager securityManager, @Nullable ImportNode importNode) { + try { + return module.resolve(securityManager); + } catch (SecurityManagerException | PackageLoadError e) { + throw new VmExceptionBuilder().withOptionalLocation(importNode).withCause(e).build(); + } catch (FileNotFoundException | NoSuchFileException e) { + throw new VmExceptionBuilder() + .withOptionalLocation(importNode) + .evalError("cannotFindModule", module.getUri()) + .build(); + } catch (IOException e) { + throw new VmExceptionBuilder() + .withOptionalLocation(importNode) + .evalError("ioErrorLoadingModule", module.getUri()) + .withCause(e) + .build(); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleInfo.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleInfo.java new file mode 100644 index 00000000..f452e03e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleInfo.java @@ -0,0 +1,186 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.source.SourceSection; +import java.net.URI; +import java.util.*; +import org.pkl.core.ModuleSchema; +import org.pkl.core.PClass; +import org.pkl.core.TypeAlias; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.ast.expression.unary.ImportGlobNode; +import org.pkl.core.ast.expression.unary.ImportNode; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.module.ResolvedModuleKey; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +public final class ModuleInfo { + private final SourceSection headerSection; + private final SourceSection sourceSection; + private final @Nullable SourceSection docComment; + private final String moduleName; + private final ModuleKey moduleKey; + private final ResolvedModuleKey resolvedModuleKey; + private final boolean isAmend; + + @LateInit private List annotations; + + @LateInit private VmTyped __mirror; + private final Object mirrorLock = new Object(); + + @LateInit private ModuleSchema __moduleSchema; + private final Object moduleSchemaLock = new Object(); + + public ModuleInfo( + SourceSection sourceSection, + SourceSection headerSection, + @Nullable SourceSection docComment, + String moduleName, + ModuleKey moduleKey, + ResolvedModuleKey resolvedModuleKey, + boolean isAmend) { + + this.sourceSection = sourceSection; + this.headerSection = headerSection; + this.docComment = docComment; + this.moduleName = moduleName; + this.moduleKey = moduleKey; + this.resolvedModuleKey = resolvedModuleKey; + this.isAmend = isAmend; + } + + public void initAnnotations(List annotations) { + assert this.annotations == null; + this.annotations = annotations; + } + + public List getAnnotations() { + assert annotations != null; + return annotations; + } + + public SourceSection getSourceSection() { + return sourceSection; + } + + public SourceSection getHeaderSection() { + return headerSection; + } + + public @Nullable SourceSection getDocComment() { + return docComment; + } + + public String getModuleName() { + return moduleName; + } + + public ModuleKey getModuleKey() { + return moduleKey; + } + + public ResolvedModuleKey getResolvedModuleKey() { + return resolvedModuleKey; + } + + public VmTyped getMirror(VmTyped module) { + synchronized (mirrorLock) { + assert (module.getModuleInfo() == this); + + if (__mirror == null) { + __mirror = MirrorFactories.moduleFactory.create(module); + } + return __mirror; + } + } + + public ModuleSchema getModuleSchema(VmTyped module) { + synchronized (moduleSchemaLock) { + assert (module.getModuleInfo() == this); + + if (__moduleSchema == null) { + var parent = module.getParent(); + // every module has a superclass and hence also a parent + assert parent != null; + + ModuleSchema supermodule = null; + if (parent != BaseModule.getModuleClass().getPrototype()) { + supermodule = parent.getModuleInfo().getModuleSchema(parent); + } + + var imports = new LinkedHashMap(); + var classes = new LinkedHashMap(); + var typeAliases = new LinkedHashMap(); + + for (var propertyDef : EconomicMaps.getValues(module.getMembers())) { + if (propertyDef.isImport()) { + MemberNode memberNode = propertyDef.getMemberNode(); + assert memberNode != null; // import is never a constant + var importNode = memberNode.getBodyNode(); + var importUri = + importNode instanceof ImportNode + ? ((ImportNode) importNode).getImportUri() + : ((ImportGlobNode) importNode).getImportUri(); + imports.put(propertyDef.getName().toString(), importUri); + continue; + } + + if (propertyDef.isLocal()) continue; + + if (propertyDef.isClass()) { + var clazz = (VmClass) module.getCachedValue(propertyDef.getName()); + if (clazz == null) { + clazz = (VmClass) propertyDef.getCallTarget().call(module, module); + } + classes.put(clazz.getSimpleName(), clazz.export()); + continue; + } + + if (propertyDef.isTypeAlias()) { + var typeAlias = (VmTypeAlias) module.getCachedValue(propertyDef.getName()); + if (typeAlias == null) { + typeAlias = (VmTypeAlias) propertyDef.getCallTarget().call(module, module); + } + typeAliases.put(typeAlias.getSimpleName(), typeAlias.export()); + } + } + + __moduleSchema = + new ModuleSchema( + moduleKey.getUri(), + moduleName, + isAmend, + supermodule, + module.getVmClass().export(), + VmUtils.exportDocComment(module.getModuleInfo().docComment), + VmUtils.exportAnnotations(module.getModuleInfo().annotations), + classes, + typeAliases, + imports); + } + + return __moduleSchema; + } + } + + /** Tells whether this module amends another module. */ + public boolean isAmend() { + return isAmend; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleResolver.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleResolver.java new file mode 100644 index 00000000..d0029195 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleResolver.java @@ -0,0 +1,94 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.nodes.Node; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Optional; +import org.pkl.core.ModuleSource; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.module.ModuleKeyFactory; +import org.pkl.core.module.ModuleKeys; +import org.pkl.core.util.Nullable; + +public final class ModuleResolver { + private final Collection factories; + + public ModuleResolver(Collection factories) { + this.factories = factories; + } + + public Collection getFactories() { + return factories; + } + + public ModuleKey resolve(ModuleSource moduleSource) { + if (!moduleSource.getUri().isAbsolute()) { + throw new VmExceptionBuilder() + .evalError("cannotEvaluateRelativeModuleUri", moduleSource.getUri()) + .build(); + } + if (moduleSource.getContents() != null) { + // `ModuleSource.text()` creates a synthetic module with URI `repl:text`, so it should be + // matched to `ModuleKeys.synthetic`. + if (moduleSource.getUri().equals(VmUtils.REPL_TEXT_URI)) { + return ModuleKeys.synthetic(moduleSource.getUri(), moduleSource.getContents()); + } + return resolveCached(moduleSource.getUri(), moduleSource.getContents()); + } + return resolve(moduleSource.getUri()); + } + + public ModuleKey resolve(URI moduleUri) { + return resolve(moduleUri, null); + } + + public ModuleKey resolveCached(URI moduleUri, String text) { + var underlyingModuleKey = resolve(moduleUri); + return ModuleKeys.cached(underlyingModuleKey, text); + } + + public ModuleKey resolve(URI moduleUri, @Nullable Node importNode) { + if (!moduleUri.isAbsolute()) { + throw new VmExceptionBuilder() + .withOptionalLocation(importNode) + .bug("Cannot resolve relative URI `%s`.", moduleUri) + .build(); + } + + var normalized = moduleUri.normalize(); + for (var factory : factories) { + Optional key; + try { + key = factory.create(normalized); + } catch (URISyntaxException e) { + throw new VmExceptionBuilder() + .withOptionalLocation(importNode) + .evalError("invalidModuleUri", moduleUri) + .withHint(e.getReason()) + .build(); + } + if (key.isPresent()) return key.get(); + } + + throw new VmExceptionBuilder() + .evalError("noModuleLoaderRegistered", moduleUri) + .withOptionalLocation(importNode) + .build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/NullReceiverException.java b/pkl-core/src/main/java/org/pkl/core/runtime/NullReceiverException.java new file mode 100644 index 00000000..8e0ae492 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/NullReceiverException.java @@ -0,0 +1,26 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.nodes.ControlFlowException; + +public final class NullReceiverException extends ControlFlowException { + public static final NullReceiverException INSTANCE = new NullReceiverException(); + + private NullReceiverException() { + super(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/PlatformModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/PlatformModule.java new file mode 100644 index 00000000..9ccff8b1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/PlatformModule.java @@ -0,0 +1,85 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives; +import java.net.URI; + +public final class PlatformModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:platform"), instance); + } + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getPlatformClass() { + return PlatformClass.instance; + } + + public static VmClass getLanguageClass() { + return LanguageClass.instance; + } + + public static VmClass getRuntimeClass() { + return RuntimeClass.instance; + } + + public static VmClass getVirtualMachineClass() { + return VirtualMachineClass.instance; + } + + public static VmClass getOperatingSystemClass() { + return OperatingSystemClass.instance; + } + + public static VmClass getProcessorClass() { + return ProcessorClass.instance; + } + + private static final class PlatformClass { + static final VmClass instance = loadClass("Platform"); + } + + private static final class LanguageClass { + static final VmClass instance = loadClass("Language"); + } + + private static final class RuntimeClass { + static final VmClass instance = loadClass("Runtime"); + } + + private static final class VirtualMachineClass { + static final VmClass instance = loadClass("VirtualMachine"); + } + + private static final class OperatingSystemClass { + static final VmClass instance = loadClass("OperatingSystem"); + } + + private static final class ProcessorClass { + static final VmClass instance = loadClass("Processor"); + } + + @CompilerDirectives.TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ProjectModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/ProjectModule.java new file mode 100644 index 00000000..b924196d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ProjectModule.java @@ -0,0 +1,32 @@ +/** + * 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.runtime; + +import static org.pkl.core.PClassInfo.pklProjectUri; + +public class ProjectModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(pklProjectUri, instance); + } + + private ProjectModule() {} + + public static VmTyped getModule() { + return instance; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ReaderBase.java b/pkl-core/src/main/java/org/pkl/core/runtime/ReaderBase.java new file mode 100644 index 00000000..d5b2e5de --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ReaderBase.java @@ -0,0 +1,76 @@ +/** + * 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.runtime; + +import java.io.IOException; +import java.net.URI; +import java.util.List; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.module.PathElement; +import org.pkl.core.util.IoUtils; + +public interface ReaderBase { + /** + * Tells if the URIs represented by this module key or resource reader should be interpreted as hierarchical. + */ + boolean hasHierarchicalUris(); + + /** Tells if this module key or resource reader supports globbing. */ + boolean isGlobbable(); + + /** + * Tells if relative paths of this URI should be resolved from {@link URI#getFragment()}, rather + * than {@link URI#getPath()}. + */ + default boolean hasFragmentPaths() { + return false; + } + + /** + * Tells if this module key or resource reader has an element at {@code elementUri}. + * + *

This method only needs to be implemented if {@link #hasHierarchicalUris()} returns true, and + * if either {@link #isGlobbable()} or {@link ModuleKey#isLocal()} returns true. + */ + default boolean hasElement(SecurityManager securityManager, URI elementUri) + throws IOException, SecurityManagerException { + throw new UnsupportedOperationException(); + } + + /** + * List elements within a base URI. + * + *

This method is called by the {@link org.pkl.core.util.GlobResolver} when resolving glob + * expressions if {@link #isGlobbable()} returns true. + * + *

This method does not need to be implemented if {@link #isGlobbable()} returns false. + * + *

If {@link #hasHierarchicalUris()} returns false, {@code URI} is effectively an empty URI and + * should be ignored. In this case, this method is expected to list all elements represented by + * this reader. + */ + default List listElements(SecurityManager securityManager, URI baseUri) + throws IOException, SecurityManagerException { + throw new UnsupportedOperationException(); + } + + default URI resolveUri(URI baseUri, URI uri) throws IOException, SecurityManagerException { + return IoUtils.resolve(this, baseUri, uri); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ReflectModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/ReflectModule.java new file mode 100644 index 00000000..6f09a582 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ReflectModule.java @@ -0,0 +1,180 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.net.URI; + +public final class ReflectModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + // Forcing of `instance` inside `loadModule()` results in calls to methods such as + // `ReflectModule.getNullableTypeClass()` via `MirrorFactories`. + // By calling loadModule() in the outer class's static initializer (rather than having another + // nested class for this), + // initialization loops (e.g., during execution of native-image) are avoided. + loadModule(URI.create("pkl:reflect"), instance); + } + + private ReflectModule() {} + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getModuleClass() { + return ModuleClass.instance; + } + + public static VmClass getClassClass() { + return ClassClass.instance; + } + + public static VmClass getTypeAliasClass() { + return TypeAliasClass.instance; + } + + public static VmClass getPropertyClass() { + return PropertyClass.instance; + } + + public static VmClass getMethodClass() { + return MethodClass.instance; + } + + public static VmClass getMethodParameterClass() { + return MethodParameterClass.instance; + } + + public static VmClass getTypeParameterClass() { + return TypeParameterClass.instance; + } + + public static VmClass getDeclaredTypeClass() { + return DeclaredTypeClass.instance; + } + + public static VmClass getStringLiteralTypeClass() { + return StringLiteralTypeClass.instance; + } + + public static VmClass getUnionTypeClass() { + return UnionTypeClass.instance; + } + + public static VmClass getNullableTypeClass() { + return NullableTypeClass.instance; + } + + public static VmClass getModuleTypeClass() { + return ModuleTypeClass.instance; + } + + public static VmClass getFunctionTypeClass() { + return FunctionTypeClass.instance; + } + + public static VmClass getUnknownTypeClass() { + return UnknownTypeClass.instance; + } + + public static VmClass getNothingTypeClass() { + return NothingTypeClass.instance; + } + + public static VmClass getTypeVariableClass() { + return TypeVariableClass.instance; + } + + public static VmClass getSourceLocationClass() { + return SourceLocationClass.instance; + } + + private static final class ModuleClass { + static final VmClass instance = loadClass("Module"); + } + + private static final class ClassClass { + static final VmClass instance = loadClass("Class"); + } + + private static final class TypeAliasClass { + static final VmClass instance = loadClass("TypeAlias"); + } + + private static final class PropertyClass { + static final VmClass instance = loadClass("Property"); + } + + private static final class MethodClass { + static final VmClass instance = loadClass("Method"); + } + + private static final class MethodParameterClass { + static final VmClass instance = loadClass("MethodParameter"); + } + + private static final class TypeParameterClass { + static final VmClass instance = loadClass("TypeParameter"); + } + + private static final class DeclaredTypeClass { + static final VmClass instance = loadClass("DeclaredType"); + } + + private static final class StringLiteralTypeClass { + static final VmClass instance = loadClass("StringLiteralType"); + } + + private static final class UnionTypeClass { + static final VmClass instance = loadClass("UnionType"); + } + + private static final class NullableTypeClass { + static final VmClass instance = loadClass("NullableType"); + } + + private static final class ModuleTypeClass { + static final VmClass instance = loadClass("ModuleType"); + } + + private static final class FunctionTypeClass { + static final VmClass instance = loadClass("FunctionType"); + } + + private static final class UnknownTypeClass { + static final VmClass instance = loadClass("UnknownType"); + } + + private static final class NothingTypeClass { + static final VmClass instance = loadClass("NothingType"); + } + + private static final class TypeVariableClass { + static final VmClass instance = loadClass("TypeVariable"); + } + + private static final class SourceLocationClass { + static final VmClass instance = loadClass("SourceLocation"); + } + + @TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ReleaseModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/ReleaseModule.java new file mode 100644 index 00000000..392648b4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ReleaseModule.java @@ -0,0 +1,69 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives; +import java.net.URI; + +public final class ReleaseModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:release"), instance); + } + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getReleaseClass() { + return ReleaseModule.ReleaseClass.instance; + } + + public static VmClass getSourceCodeClass() { + return ReleaseModule.SourceCodeClass.instance; + } + + public static VmClass getDocumentationClass() { + return ReleaseModule.DocumentationClass.instance; + } + + public static VmClass getStandardLibraryClass() { + return ReleaseModule.StandardLibraryClass.instance; + } + + private static final class ReleaseClass { + static final VmClass instance = loadClass("Release"); + } + + private static final class SourceCodeClass { + static final VmClass instance = loadClass("SourceCode"); + } + + private static final class DocumentationClass { + static final VmClass instance = loadClass("Documentation"); + } + + private static final class StandardLibraryClass { + static final VmClass instance = loadClass("StandardLibrary"); + } + + @CompilerDirectives.TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java new file mode 100644 index 00000000..8709b502 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java @@ -0,0 +1,172 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.nodes.Node; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.pkl.core.SecurityManager; +import org.pkl.core.SecurityManagerException; +import org.pkl.core.module.ModuleKey; +import org.pkl.core.packages.PackageLoadError; +import org.pkl.core.resource.Resource; +import org.pkl.core.resource.ResourceReader; +import org.pkl.core.stdlib.VmObjectFactory; +import org.pkl.core.util.GlobResolver; +import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; +import org.pkl.core.util.GlobResolver.ResolvedGlobElement; + +public final class ResourceManager { + private final Map resourceReaders = new HashMap<>(); + private final SecurityManager securityManager; + private final VmObjectFactory resourceFactory; + + // cache resources indefinitely to make resource reads deterministic + private final Map> resources = new HashMap<>(); + + private final Map> globExpressions = new HashMap<>(); + + public ResourceManager(SecurityManager securityManager, Collection readers) { + this.securityManager = securityManager; + + for (var reader : readers) { + resourceReaders.put(reader.getUriScheme(), reader); + } + + resourceFactory = + new VmObjectFactory(BaseModule::getResourceClass) + .addProperty("uri", resource -> resource.getUri().toString()) + .addProperty("text", Resource::getText) + .addProperty("base64", Resource::getBase64); + } + + /** + * Resolves the glob URI into a set of URIs. + * + *

The glob URI must be absolute. For example: {@code "file:///foo/bar/*.pkl"}. + */ + @TruffleBoundary + public List resolveGlob( + URI globUri, + URI enclosingUri, + ModuleKey enclosingModuleKey, + Node readNode, + String globExpression) { + return globExpressions.computeIfAbsent( + globUri.normalize(), + uri -> { + var scheme = uri.getScheme(); + URI resolvedUri; + try { + resolvedUri = enclosingModuleKey.resolveUri(globUri); + } catch (SecurityManagerException | IOException e) { + throw new VmExceptionBuilder().withLocation(readNode).withCause(e).build(); + } + try { + var reader = resourceReaders.get(resolvedUri.getScheme()); + if (reader == null) { + throw new VmExceptionBuilder() + .withLocation(readNode) + .evalError("noResourceReaderRegistered", scheme) + .build(); + } + if (!reader.isGlobbable()) { + throw new VmExceptionBuilder() + .evalError("cannotGlobUri", uri, scheme) + .withLocation(readNode) + .build(); + } + var securityManager = VmContext.get(readNode).getSecurityManager(); + return GlobResolver.resolveGlob( + securityManager, reader, enclosingModuleKey, enclosingUri, globExpression); + } catch (InvalidGlobPatternException e) { + throw new VmExceptionBuilder() + .evalError("invalidGlobPattern", globExpression) + .withHint(e.getMessage()) + .withLocation(readNode) + .build(); + } catch (SecurityManagerException e) { + throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build(); + } catch (IOException e) { + throw new VmExceptionBuilder() + .evalError("ioErrorResolvingGlob", globExpression) + .withCause(e) + .withLocation(readNode) + .build(); + } + }); + } + + @TruffleBoundary + public Optional read(URI resourceUri, Node readNode) { + return resources.computeIfAbsent( + resourceUri.normalize(), + uri -> { + try { + securityManager.checkReadResource(uri); + } catch (SecurityManagerException e) { + throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build(); + } + + var reader = resourceReaders.get(uri.getScheme()); + if (reader == null) { + throw new VmExceptionBuilder() + .withLocation(readNode) + .evalError("noResourceReaderRegistered", resourceUri.getScheme()) + .build(); + } + + Optional resource; + try { + resource = reader.read(uri); + } catch (IOException e) { + throw new VmExceptionBuilder() + .evalError("ioErrorReadingResource", uri) + .withCause(e) + .withLocation(readNode) + .build(); + } catch (URISyntaxException e) { + throw new VmExceptionBuilder() + .evalError("invalidResourceUri", resourceUri) + .withHint(e.getReason()) + .withLocation(readNode) + .build(); + } catch (SecurityManagerException | PackageLoadError e) { + throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build(); + } + if (resource.isEmpty()) return resource; + + var res = resource.get(); + if (res instanceof String) return resource; + + if (res instanceof Resource) { + return Optional.of(resourceFactory.create((Resource) res)); + } + + throw new VmExceptionBuilder() + .evalError("unsupportedResourceType", reader.getClass().getName(), res.getClass()) + .withLocation(readNode) + .build(); + }); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/SemVerModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/SemVerModule.java new file mode 100644 index 00000000..dd229e36 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/SemVerModule.java @@ -0,0 +1,48 @@ +/** + * 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.runtime; + +import static org.pkl.core.PClassInfo.pklSemverUri; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; + +public final class SemVerModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(pklSemverUri, instance); + } + + private SemVerModule() {} + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getVersionClass() { + return VersionClass.instance; + } + + private static final class VersionClass { + static final VmClass instance = loadClass("Version"); + } + + @TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/SettingsModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/SettingsModule.java new file mode 100644 index 00000000..b49cae01 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/SettingsModule.java @@ -0,0 +1,32 @@ +/** + * 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.runtime; + +import java.net.URI; + +public final class SettingsModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:settings"), instance); + } + + private SettingsModule() {} + + public static VmTyped getModule() { + return instance; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceGenerator.java b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceGenerator.java new file mode 100644 index 00000000..a4879ad7 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceGenerator.java @@ -0,0 +1,103 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.TruffleStackTrace; +import com.oracle.truffle.api.TruffleStackTraceElement; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; +import java.util.*; +import org.pkl.core.StackFrame; +import org.pkl.core.ast.MemberNode; +import org.pkl.core.util.Nullable; + +class StackTraceGenerator { + private final VmException exception; + + private final List frames = new ArrayList<>(); + + static List capture(VmException exception) { + return new StackTraceGenerator(exception).capture(); + } + + private StackTraceGenerator(VmException exception) { + this.exception = exception; + } + + private List capture() { + var truffleElements = TruffleStackTrace.getStackTrace(exception); + if (truffleElements.isEmpty()) { + addFrame(exception.getSourceSection(), exception.getMemberName()); + } else { + var isFirst = true; // copy before mutating to be on the safe side + var insertedStackFrames = new HashMap<>(exception.getInsertedStackFrames()); + for (var element : truffleElements) { + var callNode = element.getLocation(); + addFrame(findDisplayableSourceSection(callNode, isFirst), getMemberName(element)); + isFirst = false; + + var callTarget = element.getTarget(); + var insertedFrame = insertedStackFrames.remove(callTarget); + if (insertedFrame != null) frames.add(insertedFrame); + } + } + + return frames; + } + + // customization of Node.getEncapsulatingSourceSection() + private SourceSection findDisplayableSourceSection(@Nullable Node callNode, boolean isFirst) { + if (isFirst && exception.getSourceSection() != null) { + return exception.getSourceSection(); + } + + for (Node current = callNode; current != null; current = current.getParent()) { + if (current.getSourceSection() != null) { + return current instanceof MemberNode + // Always display the member body's source section instead of the member + // (root) node's source section (which includes doc comment etc.), even + // if `callNode` is a child of root node rather than body node. + // This improves stack trace output for failed property type checks. + ? ((MemberNode) current).getBodySection() + : current.getSourceSection(); + } + } + + return VmUtils.unavailableSourceSection(); + } + + private void addFrame(@Nullable SourceSection section, @Nullable String memberName) { + if (section == null || !section.isAvailable()) { + // no point in displaying this frame. + // a legitimate case where we end up here is a default property + // value failing its property type check (e.g. List(!isEmpty)). + // in that case, unless we want to display pseudo code for the implicit + // default value, there is nothing better than skipping the frame. + return; + } + + frames.add(VmUtils.createStackFrame(section, memberName)); + } + + private @Nullable String getMemberName(@Nullable TruffleStackTraceElement element) { + if (element == null) return null; + + var rootNode = element.getTarget().getRootNode(); + if (rootNode == null) return null; + + return rootNode.getName(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java new file mode 100644 index 00000000..422cc378 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java @@ -0,0 +1,215 @@ +/** + * 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.runtime; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import org.pkl.core.StackFrame; +import org.pkl.core.util.Nullable; + +public class StackTraceRenderer { + private final Function frameTransformer; + + public StackTraceRenderer(Function frameTransformer) { + this.frameTransformer = frameTransformer; + } + + public void render(List frames, @Nullable String hint, StringBuilder builder) { + var compressed = compressFrames(frames); + doRender(compressed, hint, builder, "", true); + } + + // non-private for testing + void doRender( + List frames, + @Nullable String hint, + StringBuilder builder, + String leftMargin, + boolean isFirstElement) { + for (var frame : frames) { + if (frame instanceof StackFrameLoop) { + var loop = (StackFrameLoop) frame; + // ensure a cycle of length 1 doesn't get rendered as a loop + if (loop.count == 1) { + doRender(loop.frames, null, builder, leftMargin, isFirstElement); + } else { + if (!isFirstElement) { + builder.append(leftMargin).append("\n"); + } + builder.append(leftMargin).append("┌─ ").append(loop.count).append(" repetitions of:\n"); + var newLeftMargin = leftMargin + "│ "; + doRender(loop.frames, null, builder, newLeftMargin, isFirstElement); + if (isFirstElement) { + renderHint(hint, builder, newLeftMargin); + isFirstElement = false; + } + builder.append(leftMargin).append("└─\n"); + } + } else { + if (!isFirstElement) { + builder.append(leftMargin).append('\n'); + } + renderFrame((StackFrame) frame, builder, leftMargin); + } + + if (isFirstElement) { + renderHint(hint, builder, leftMargin); + isFirstElement = false; + } + } + } + + private void renderFrame(StackFrame frame, StringBuilder builder, String leftMargin) { + var transformed = frameTransformer.apply(frame); + renderSourceLine(transformed, builder, leftMargin); + renderSourceLocation(transformed, builder, leftMargin); + } + + private void renderHint(@Nullable String hint, StringBuilder builder, String leftMargin) { + if (hint == null || hint.isEmpty()) return; + + builder.append('\n'); + builder.append(leftMargin); + builder.append(hint); + builder.append('\n'); + } + + private void renderSourceLine(StackFrame frame, StringBuilder builder, String leftMargin) { + var originalSourceLine = frame.getSourceLines().get(0); + var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine); + var sourceLine = originalSourceLine.strip(); + var startColumn = frame.getStartColumn() - leadingWhitespace; + var endColumn = + frame.getStartLine() == frame.getEndLine() + ? frame.getEndColumn() - leadingWhitespace + : sourceLine.length(); + + var prefix = frame.getStartLine() + " | "; + builder.append(leftMargin).append(prefix).append(sourceLine).append('\n'); + builder.append(leftMargin); + //noinspection StringRepeatCanBeUsed + for (int i = 1; i < prefix.length() + startColumn; i++) { + builder.append(' '); + } + //noinspection StringRepeatCanBeUsed + for (int i = startColumn; i <= endColumn; i++) { + builder.append('^'); + } + builder.append('\n'); + } + + private void renderSourceLocation(StackFrame frame, StringBuilder builder, String leftMargin) { + builder.append(leftMargin).append("at "); + if (frame.getMemberName() != null) { + builder.append(frame.getMemberName()); + } else { + builder.append(""); + } + builder.append(" (").append(frame.getModuleUri()).append(')').append('\n'); + } + + /** + * `StackFrame` and `StackFrameLoop` don't currently have a common base interface because the + * former is public API and the latter isn't. + */ + // non-private for testing + static class StackFrameLoop { + final List frames; + final int count; + + StackFrameLoop(List frames, int count) { + this.count = count; + this.frames = frames; + } + } + + // non-private for testing + static List compressFrames(List frames) { + return doCompressFrames(frames, new int[frames.size()], new ArrayList<>(), 0, frames.size()); + } + + private static List doCompressFrames( + List frames, int[] lpps, List result, int beginning, int ending) { + // Algorithm was written with reversed `frames` in mind. + // Instead of reversing `frames`, we invert indices passed to `frames.get()`. + var framesLastIndex = frames.size() - 1; + + var totalSize = ending - beginning; + + var maxLength = -1; + var patternStart = -2; + var patternWidth = -2; + var matchEnd = -2; + + loopSearch: + for (int i = beginning; i < ending; i++) { + var best = i; + var len = 0; + lpps[i] = 0; + + var j = i + 1; + while (j < ending) { + var frame1 = frames.get(framesLastIndex - j); + var frame2 = frames.get(framesLastIndex - (len + i)); + var match = frame1.equals(frame2); + if (!match && len != 0) { + len = lpps[len - 1]; + } else { + len += match ? 1 : 0; + lpps[j] = len; + if (len > lpps[best]) { + best = j; + } else if (len > 0 && len == lpps[j - 1]) { + // Degenerative; e.g. ABAAB; we don't care for "prefixes that are suffixes" for a + // non-empty infix. In other words, we only look for regex `P+P` and not `P+IP` + continue loopSearch; + } + j++; + } + } + + var length = best - i + 1; + if (length > 1 && maxLength < length) { + maxLength = length; + matchEnd = best; + patternStart = i; + } + if (maxLength > ending - i || maxLength * 2 > totalSize) { + // There are no better matches to be found. + break; + } + } + + // Write to result in reverse order. + if (maxLength > 1) { + patternWidth = matchEnd - lpps[matchEnd] - patternStart + 1; + doCompressFrames(frames, lpps, result, matchEnd + 1, ending); + result.add( + new StackFrameLoop( + doCompressFrames( + frames, lpps, new ArrayList<>(), patternStart, patternStart + patternWidth), + maxLength / patternWidth)); + doCompressFrames(frames, lpps, result, beginning, patternStart); + } else { + // No patterns found in frames[beginning...ending]. + for (int i = ending - 1; i >= beginning; i--) { + result.add(frames.get(framesLastIndex - i)); + } + } + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/StdLibModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/StdLibModule.java new file mode 100644 index 00000000..7c69c545 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/StdLibModule.java @@ -0,0 +1,68 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.net.URI; +import java.util.List; +import java.util.Map; +import org.pkl.core.Loggers; +import org.pkl.core.SecurityManagers; +import org.pkl.core.StackFrameTransformers; +import org.pkl.core.module.ModuleKeyFactories; +import org.pkl.core.module.ModuleKeys; +import org.pkl.core.module.ResolvedModuleKey; + +public abstract class StdLibModule { + @TruffleBoundary + protected static void loadModule(URI uri, VmTyped instance) { + // evaluate eagerly to increase thread safety + // (stdlib module objects are statically shared singletons when running on JVM) + // and ensure compile-time evaluation in AOT mode + VmUtils.createContext( + () -> { + var vmContext = VmContext.get(null); + vmContext.initialize( + new VmContext.Holder( + StackFrameTransformers.defaultTransformer, + SecurityManagers.defaultManager, + new ModuleResolver(List.of(ModuleKeyFactories.standardLibrary)), + new ResourceManager(SecurityManagers.defaultManager, List.of()), + Loggers.noop(), + Map.of(), + Map.of(), + null, + null, + null, + null)); + var language = VmLanguage.get(null); + var moduleKey = ModuleKeys.standardLibrary(uri); + var source = VmUtils.loadSource((ResolvedModuleKey) moduleKey); + language.initializeModule( + moduleKey, + (ResolvedModuleKey) moduleKey, + vmContext.getModuleResolver(), + source, + instance, + null); + // evaluate eagerly to increase thread safety + // (stdlib module objects are statically shared singletons when running on JVM) + // and ensure compile-time evaluation in AOT mode + instance.force(false, true); + }) + .close(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestModule.java new file mode 100644 index 00000000..047f5975 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestModule.java @@ -0,0 +1,32 @@ +/** + * 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.runtime; + +import java.net.URI; + +public final class TestModule extends StdLibModule { + static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:test"), instance); + } + + private TestModule() {} + + public static VmTyped getModule() { + return instance; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestResults.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestResults.java new file mode 100644 index 00000000..9b4c149d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestResults.java @@ -0,0 +1,231 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.source.SourceSection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.pkl.core.PklException; + +/** Aggregate test results for a module. Used to verify test failures and generate reports. */ +public final class TestResults { + + private final String module; + private final String displayUri; + private final List results = new ArrayList<>(); + private String err = ""; + + public TestResults(String module, String displayUri) { + this.module = module; + this.displayUri = displayUri; + } + + public String getModuleName() { + return module; + } + + public String getDisplayUri() { + return displayUri; + } + + public List getResults() { + return Collections.unmodifiableList(results); + } + + public TestResult newResult(String name) { + var result = new TestResult(name); + results.add(result); + return result; + } + + public void newResult(String name, Failure failure) { + var result = new TestResult(name); + result.addFailure(failure); + results.add(result); + } + + public int totalTests() { + return results.size(); + } + + public int totalFailures() { + int total = 0; + for (var res : results) { + total += res.getFailures().size(); + } + return total; + } + + public boolean failed() { + for (var res : results) { + if (res.isFailure()) return true; + } + return false; + } + + public String getErr() { + return err; + } + + public void setErr(String err) { + this.err = err; + } + + public static class TestResult { + + private final String name; + private final List failures = new ArrayList<>(); + private final List errors = new ArrayList<>(); + private boolean isExampleWritten = false; + + public TestResult(String name) { + this.name = name; + } + + public boolean isSuccess() { + return failures.isEmpty() && errors.isEmpty(); + } + + boolean isFailure() { + return !isSuccess(); + } + + public String getName() { + return name; + } + + public boolean isExampleWritten() { + return isExampleWritten; + } + + public void setExampleWritten(boolean exampleWritten) { + isExampleWritten = exampleWritten; + } + + public List getFailures() { + return Collections.unmodifiableList(failures); + } + + public void addFailure(Failure description) { + failures.add(description); + } + + public List getErrors() { + return Collections.unmodifiableList(errors); + } + + public void addError(Error err) { + errors.add(err); + } + } + + public static class Failure { + + private final String kind; + private final String rendered; + + private Failure(String kind, String rendered) { + this.kind = kind; + this.rendered = rendered; + } + + public String getKind() { + return kind; + } + + public String getRendered() { + return rendered; + } + + public static Failure buildFactFailure(SourceSection sourceSection, String description) { + return new Failure( + "Fact Failure", sourceSection.getCharacters() + " ❌ (" + description + ")"); + } + + public static Failure buildExampleLengthMismatchFailure( + String location, String property, int expectedLength, int actualLength) { + String builder = + "(" + + location + + ")\n" + + "Output mismatch: Expected \"" + + property + + "\" to contain " + + expectedLength + + " examples, but found " + + actualLength; + return new Failure("Output Mismatch (Length)", builder); + } + + public static Failure buildExamplePropertyMismatchFailure( + String location, String property, boolean isMissingInExpected) { + var builder = new StringBuilder(); + builder + .append("(") + .append(location) + .append(")\n") + .append("Output mismatch: \"") + .append(property); + if (isMissingInExpected) { + builder.append("\" exists in actual but not in expected output"); + } else { + builder.append("\" exists in expected but not in actual output"); + } + return new Failure("Output Mismatch", builder.toString()); + } + + public static Failure buildExampleFailure( + String location, + String expectedLocation, + String expectedValue, + String actualLocation, + String actualValue) { + String builder = + "(" + + location + + ")\n" + + "Expected: (" + + expectedLocation + + ")\n" + + expectedValue + + "\nActual: (" + + actualLocation + + ")\n" + + actualValue; + return new Failure("Example Failure", builder); + } + } + + public static class Error { + + private final String message; + private final PklException exception; + + public Error(String message, PklException exception) { + this.message = message; + this.exception = exception; + } + + public String getMessage() { + return message; + } + + public Exception getException() { + return exception; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java new file mode 100644 index 00000000..9d07393d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java @@ -0,0 +1,315 @@ +/** + * 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.runtime; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.pkl.core.BufferedLogger; +import org.pkl.core.StackFrameTransformer; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.module.ModuleKeys; +import org.pkl.core.runtime.TestResults.Error; +import org.pkl.core.runtime.TestResults.Failure; +import org.pkl.core.stdlib.PklConverter; +import org.pkl.core.stdlib.base.PcfRenderer; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.MutableBoolean; +import org.pkl.core.util.MutableReference; + +/** Runs test results examples and facts. */ +public class TestRunner { + private static final PklConverter converter = new PklConverter(VmMapping.empty()); + private final boolean overwrite; + private final StackFrameTransformer stackFrameTransformer; + private final BufferedLogger logger; + + public TestRunner( + BufferedLogger logger, StackFrameTransformer stackFrameTransformer, boolean overwrite) { + this.logger = logger; + this.stackFrameTransformer = stackFrameTransformer; + this.overwrite = overwrite; + } + + public TestResults run(VmTyped testModule) { + var info = VmUtils.getModuleInfo(testModule); + var results = new TestResults(info.getModuleName(), getDisplayUri(info)); + + try { + checkAmendsPklTest(testModule); + runFacts(testModule, results); + runExamples(testModule, info, results); + } catch (VmException v) { + var meta = results.newResult(info.getModuleName()); + meta.addError(new Error(v.getMessage(), v.toPklException(stackFrameTransformer))); + } + results.setErr(logger.getLogs()); + return results; + } + + private void checkAmendsPklTest(VmTyped value) { + var testModuleClass = TestModule.getModule().getVmClass(); + var moduleClass = value.getVmClass(); + while (moduleClass != testModuleClass) { + moduleClass = moduleClass.getSuperclass(); + if (moduleClass == null) { + throw new VmExceptionBuilder().typeMismatch(value, testModuleClass).build(); + } + } + } + + private void runFacts(VmTyped testModule, TestResults results) { + var facts = VmUtils.readMember(testModule, Identifier.FACTS); + if (facts instanceof VmNull) return; + + var factsMapping = (VmMapping) facts; + factsMapping.forceAndIterateMemberValues( + (groupKey, groupMember, groupValue) -> { + var result = results.newResult(String.valueOf(groupKey)); + var groupListing = (VmListing) groupValue; + groupListing.forceAndIterateMemberValues( + ((factIndex, factMember, factValue) -> { + assert factValue instanceof Boolean; + if (factValue == Boolean.FALSE) { + result.addFailure( + Failure.buildFactFailure( + factMember.getSourceSection(), getDisplayUri(factMember))); + } + return true; + })); + return true; + }); + } + + private void runExamples(VmTyped testModule, ModuleInfo info, TestResults results) { + var examples = VmUtils.readMember(testModule, Identifier.EXAMPLES); + if (examples instanceof VmNull) return; + + var moduleUri = info.getModuleKey().getUri(); + if (!moduleUri.getScheme().equalsIgnoreCase("file")) { + throw new VmExceptionBuilder() + .evalError("cannotEvaluateNonFileBasedTestModule", moduleUri) + .build(); + } + + var examplesMapping = (VmMapping) examples; + var moduleFile = Path.of(moduleUri); + var expectedOutputFile = moduleFile.resolveSibling(moduleFile.getFileName() + "-expected.pcf"); + var actualOutputFile = moduleFile.resolveSibling(moduleFile.getFileName() + "-actual.pcf"); + + try { + Files.deleteIfExists(actualOutputFile); + } catch (IOException e) { + throw new VmExceptionBuilder() + .evalError("ioErrorWritingTestOutputFile", actualOutputFile) + .withCause(e) + .build(); + } + try { + if (overwrite) { + Files.deleteIfExists(expectedOutputFile); + } + } catch (IOException e) { + throw new VmExceptionBuilder() + .evalError("ioErrorWritingTestOutputFile", expectedOutputFile) + .withCause(e) + .build(); + } + + if (Files.exists(expectedOutputFile)) { + doRunAndValidateExamples(examplesMapping, expectedOutputFile, actualOutputFile, results); + } else { + doRunAndWriteExamples(examplesMapping, expectedOutputFile, results); + } + } + + private void doRunAndValidateExamples( + VmMapping examples, Path expectedOutputFile, Path actualOutputFile, TestResults results) { + var expectedExampleOutputs = loadExampleOutputs(expectedOutputFile); + var actualExampleOutputs = new MutableReference(null); + var allGroupsSucceeded = new MutableBoolean(true); + examples.forceAndIterateMemberValues( + (groupKey, groupMember, groupValue) -> { + var testName = String.valueOf(groupKey); + var group = (VmListing) groupValue; + var expectedGroup = + (VmDynamic) VmUtils.readMemberOrNull(expectedExampleOutputs, groupKey); + + if (expectedGroup == null) { + results.newResult( + testName, + Failure.buildExamplePropertyMismatchFailure( + getDisplayUri(groupMember), String.valueOf(groupKey), true)); + return true; + } + + if (group.getLength() != expectedGroup.getLength()) { + results.newResult( + testName, + Failure.buildExampleLengthMismatchFailure( + getDisplayUri(groupMember), + String.valueOf(groupKey), + expectedGroup.getLength(), + group.getLength())); + return true; + } + + var groupSucceeded = new MutableBoolean(true); + group.forceAndIterateMemberValues( + ((exampleIndex, exampleMember, exampleValue) -> { + var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex); + + var exampleValuePcf = renderAsPcf(exampleValue); + var expectedValuePcf = renderAsPcf(expectedValue); + + if (!(exampleValuePcf.equals(expectedValuePcf))) { + if (actualExampleOutputs.isNull()) { + // immediately write and load `-actual.pcf` + // so that we can generate deep link with correct line number for each + // mismatch + writeExampleOutputs(actualOutputFile, examples); + actualExampleOutputs.set(loadExampleOutputs(actualOutputFile)); + } + + groupSucceeded.set(false); + + var expectedMember = VmUtils.findMember(expectedGroup, exampleIndex); + assert expectedMember != null; + + var actualGroup = + (VmObjectLike) VmUtils.readMemberOrNull(actualExampleOutputs.get(), groupKey); + var actualMember = + actualGroup == null ? null : VmUtils.findMember(actualGroup, exampleIndex); + if (actualMember == null) { + // file was written earlier in this method; + // must have been tampered with by another process + throw new VmExceptionBuilder() + .evalError("invalidOutputFileStructure", actualOutputFile) + .build(); + } + + results.newResult( + testName, + Failure.buildExampleFailure( + getDisplayUri(exampleMember), + getDisplayUri(expectedMember), + expectedValuePcf, + getDisplayUri(actualMember), + exampleValuePcf)); + } + + return true; + })); + + if (groupSucceeded.get()) { + results.newResult(testName); + } else { + allGroupsSucceeded.set(false); + } + + return true; + }); + + expectedExampleOutputs.iterateMembers( + (groupKey, groupMember) -> { + if (groupMember.isLocalOrExternalOrHidden()) { + return true; + } + if (examples.getCachedValue(groupKey) == null) { + allGroupsSucceeded.set(false); + results.newResult( + String.valueOf(groupKey), + Failure.buildExamplePropertyMismatchFailure( + getDisplayUri(groupMember), String.valueOf(groupKey), false)); + } + return true; + }); + + if (!allGroupsSucceeded.get() && actualExampleOutputs.isNull()) { + writeExampleOutputs(actualOutputFile, examples); + } + } + + private void doRunAndWriteExamples(VmMapping examples, Path outputFile, TestResults results) { + examples.forceAndIterateMemberValues( + (groupKey, groupMember, groupValue) -> { + results.newResult(String.valueOf(groupKey)).setExampleWritten(true); + return true; + }); + writeExampleOutputs(outputFile, examples); + } + + private void writeExampleOutputs(Path outputFile, VmMapping examples) { + var outputFileContent = + new VmDynamic( + VmUtils.createEmptyMaterializedFrame(), + BaseModule.getDynamicClass().getPrototype(), + EconomicMaps.of( + Identifier.EXAMPLES, + VmUtils.createSyntheticObjectProperty(Identifier.EXAMPLES, "examples", examples)), + 0); + var builder = new StringBuilder(); + new PcfRenderer(builder, " ", converter, false, false).renderDocument(outputFileContent); + try { + Files.writeString(outputFile, builder); + } catch (IOException e) { + throw new VmExceptionBuilder() + .evalError("ioErrorWritingTestOutputFile", outputFile) + .withCause(e) + .build(); + } + } + + private VmDynamic loadExampleOutputs(Path outputFile) { + // load file manually to prevent it from being cached (won't need it again) + String fileContent; + try { + fileContent = Files.readString(outputFile, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new VmExceptionBuilder() + .evalError("ioErrorReadingTestOutputFile", outputFile) + .withCause(e) + .build(); + } + var module = + VmLanguage.get(null).loadModule(ModuleKeys.synthetic(outputFile.toUri(), fileContent)); + var examples = (VmDynamic) VmUtils.readMemberOrNull(module, Identifier.EXAMPLES); + if (examples == null) { + throw new VmExceptionBuilder().evalError("invalidOutputFileStructure", outputFile).build(); + } + return examples; + } + + private static String renderAsPcf(Object pklValue) { + var builder = new StringBuilder(); + new PcfRenderer(builder, " ", converter, false, false).renderValue(pklValue); + if (pklValue instanceof VmObject) { + builder.insert(0, "new "); + } + return builder.toString(); + } + + private static String getDisplayUri(ObjectMember member) { + return VmUtils.getDisplayUri( + member.getSourceSection(), VmContext.get(null).getFrameTransformer()); + } + + private static String getDisplayUri(ModuleInfo moduleInfo) { + return VmUtils.getDisplayUri( + moduleInfo.getModuleKey().getUri(), VmContext.get(null).getFrameTransformer()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmBugException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmBugException.java new file mode 100644 index 00000000..f3e2f743 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmBugException.java @@ -0,0 +1,62 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.pkl.core.*; +import org.pkl.core.util.Nullable; + +public final class VmBugException extends VmException { + public VmBugException( + String message, + @Nullable Throwable cause, + boolean isExternalMessage, + Object[] messageArguments, + List programValues, + @Nullable Node location, + @Nullable SourceSection sourceSection, + @Nullable String memberName, + @Nullable String hint) { + + super( + message, + cause, + isExternalMessage, + messageArguments, + programValues, + location, + sourceSection, + memberName, + hint); + } + + @Override + @TruffleBoundary + public String getLocalizedMessage() { + return String.format(getMessage(), getMessageArguments()); + } + + @Override + @TruffleBoundary + public PklException toPklException(StackFrameTransformer transformer) { + var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer)); + var rendered = renderer.render(this); + return new PklBugException(rendered, this); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java new file mode 100644 index 00000000..30fd4b6a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java @@ -0,0 +1,792 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.source.SourceSection; +import java.util.*; +import java.util.function.*; +import javax.annotation.concurrent.GuardedBy; +import org.graalvm.collections.*; +import org.pkl.core.Member.SourceLocation; +import org.pkl.core.PClass; +import org.pkl.core.PClassInfo; +import org.pkl.core.PObject; +import org.pkl.core.TypeParameter; +import org.pkl.core.ast.*; +import org.pkl.core.ast.member.*; +import org.pkl.core.ast.type.TypeNode; +import org.pkl.core.util.CollectionUtils; +import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +// Most stdlib modules and their members are initialized in static initializers +// and reused across Truffle contexts. +// As a consequence, VmObject/VmClass instances of stdlib modules must be thread-safe. +// The currently implemented (and likely insufficient) solution is to +// * deeply force standard library modules at initialization time. +// * ensure that any further mutation (e.g., lazy initialization in VmClass) is thread-safe. +public final class VmClass extends VmValue { + private final SourceSection sourceSection; + private final SourceSection headerSection; + private final @Nullable SourceSection docComment; + private final List annotations; + private final int modifiers; + private final PClassInfo classInfo; + private final List typeParameters; + private final VmTyped prototype; + + private final EconomicMap declaredProperties = EconomicMaps.create(); + private final EconomicMap declaredMethods = EconomicMaps.create(); + + // initialized to non-null value by `initSupertype()` for all classes but `pkl.base#Any` + @CompilationFinal private @Nullable TypeNode supertypeNode; + @CompilationFinal private @Nullable VmClass superclass; + + @LateInit + @GuardedBy("allPropertiesLock") + private UnmodifiableEconomicMap __allProperties; + + private final Object allPropertiesLock = new Object(); + + @LateInit + @GuardedBy("allMethodsLock") + private UnmodifiableEconomicMap __allMethods; + + private final Object allMethodsLock = new Object(); + + // Element type is `Object` rather than `Identifier` to enable `contains(Object)` tests + // (see signature of `UnmodifiableEconomicSet.contains()`). + @LateInit + @GuardedBy("allRegularPropertyNamesLock") + private UnmodifiableEconomicSet __allRegularPropertyNames; + + private final Object allRegularPropertyNamesLock = new Object(); + + // Element type is `Object` rather than `Identifier` to enable `contains(Object)` tests + // (see signature of `UnmodifiableEconomicSet.contains()`). + @LateInit + @GuardedBy("allHiddenPropertyNamesLock") + private UnmodifiableEconomicSet __allHiddenPropertyNames; + + private final Object allHiddenPropertyNamesLock = new Object(); + + // Helps to to overcome recursive initialization issues + // between classes and annotations in pkl.base. + @CompilationFinal private volatile boolean isInitialized; + + // PClass must be cached for correctness (identity is equality) + @LateInit + @GuardedBy("pClassLock") + private PClass __pClass; + + private final Object pClassLock = new Object(); + + @LateInit + @GuardedBy("mirrorLock") + private VmTyped __mirror; + + private final Object mirrorLock = new Object(); + + @LateInit + @GuardedBy("typedToDynamicMembersLock") + private EconomicMap __typedToDynamicMembers; + + private final Object typedToDynamicMembersLock = new Object(); + + @LateInit + @GuardedBy("dynamicToTypedMembersLock") + private EconomicMap __dynamicToTypedMembers; + + private final Object dynamicToTypedMembersLock = new Object(); + + @LateInit + @GuardedBy("mapToTypedMembersLock") + private EconomicMap __mapToTypedMembers; + + private final Object mapToTypedMembersLock = new Object(); + + public VmClass( + SourceSection sourceSection, + SourceSection headerSection, + @Nullable SourceSection docComment, + List annotations, + int modifiers, + PClassInfo classInfo, + List typeParameters, + VmTyped prototype) { + + this.sourceSection = sourceSection; + this.headerSection = headerSection; + this.docComment = docComment; + this.annotations = annotations; + this.modifiers = modifiers; + this.classInfo = classInfo; + this.typeParameters = typeParameters; + + this.prototype = prototype; + prototype.lateInitVmClass(this); + } + + public void initSupertype(TypeNode supertypeNode, VmClass superclass) { + assert this.supertypeNode == null; + assert this.superclass == null; + + this.supertypeNode = supertypeNode; + this.superclass = superclass; + prototype.lateInitParent(superclass.getPrototype()); + } + + @TruffleBoundary + public void addProperty(ClassProperty property) { + prototype.addProperty(property.getInitializer()); + EconomicMaps.put(declaredProperties, property.getName(), property); + + if (!property.isLocal()) { + __allProperties = null; + __allHiddenPropertyNames = null; + } + } + + @TruffleBoundary + public void addProperties(Iterable properties) { + for (var property : properties) { + addProperty(property); + } + } + + @TruffleBoundary + public void addMethod(ClassMethod method) { + EconomicMaps.put(declaredMethods, method.getName(), method); + + if (!method.isLocal()) { + __allMethods = null; + } + } + + @TruffleBoundary + public void addMethods(Iterable methods) { + for (var method : methods) { + addMethod(method); + } + } + + // Note: Superclasses may not have finished their initialization when this method is called. + public void notifyInitialized() { + isInitialized = true; + } + + public int getTypeParameterCount() { + return typeParameters.size(); + } + + /** + * Returns the property with the given name declared in this class, or {@code null} if no such + * property was found. Does return local properties. + */ + public @Nullable ClassProperty getDeclaredProperty(Identifier name) { + return EconomicMaps.get(declaredProperties, name); + } + + /** Returns all properties declared in this class. Does include local properties. */ + public Iterable getDeclaredProperties() { + return EconomicMaps.getValues(declaredProperties); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getClassClass(); + } + + public SourceSection getSourceSection() { + return sourceSection; + } + + public SourceSection getHeaderSection() { + return headerSection; + } + + public @Nullable SourceSection getDocComment() { + return docComment; + } + + public List getAnnotations() { + return annotations; + } + + public String getModuleName() { + return classInfo.getModuleName(); + } + + /** + * Returns the module that this class is declared in. For a module class, returns the + * corresponding module. + */ + public VmTyped getModule() { + //noinspection ConstantConditions + return classInfo.isModuleClass() ? prototype : (VmTyped) prototype.getEnclosingOwner(); + } + + public VmTyped getModuleMirror() { + return getModule().getModuleInfo().getMirror(getModule()); + } + + public String getSimpleName() { + return classInfo.getSimpleName(); + } + + /** + * Returns the qualified name of this class, `#`. Note that a qualified + * class name isn't guaranteed to be unique, especially if the module name is not declared but + * inferred. + */ + public String getQualifiedName() { + return classInfo.getQualifiedName(); + } + + public String getDisplayName() { + return classInfo.getDisplayName(); + } + + public PClassInfo getPClassInfo() { + return classInfo; + } + + @TruffleBoundary + public boolean isHiddenProperty(Object key) { + return getAllHiddenPropertyNames().contains(key); + } + + /** + * Returns the property with the given name declared in this class or a superclass, or {@code + * null} if no such property was found. Does not return local properties. + */ + public @Nullable ClassProperty getProperty(Identifier name) { + return EconomicMaps.get(getAllProperties(), name); + } + + /** Shorthand for {@code getProperty(name) != null}. */ + public boolean hasProperty(Identifier name) { + return !isInitialized || EconomicMaps.containsKey(getAllProperties(), name); + } + + /** + * Returns the names of all non-hidden non-local non-external properties defined in this class and + * its superclasses. + */ + public UnmodifiableEconomicSet getAllRegularPropertyNames() { + synchronized (allRegularPropertyNamesLock) { + if (__allRegularPropertyNames == null) { + __allRegularPropertyNames = collectAllRegularPropertyNames(); + } + return __allRegularPropertyNames; + } + } + + /** Returns the names of all hidden properties defined in this class and its superclasses. */ + public UnmodifiableEconomicSet getAllHiddenPropertyNames() { + synchronized (allHiddenPropertyNamesLock) { + if (__allHiddenPropertyNames == null) { + __allHiddenPropertyNames = collectAllHiddenPropertyNames(); + } + return __allHiddenPropertyNames; + } + } + + /** Includes local methods. */ + public boolean hasDeclaredMethod(Identifier name) { + return EconomicMaps.containsKey(declaredMethods, name); + } + + /** Does return local methods. */ + public @Nullable ClassMethod getDeclaredMethod(Identifier name) { + return EconomicMaps.get(declaredMethods, name); + } + + /** Includes local methods. */ + public Iterable getDeclaredMethods() { + return EconomicMaps.getValues(declaredMethods); + } + + /** Does not return local methods. */ + public @Nullable ClassMethod getMethod(Identifier name) { + return EconomicMaps.get(getAllMethods(), name); + } + + /** Does not include local methods. */ + public Iterable getMethods() { + return EconomicMaps.getValues(getAllMethods()); + } + + public @Nullable VmClass getSuperclass() { + return superclass; + } + + @Override + public VmTyped getPrototype() { + return prototype; + } + + public boolean isAbstract() { + return VmModifier.isAbstract(modifiers); + } + + public boolean isExternal() { + return VmModifier.isExternal(modifiers); + } + + public boolean isOpen() { + return VmModifier.isOpen(modifiers); + } + + public boolean isClosed() { + return VmModifier.isClosed(modifiers); + } + + public boolean isInstantiable() { + return VmModifier.isInstantiable(modifiers); + } + + public boolean isNullClass() { + return isClass(BaseModule.getNullClass(), "pkl.base#Null"); + } + + public boolean isCollectionClass() { + return isClass(BaseModule.getCollectionClass(), "pkl.base#Collection"); + } + + public boolean isListClass() { + return isClass(BaseModule.getListClass(), "pkl.base#List"); + } + + public boolean isSetClass() { + return isClass(BaseModule.getSetClass(), "pkl.base#Set"); + } + + public boolean isMapClass() { + return isClass(BaseModule.getMapClass(), "pkl.base#Map"); + } + + public boolean isListingClass() { + return isClass(BaseModule.getListingClass(), "pkl.base#Listing"); + } + + public boolean isMappingClass() { + return isClass(BaseModule.getMappingClass(), "pkl.base#Mapping"); + } + + public boolean isDynamicClass() { + return isClass(BaseModule.getDynamicClass(), "pkl.base#Dynamic"); + } + + public boolean isPairClass() { + return isClass(BaseModule.getPairClass(), "pkl.base#Pair"); + } + + public boolean isFunctionClass() { + return isClass(BaseModule.getFunctionClass(), "pkl.base#Function"); + } + + public boolean isFunctionNClass() { + return superclass != null + && superclass.isClass(BaseModule.getFunctionClass(), "pkl.base#Function"); + } + + public boolean isModuleClass() { + return isClass(BaseModule.getModuleClass(), "pkl.base#Module"); + } + + public boolean isClassClass() { + return isClass(BaseModule.getClassClass(), "pkl.base#Class"); + } + + public boolean isVarArgsClass() { + return isClass(BaseModule.getVarArgsClass(), "pkl.base#VarArgs"); + } + + private boolean isClass(@Nullable VmClass clazz, String qualifiedClassName) { + // may be null during evaluation of base module + return clazz != null ? this == clazz : getQualifiedName().equals(qualifiedClassName); + } + + public boolean isSuperclassOf(VmClass other) { + if (isClosed()) return this == other; + + for (var clazz = other; clazz != null; clazz = clazz.getSuperclass()) { + if (clazz == this) return true; + } + return false; + } + + public boolean isSubclassOf(VmClass other) { + return other.isSuperclassOf(this); + } + + public void visitMethodDefsTopDown(Consumer visitor) { + if (superclass != null) { + superclass.visitMethodDefsTopDown(visitor); + } + EconomicMaps.getValues(declaredMethods).forEach(visitor); + } + + @Override + public void force(boolean allowUndefinedValues) { + // do nothing + } + + public VmTyped getMirror() { + synchronized (mirrorLock) { + if (__mirror == null) { + __mirror = MirrorFactories.classFactory.create(this); + } + return __mirror; + } + } + + @TruffleBoundary + public EconomicMap getTypedToDynamicMembers() { + synchronized (typedToDynamicMembersLock) { + if (__typedToDynamicMembers == null) { + __typedToDynamicMembers = + createDelegatingMembers( + (member) -> + new UntypedObjectMemberNode( + null, new FrameDescriptor(), member, new DelegateToExtraStorageObjNode())); + } + return __typedToDynamicMembers; + } + } + + @TruffleBoundary + public EconomicMap getDynamicToTypedMembers() { + synchronized (dynamicToTypedMembersLock) { + if (__dynamicToTypedMembers == null) { + //noinspection ConstantConditions + __dynamicToTypedMembers = + createDelegatingMembers( + (member) -> + TypeCheckedPropertyNodeGen.create( + null, + new FrameDescriptor(), + member, + new DelegateToExtraStorageObjOrParentNode())); + } + return __dynamicToTypedMembers; + } + } + + @TruffleBoundary + public EconomicMap getMapToTypedMembers() { + synchronized (mapToTypedMembersLock) { + if (__mapToTypedMembers == null) { + //noinspection ConstantConditions + __mapToTypedMembers = + createDelegatingMembers( + (member) -> + TypeCheckedPropertyNodeGen.create( + null, + new FrameDescriptor(), + member, + new DelegateToExtraStorageMapOrParentNode())); + } + return __mapToTypedMembers; + } + } + + private EconomicMap createDelegatingMembers( + Function memberNodeFactory) { + var result = EconomicMaps.create(); + for (var cursor = getAllProperties().getEntries(); cursor.advance(); ) { + var property = cursor.getValue(); + // Typed->Dynamic conversion: Dynamic objects cannot currently have hidden members. + // Dynamic/Map->Typed conversion: Overall it seems more useful for the typed object + // to inherit its prototype's value for the hidden property (e.g., Module.output). + if (property.isHidden()) continue; + + var name = cursor.getKey(); + var member = + new ObjectMember( + VmUtils.unavailableSourceSection(), + VmUtils.unavailableSourceSection(), + VmModifier.NONE, + name, + name.toString()); + member.initMemberNode(memberNodeFactory.apply(member)); + result.put(name, member); + } + return result; + } + + public VmSet getModifierMirrors() { + return VmModifier.getMirrors(modifiers, true); + } + + public VmList getTypeParameterMirrors() { + var builder = VmList.EMPTY.builder(); + for (var typeParameter : typeParameters) { + builder.add(MirrorFactories.typeParameterFactory.create(typeParameter)); + } + return builder.build(); + } + + public VmValue getSuperclassMirror() { + return superclass == null ? VmNull.withoutDefault() : superclass.getMirror(); + } + + public VmValue getSupertypeMirror() { + return supertypeNode == null ? VmNull.withoutDefault() : supertypeNode.getMirror(); + } + + public VmMap getPropertyMirrors() { + var builder = VmMap.builder(); + for (var property : declaredProperties.getValues()) { + if (property.isLocal()) continue; + builder.add(property.getName().toString(), property.getMirror()); + } + return builder.build(); + } + + public VmMap getMethodMirrors() { + var builder = VmMap.builder(); + for (var method : declaredMethods.getValues()) { + if (method.isLocal()) continue; + builder.add(method.getName().toString(), method.getMirror()); + } + return builder.build(); + } + + @Override + @TruffleBoundary + public PClass export() { + synchronized (pClassLock) { + if (__pClass == null) { + var exportedAnnotations = new ArrayList(); + var properties = + CollectionUtils.newLinkedHashMap( + EconomicMaps.size(declaredProperties)); + var methods = + CollectionUtils.newLinkedHashMap( + EconomicMaps.size(declaredMethods)); + + // set pClass before exporting class members to prevent + // infinite recursion in case of cyclic references + __pClass = + new PClass( + VmUtils.exportDocComment(docComment), + new SourceLocation(headerSection.getStartLine(), sourceSection.getEndLine()), + VmModifier.export(modifiers, true), + exportedAnnotations, + classInfo, + typeParameters, + properties, + methods); + + for (var parameter : typeParameters) { + parameter.initOwner(__pClass); + } + + if (supertypeNode != null) { + assert superclass != null; + __pClass.initSupertype(TypeNode.export(supertypeNode), superclass.export()); + } + + VmUtils.exportAnnotations(annotations, exportedAnnotations); + + for (var property : EconomicMaps.getValues(declaredProperties)) { + if (isClassPropertyDefinition(property)) { + properties.put(property.getName().toString(), property.export(__pClass)); + } + } + + for (var method : EconomicMaps.getValues(declaredMethods)) { + if (method.isLocal()) continue; + methods.put(method.getName().toString(), method.export(__pClass)); + } + } + + return __pClass; + } + } + + @Override + public void accept(VmValueVisitor visitor) { + visitor.visitClass(this); + } + + @Override + public T accept(VmValueConverter converter, Iterable path) { + return converter.convertClass(this, path); + } + + @Override + public String toString() { + return getDisplayName(); + } + + private UnmodifiableEconomicMap getAllProperties() { + synchronized (allPropertiesLock) { + if (__allProperties == null) { + // can't do this in ClassNode because it requires a fully initialized inheritance hierarchy + // (which may not yet exist when ClassNode runs due to circular class dependencies) + __allProperties = collectAllProperties(); + } + return __allProperties; + } + } + + private UnmodifiableEconomicMap getAllMethods() { + synchronized (allMethodsLock) { + if (__allMethods == null) { + // can't do this in ClassNode because it requires a fully initialized inheritance hierarchy + // (which may not yet exist when ClassNode runs due to circular class dependencies) + __allMethods = collectAllMethods(); + } + return __allMethods; + } + } + + /** + * Tells if the given property defines a member of this class. Requires a fully initialized + * inheritance hierarchy. + */ + private boolean isClassPropertyDefinition(ClassProperty declaredProperty) { + if (declaredProperty.isLocal() || declaredProperty.isClass() || declaredProperty.isTypeAlias()) + return false; + return getProperty(declaredProperty.getName()) == declaredProperty; + } + + @TruffleBoundary + private UnmodifiableEconomicMap collectAllProperties() { + if (EconomicMaps.isEmpty(declaredProperties)) { + return superclass == null ? EconomicMaps.create() : superclass.getAllProperties(); + } + + var size = + EconomicMaps.size(declaredProperties) + + (superclass == null ? 0 : EconomicMaps.size(superclass.getAllProperties())); + var result = EconomicMaps.create(size); + + if (superclass != null) { + EconomicMaps.putAll(result, superclass.getAllProperties()); + } + + for (var property : EconomicMaps.getValues(declaredProperties)) { + if (property.isLocal()) continue; + + // A property is considered a class property definition + // if it has a type annotation or has no superdefinition (ad-hoc case). + // Otherwise, it is considered an object property definition, + // which means it affects the class prototype but not the class itself. + // An example for the latter is when `Module.output` is overridden with `output { ... }`. + if (property.getTypeNode() != null || !EconomicMaps.containsKey(result, property.getName())) { + EconomicMaps.put(result, property.getName(), property); + } + } + + return result; + } + + @TruffleBoundary + private UnmodifiableEconomicMap collectAllMethods() { + if (EconomicMaps.isEmpty(declaredMethods)) { + return superclass == null ? EconomicMaps.create() : superclass.getAllMethods(); + } + + var size = + EconomicMaps.size(declaredMethods) + + (superclass == null ? 0 : EconomicMaps.size(superclass.getAllMethods())); + var result = EconomicMaps.create(size); + + if (superclass != null) { + EconomicMaps.putAll(result, superclass.getAllMethods()); + } + + for (var method : EconomicMaps.getValues(declaredMethods)) { + if (method.isLocal()) continue; + + EconomicMaps.put(result, method.getName(), method); + } + + return result; + } + + @TruffleBoundary + private UnmodifiableEconomicSet collectAllRegularPropertyNames() { + if (EconomicMaps.isEmpty(declaredProperties)) { + return superclass == null ? EconomicSet.create() : superclass.getAllRegularPropertyNames(); + } + + var size = superclass == null ? 0 : superclass.getAllRegularPropertyNames().size(); + var result = EconomicSet.create(size); + for (var property : EconomicMaps.getValues(declaredProperties)) { + if (!(property.isLocal() || isHiddenProperty(property.getName()) || property.isExternal())) { + result.add(property.getName()); + } + } + + if (superclass == null) { + return result; + } + + if (result.isEmpty()) { + return superclass.getAllRegularPropertyNames(); + } + + result.addAll(superclass.getAllRegularPropertyNames()); + return result; + } + + @TruffleBoundary + private UnmodifiableEconomicSet collectAllHiddenPropertyNames() { + if (EconomicMaps.isEmpty(declaredProperties)) { + return superclass == null ? EconomicSet.create() : superclass.getAllHiddenPropertyNames(); + } + + var size = superclass == null ? 0 : superclass.getAllHiddenPropertyNames().size(); + var result = EconomicSet.create(size); + for (var property : EconomicMaps.getValues(declaredProperties)) { + if (property.isHidden()) { + result.add(property.getName()); + } + } + + if (superclass == null) { + return result; + } + + if (result.isEmpty()) { + return superclass.getAllHiddenPropertyNames(); + } + + result.addAll(superclass.getAllHiddenPropertyNames()); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + // each class is represented by a unique instance of VmClass + return this == obj; + } + + @Override + public int hashCode() { + // use a more deterministic hash code than System.identityHashCode() + return classInfo.hashCode(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmCollection.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmCollection.java new file mode 100644 index 00000000..f63b1e9d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmCollection.java @@ -0,0 +1,182 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import java.util.Iterator; +import org.organicdesign.fp.xform.Xform; + +public abstract class VmCollection extends VmValue implements Iterable { + public interface Builder { + void add(Object element); + + void addAll(Iterable elements); + + T build(); + } + + public abstract int getLength(); + + public abstract boolean isEmpty(); + + @Override + public boolean isSequence() { + return true; + } + + public abstract VmCollection add(Object element); + + public abstract VmCollection concatenate(VmCollection other); + + public abstract Iterator reverseIterator(); + + public abstract Builder builder(); + + public final void checkNonEmpty() { + if (isEmpty()) { + CompilerDirectives.transferToInterpreter(); + throw new VmExceptionBuilder() + .evalError("expectedNonEmptyCollection") + .withProgramValue("Collection", this) + .build(); + } + } + + public abstract boolean isLengthOne(); + + public final void checkLengthOne() { + if (!isLengthOne()) { + CompilerDirectives.transferToInterpreter(); + throw new VmExceptionBuilder() + .evalError("expectedSingleElementCollection") + .withProgramValue("Collection", this) + .build(); + } + } + + protected static void checkPositive(long n) { + VmUtils.checkPositive(n); + } + + @TruffleBoundary + public final boolean startsWith(VmCollection other) { + if (getLength() < other.getLength()) return false; + + var iter = iterator(); + var otherIter = other.iterator(); + + //noinspection WhileLoopReplaceableByForEach + while (otherIter.hasNext()) { + if (!iter.next().equals(otherIter.next())) return false; + } + + return true; + } + + @TruffleBoundary + public final boolean endsWith(VmCollection other) { + if (getLength() < other.getLength()) return false; + + var iter = reverseIterator(); + var otherIter = other.reverseIterator(); + + while (otherIter.hasNext()) { + if (!iter.next().equals(otherIter.next())) return false; + } + + return true; + } + + @TruffleBoundary + public final VmList replaceRange(long start, long exclusiveEnd, VmCollection replacement) { + var result = + Xform.of(this).take(start).concat(replacement).concat(Xform.of(this).drop(exclusiveEnd)); + return VmList.create(result); + } + + @TruffleBoundary + public final Object replaceRangeOrNull(long start, long exclusiveEnd, VmCollection replacement) { + var length = getLength(); + + if (start < 0 || start > length) { + return VmNull.withoutDefault(); + } + + if (exclusiveEnd < start || exclusiveEnd > length) { + return VmNull.withoutDefault(); + } + + var result = + Xform.of(this).take(start).concat(replacement).concat(Xform.of(this).drop(exclusiveEnd)); + return VmList.create(result); + } + + @TruffleBoundary + public final VmCollection flatten() { + var builder = builder(); + for (var elem : this) { + if (elem instanceof Iterable) { + builder.addAll((Iterable) elem); + } else if (elem instanceof VmListing) { + var listing = (VmListing) elem; + listing.forceAndIterateMemberValues( + (key, member, value) -> { + builder.add(value); + return true; + }); + } else { + CompilerDirectives.transferToInterpreter(); + throw new VmExceptionBuilder() + .evalError("cannotFlattenCollectionWithNonCollectionElement") + .withProgramValue("Element", elem) + .build(); + } + } + return builder.build(); + } + + @TruffleBoundary + public final VmCollection zip(VmCollection other) { + var builder = builder(); + var iter1 = iterator(); + var iter2 = other.iterator(); + while (iter1.hasNext() && iter2.hasNext()) { + builder.add(new VmPair(iter1.next(), iter2.next())); + } + return builder.build(); + } + + @TruffleBoundary + public final String join(String separator) { + if (isEmpty()) return ""; + + var iter = iterator(); + var builder = new StringBuilder(); + builder.append(iter.next()); + + while (iter.hasNext()) { + builder.append(separator); + builder.append(iter.next()); + } + + return builder.toString(); + } + + public final String toString() { + return VmValueRenderer.multiLine(Integer.MAX_VALUE).render(this); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java new file mode 100644 index 00000000..58a5d6f6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmContext.java @@ -0,0 +1,138 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.TruffleLanguage.ContextReference; +import com.oracle.truffle.api.nodes.Node; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import org.pkl.core.Logger; +import org.pkl.core.SecurityManager; +import org.pkl.core.StackFrameTransformer; +import org.pkl.core.module.ProjectDependenciesManager; +import org.pkl.core.packages.PackageResolver; +import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; + +public final class VmContext { + private static final ContextReference REFERENCE = + ContextReference.create(VmLanguage.class); + + @LateInit private Holder holder; + + public static final class Holder { + private static final String OUTPUT_FORMAT_KEY = "pkl.outputFormat"; + + private final StackFrameTransformer frameTransformer; + private final SecurityManager securityManager; + private final ModuleResolver moduleResolver; + private final ResourceManager resourceManager; + private final Logger logger; + private final Map environmentVariables; + private final Path moduleCacheDir; + private final Map externalProperties; + private final ModuleCache moduleCache; + private final @Nullable PackageResolver packageResolver; + private final @Nullable ProjectDependenciesManager projectDependenciesManager; + + public Holder( + StackFrameTransformer frameTransformer, + SecurityManager securityManager, + ModuleResolver moduleResolver, + ResourceManager resourceManager, + Logger logger, + Map environmentVariables, + Map externalProperties, + @Nullable Path moduleCacheDir, + @Nullable String outputFormat, + @Nullable PackageResolver packageResolver, + @Nullable ProjectDependenciesManager projectDependenciesManager) { + + this.frameTransformer = frameTransformer; + this.securityManager = securityManager; + this.moduleResolver = moduleResolver; + this.resourceManager = resourceManager; + this.logger = logger; + this.environmentVariables = environmentVariables; + this.moduleCacheDir = moduleCacheDir; + + // treat outputFormat as an external property from here on, at least for now + var props = new HashMap<>(externalProperties); + if (outputFormat != null) { + props.put(OUTPUT_FORMAT_KEY, outputFormat); + } + this.externalProperties = props; + + moduleCache = new ModuleCache(); + this.packageResolver = packageResolver; + this.projectDependenciesManager = projectDependenciesManager; + } + } + + public static VmContext get(@Nullable Node node) { + return REFERENCE.get(node); + } + + public void initialize(Holder holder) { + assert this.holder == null; + this.holder = holder; + } + + public ModuleCache getModuleCache() { + return holder.moduleCache; + } + + public @Nullable Path getModuleCacheDir() { + return holder.moduleCacheDir; + } + + public StackFrameTransformer getFrameTransformer() { + return holder.frameTransformer; + } + + public SecurityManager getSecurityManager() { + return holder.securityManager; + } + + public ModuleResolver getModuleResolver() { + return holder.moduleResolver; + } + + public ResourceManager getResourceManager() { + return holder.resourceManager; + } + + public Logger getLogger() { + return holder.logger; + } + + public Map getEnvironmentVariables() { + return holder.environmentVariables; + } + + public Map getExternalProperties() { + return holder.externalProperties; + } + + public @Nullable PackageResolver getPackageResolver() { + return holder.packageResolver; + } + + public @Nullable ProjectDependenciesManager getProjectDependenciesManager() { + return holder.projectDependenciesManager; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDataSize.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDataSize.java new file mode 100644 index 00000000..472b45df --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDataSize.java @@ -0,0 +1,168 @@ +/** + * 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.runtime; + +import static java.util.Map.*; + +import com.oracle.truffle.api.CompilerDirectives.ValueType; +import java.util.*; +import org.pkl.core.DataSize; +import org.pkl.core.DataSizeUnit; +import org.pkl.core.Value; +import org.pkl.core.util.MathUtils; +import org.pkl.core.util.Nullable; + +@ValueType +public final strictfp class VmDataSize extends VmValue implements Comparable { + private static final Map UNITS = + Map.ofEntries( + entry(Identifier.B, DataSizeUnit.BYTES), + entry(Identifier.KB, DataSizeUnit.KILOBYTES), + entry(Identifier.KIB, DataSizeUnit.KIBIBYTES), + entry(Identifier.MB, DataSizeUnit.MEGABYTES), + entry(Identifier.MIB, DataSizeUnit.MEBIBYTES), + entry(Identifier.GB, DataSizeUnit.GIGABYTES), + entry(Identifier.GIB, DataSizeUnit.GIBIBYTES), + entry(Identifier.TB, DataSizeUnit.TERABYTES), + entry(Identifier.TIB, DataSizeUnit.TEBIBYTES), + entry(Identifier.PB, DataSizeUnit.PETABYTES), + entry(Identifier.PIB, DataSizeUnit.PEBIBYTES)); + + private final double value; + private final DataSizeUnit unit; + + public VmDataSize(double value, DataSizeUnit unit) { + this.value = value; + this.unit = unit; + } + + public static @Nullable DataSizeUnit toUnit(Identifier identifier) { + return UNITS.get(identifier); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getDataSizeClass(); + } + + public double getValue() { + return value; + } + + public DataSizeUnit getUnit() { + return unit; + } + + public VmDataSize add(VmDataSize other) { + if (unit.ordinal() <= other.unit.ordinal()) { + return new VmDataSize(convertValueTo(other.unit) + other.value, other.unit); + } + return new VmDataSize(value + other.convertValueTo(unit), unit); + } + + public VmDataSize subtract(VmDataSize other) { + if (unit.ordinal() <= other.unit.ordinal()) { + return new VmDataSize(convertValueTo(other.unit) - other.value, other.unit); + } + return new VmDataSize(value - other.convertValueTo(unit), unit); + } + + public VmDataSize multiply(double num) { + return new VmDataSize(value * num, unit); + } + + public VmDataSize divide(double num) { + return new VmDataSize(value / num, unit); + } + + public double divide(VmDataSize other) { + // use same conversion strategy as add/subtract + if (unit.ordinal() <= other.unit.ordinal()) { + return convertValueTo(other.unit) / other.value; + } + return value / other.convertValueTo(unit); + } + + public VmDataSize remainder(double num) { + return new VmDataSize(value % num, unit); + } + + public VmDataSize pow(double num) { + return new VmDataSize(StrictMath.pow(value, num), unit); + } + + public VmDataSize round() { + return new VmDataSize(StrictMath.rint(value), unit); + } + + public VmDataSize convertTo(DataSizeUnit unit) { + return new VmDataSize(convertValueTo(unit), unit); + } + + @Override + public void force(boolean allowUndefinedValues) { + // do nothing + } + + @Override + public Value export() { + return new DataSize(value, unit); + } + + @Override + public void accept(VmValueVisitor visitor) { + visitor.visitDataSize(this); + } + + @Override + public T accept(VmValueConverter converter, Iterable path) { + return converter.convertDataSize(this, path); + } + + @Override + public int compareTo(VmDataSize other) { + // use same conversion strategy as add/subtract + if (unit.ordinal() <= other.unit.ordinal()) { + return Double.compare(convertValueTo(other.unit), other.value); + } + return Double.compare(value, other.convertValueTo(unit)); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof VmDataSize)) return false; + + var other = (VmDataSize) obj; + // converting to a fixed unit guarantees that equals() is commutative and consistent with + // hashCode() + return convertValueTo(DataSizeUnit.BYTES) == other.convertValueTo(DataSizeUnit.BYTES); + } + + @Override + public int hashCode() { + return Double.hashCode(convertValueTo(DataSizeUnit.BYTES)); + } + + @Override + public String toString() { + return MathUtils.isMathematicalInteger(value) ? (long) value + "." + unit : value + "." + unit; + } + + private double convertValueTo(DataSizeUnit other) { + return value * unit.getBytes() / other.getBytes(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDuration.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDuration.java new file mode 100644 index 00000000..87bc9be8 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDuration.java @@ -0,0 +1,160 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.ValueType; +import java.util.*; +import org.pkl.core.*; +import org.pkl.core.util.DurationUtils; +import org.pkl.core.util.Nullable; + +@ValueType +public final strictfp class VmDuration extends VmValue implements Comparable { + private static final Map UNITS = + Map.of( + Identifier.NS, DurationUnit.NANOS, + Identifier.US, DurationUnit.MICROS, + Identifier.MS, DurationUnit.MILLIS, + Identifier.S, DurationUnit.SECONDS, + Identifier.MIN, DurationUnit.MINUTES, + Identifier.H, DurationUnit.HOURS, + Identifier.D, DurationUnit.DAYS); + + private final double value; + private final DurationUnit unit; + + public VmDuration(double value, DurationUnit unit) { + this.value = value; + this.unit = unit; + } + + public static @Nullable DurationUnit toUnit(Identifier identifier) { + return UNITS.get(identifier); + } + + @Override + public VmClass getVmClass() { + return BaseModule.getDurationClass(); + } + + public VmDuration add(VmDuration other) { + if (unit.ordinal() <= other.unit.ordinal()) { + return new VmDuration(getValue(other.unit) + other.value, other.unit); + } + return new VmDuration(value + other.getValue(unit), unit); + } + + public VmDuration subtract(VmDuration other) { + if (unit.ordinal() <= other.unit.ordinal()) { + return new VmDuration(getValue(other.unit) - other.value, other.unit); + } + return new VmDuration(value - other.getValue(unit), unit); + } + + public double getValue() { + return value; + } + + public double getValue(DurationUnit other) { + return value * unit.getNanos() / other.getNanos(); + } + + public DurationUnit getUnit() { + return unit; + } + + public VmDuration multiply(double num) { + return new VmDuration(value * num, unit); + } + + public VmDuration divide(double num) { + return new VmDuration(value / num, unit); + } + + public double divide(VmDuration other) { + // use same conversion strategy as add/subtract + if (unit.ordinal() <= other.unit.ordinal()) { + return getValue(other.unit) / other.value; + } + return value / other.getValue(unit); + } + + public VmDuration remainder(double num) { + return new VmDuration(value % num, unit); + } + + public VmDuration pow(double num) { + return new VmDuration(StrictMath.pow(value, num), unit); + } + + public VmDuration round() { + return new VmDuration(StrictMath.rint(value), unit); + } + + public VmDuration convertTo(DurationUnit unit) { + return new VmDuration(getValue(unit), unit); + } + + @Override + public void force(boolean allowUndefinedValues) { + // do nothing + } + + @Override + public Value export() { + return new Duration(value, unit); + } + + @Override + public void accept(VmValueVisitor visitor) { + visitor.visitDuration(this); + } + + @Override + public T accept(VmValueConverter converter, Iterable path) { + return converter.convertDuration(this, path); + } + + @Override + public int compareTo(VmDuration other) { + // use same conversion strategy as add/subtract + if (unit.ordinal() <= other.unit.ordinal()) { + return Double.compare(getValue(other.unit), other.value); + } + return Double.compare(value, other.getValue(unit)); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof VmDuration)) return false; + + var other = (VmDuration) obj; + // converting to a fixed unit guarantees that equals() is commutative and consistent with + // hashCode() + return getValue(DurationUnit.NANOS) == other.getValue(DurationUnit.NANOS); + } + + @Override + public int hashCode() { + return Double.hashCode(getValue(DurationUnit.NANOS)); + } + + @Override + public String toString() { + return DurationUtils.toPklString(value, unit); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java new file mode 100644 index 00000000..c9704475 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java @@ -0,0 +1,164 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.MaterializedFrame; +import java.util.Objects; +import org.graalvm.collections.UnmodifiableEconomicMap; +import org.pkl.core.PClassInfo; +import org.pkl.core.PObject; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.util.CollectionUtils; +import org.pkl.core.util.EconomicMaps; + +public final class VmDynamic extends VmObject { + private int cachedRegularMemberCount = -1; + + private static final class EmptyHolder { + private static final VmDynamic EMPTY = + new VmDynamic( + VmUtils.createEmptyMaterializedFrame(), + BaseModule.getDynamicClass().getPrototype(), + EconomicMaps.create(), + 0); + } + + private final int length; + + public static VmDynamic empty() { + return EmptyHolder.EMPTY; + } + + public VmDynamic( + MaterializedFrame enclosingFrame, + VmObject parent, + UnmodifiableEconomicMap members, + int length) { + super(enclosingFrame, Objects.requireNonNull(parent), members); + this.length = length; + } + + @Override + public VmClass getVmClass() { + return BaseModule.getDynamicClass(); + } + + /** Returns the number of elements in this object. */ + public int getLength() { + return length; + } + + /** Tells whether this object has any elements. */ + public boolean hasElements() { + return length != 0; + } + + @Override + public boolean isSequence() { + return hasElements(); + } + + @Override + @TruffleBoundary + public PObject export() { + var properties = + CollectionUtils.newLinkedHashMap(EconomicMaps.size(cachedValues)); + + iterateMemberValues( + (key, member, value) -> { + properties.put(key.toString(), VmValue.exportNullable(value)); + return true; + }); + + return new PObject(PClassInfo.Dynamic, properties); + } + + @Override + public void accept(VmValueVisitor visitor) { + visitor.visitDynamic(this); + } + + @Override + public T accept(VmValueConverter converter, Iterable path) { + return converter.convertDynamic(this, path); + } + + @Override + @TruffleBoundary + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof VmDynamic)) return false; + + var other = (VmDynamic) obj; + // could use shallow force, but deep force is cached + force(false); + other.force(false); + if (getRegularMemberCount() != other.getRegularMemberCount()) return false; + + var cursor = cachedValues.getEntries(); + while (cursor.advance()) { + Object key = cursor.getKey(); + if (isHiddenOrLocalProperty(key)) continue; + + var value = cursor.getValue(); + assert value != null; + var otherValue = other.getCachedValue(key); + if (!value.equals(otherValue)) return false; + } + + return true; + } + + @Override + @TruffleBoundary + public int hashCode() { + if (cachedHash != 0) return cachedHash; + + force(false); + var result = 0; + var cursor = cachedValues.getEntries(); + + while (cursor.advance()) { + var key = cursor.getKey(); + if (isHiddenOrLocalProperty(key)) continue; + + var value = cursor.getValue(); + assert value != null; + result += key.hashCode() ^ value.hashCode(); + } + + cachedHash = result; + return result; + } + + // assumes object has been forced + public int getRegularMemberCount() { + if (cachedRegularMemberCount != -1) return cachedRegularMemberCount; + + var result = 0; + for (var key : cachedValues.getKeys()) { + if (!isHiddenOrLocalProperty(key)) result += 1; + } + cachedRegularMemberCount = result; + return result; + } + + private boolean isHiddenOrLocalProperty(Object key) { + return key instanceof Identifier + && (key == Identifier.DEFAULT || ((Identifier) key).isLocalProp()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmEvalException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmEvalException.java new file mode 100644 index 00000000..cef00778 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmEvalException.java @@ -0,0 +1,46 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; +import java.util.List; +import org.pkl.core.util.Nullable; + +public class VmEvalException extends VmException { + public VmEvalException( + String message, + @Nullable Throwable cause, + boolean isExternalMessage, + Object[] messageArguments, + List programValues, + @Nullable Node location, + @Nullable SourceSection sourceSection, + @Nullable String memberName, + @Nullable String hint) { + + super( + message, + cause, + isExternalMessage, + messageArguments, + programValues, + location, + sourceSection, + memberName, + hint); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java new file mode 100644 index 00000000..c971bf22 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java @@ -0,0 +1,124 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.pkl.core.*; +import org.pkl.core.util.Nullable; + +public abstract class VmException extends AbstractTruffleException { + private final boolean isExternalMessage; + private final Object[] messageArguments; + private final List programValues; + private final @Nullable SourceSection sourceSection; + private final @Nullable String memberName; + protected @Nullable String hint; + + private final Map insertedStackFrames = new HashMap<>(); + + public VmException( + String message, + @Nullable Throwable cause, + boolean isExternalMessage, + Object[] messageArguments, + List programValues, + @Nullable Node location, + @Nullable SourceSection sourceSection, + @Nullable String memberName, + @Nullable String hint) { + super(message, cause, UNLIMITED_STACK_TRACE, location); + this.isExternalMessage = isExternalMessage; + this.messageArguments = messageArguments; + this.programValues = programValues; + this.sourceSection = sourceSection; + this.memberName = memberName; + this.hint = hint; + } + + public final boolean isExternalMessage() { + return isExternalMessage; + } + + public final Object[] getMessageArguments() { + return messageArguments; + } + + public final List getProgramValues() { + return programValues; + } + + public final @Nullable SourceSection getSourceSection() { + return sourceSection; + } + + public final @Nullable String getMemberName() { + return memberName; + } + + public @Nullable String getHint() { + return hint; + } + + public void setHint(@Nullable String hint) { + this.hint = hint; + } + + /** + * Stack frames to insert into the stack trace presented to the user. For each entry `(target, + * frame)`, `frame` will be inserted below the top-most frame associated with `target`. + */ + public final Map getInsertedStackFrames() { + return insertedStackFrames; + } + + public enum Kind { + EVAL_ERROR, + UNDEFINED_VALUE, + BUG + } + + public static final class ProgramValue { + private static final VmValueRenderer valueRenderer = VmValueRenderer.singleLine(80); + + public final String name; + public final Object value; + + public ProgramValue(String name, Object value) { + this.name = name; + this.value = value; + } + + @Override + @TruffleBoundary + public String toString() { + return valueRenderer.render(value); + } + } + + @TruffleBoundary + public PklException toPklException(StackFrameTransformer transformer) { + var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer)); + var rendered = renderer.render(this); + return new PklException(rendered); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java new file mode 100644 index 00000000..8ff36cbf --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java @@ -0,0 +1,395 @@ +/** + * 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.runtime; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.pkl.core.runtime.MemberLookupSuggestions.Candidate.Kind; +import org.pkl.core.runtime.VmException.ProgramValue; +import org.pkl.core.util.Nullable; + +/** + * Error message guidelines: + * + *