Initial commit

This commit is contained in:
Peter Niederwieser
2016-01-19 14:51:19 +01:00
committed by Dan Chao
commit ecad035dca
2972 changed files with 211653 additions and 0 deletions

View File

@@ -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<String> = setOf(GeneratedBy::class.java.name)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.RELEASE_11
override fun process(annotations: Set<TypeElement>, 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<TypeElement>()
.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<PackageElement, List<TypeElement>>) {
for ((pkg, nodeClasses) in nodeClassesByPackage) {
generateRegistryClass(pkg, nodeClasses)
}
}
private fun generateRegistryClass(pkg: PackageElement, nodeClasses: List<TypeElement>) {
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 `<SimpleModuleName>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<PackageElement>) {
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() }
}

View File

@@ -0,0 +1,2 @@
# https://docs.gradle.org/current/userguide/java_plugin.html#aggregating_annotation_processors
org.pkl.core.generator.MemberRegistryGenerator,aggregating

View File

@@ -0,0 +1 @@
org.pkl.core.generator.MemberRegistryGenerator

View File

@@ -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

View File

@@ -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<StringInterpolationScope> 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?
;

View File

@@ -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
| <assoc=right> 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
| <assoc=right> 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'
;

View File

@@ -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();
}
}

View File

@@ -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<String, Object> getProperties();
}

View File

@@ -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> T accept(ValueConverter<T> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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> T accept(ValueConverter<T> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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.
*
* <p>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.
*
* <p>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<String, FileOutput> evaluateOutputFiles(ModuleSource moduleSource);
/**
* Evaluates the Pkl expression represented as {@code expression}, returning the Java
* representation of the result.
*
* <p>The following table describes how Pkl types are represented in Java:
*
* <table>
* <thead>
* <tr>
* <th>Pkl type</th>
* <th>Java type</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>Null</td>
* <td>{@link PNull}</td>
* </tr>
* <tr>
* <td>String</td>
* <td>{@link String}</td>
* </tr>
* <tr>
* <td>Boolean</td>
* <td>{@link Boolean}</td>
* </tr>
* <tr>
* <td>Int</td>
* <td>{@link Long}</td>
* </tr>
* <tr>
* <td>Float</td>
* <td>{@link Double}</td>
* </tr>
* <tr>
* <td>Typed, Dynamic</td>
* <td>{@link PObject} ({@link PModule} if the object is a module)</td>
* </tr>
* <tr>
* <td>Mapping, Map</td>
* <td>{@link Map}</td>
* </tr>
* <tr>
* <td>Listing, List</td>
* <td>{@link java.util.List}</td>
* </tr>
* <tr>
* <td>Set</td>
* <td>{@link java.util.Set}</td>
* </tr>
* <tr>
* <td>Pair</td>
* <td>{@link Pair}</td>
* </tr>
* <tr>
* <td>Regex</td>
* <td>{@link java.util.regex.Pattern}</td>
* </tr>
* <tr>
* <td>DataSize</td>
* <td>{@link DataSize}</td>
* </tr>
* <tr>
* <td>Duration</td>
* <td>{@link Duration}</td>
* </tr>
* <tr>
* <td>Class</td>
* <td>{@link PClass}</td>
* </tr>
* <tr>
* <td>TypeAlias</td>
* <td>{@link TypeAlias}</td>
* </tr>
* </tbody>
* </table>
*
* <p>The following Pkl types have no Java representation, and an error is thrown if an expression
* computes to a value of these types:
*
* <ul>
* <li>IntSeq
* <li>Function
* </ul>
*
* @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.
*
* <p>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> T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo<T> classInfo);
/**
* Runs tests within the module, and returns the test results.
*
* <p>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.
*
* <p>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.
*
* <p>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();
}

View File

@@ -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<ModuleKeyFactory> moduleKeyFactories = new ArrayList<>();
private final List<ResourceReader> resourceReaders = new ArrayList<>();
private final Map<String, String> environmentVariables = new HashMap<>();
private final Map<String, String> 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:
*
* <ul>
* <li>{@link SecurityManagers#defaultAllowedModules}
* <li>{@link SecurityManagers#defaultAllowedResources}
* <li>{@link Loggers#noop()}
* <li>{@link ModuleKeyFactories#standardLibrary}
* <li>{@link ModuleKeyFactories#classPath}
* <li>{@link ModuleKeyFactories#fromServiceProviders}
* <li>{@link ModuleKeyFactories#file}
* <li>{@link ModuleKeyFactories#pkg}
* <li>{@link ModuleKeyFactories#projectpackage}
* <li>{@link ModuleKeyFactories#genericUrl}
* <li>{@link ResourceReaders#environmentVariable}
* <li>{@link ResourceReaders#externalProperty}
* <li>{@link ResourceReaders#classPath}
* <li>{@link ResourceReaders#file}
* <li>{@link ResourceReaders#http}
* <li>{@link ResourceReaders#https}
* <li>{@link ResourceReaders#pkg}
* <li>{@link ResourceReaders#projectpackage}
* <li>{@link System#getProperties}
* </ul>
*/
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<Pattern> 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<Pattern> 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<Pattern> 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<Pattern> 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<ModuleKeyFactory> factories) {
moduleKeyFactories.addAll(factories);
return this;
}
/** Removes any existing module key factories, then adds the given factories. */
public EvaluatorBuilder setModuleKeyFactories(Collection<ModuleKeyFactory> factories) {
moduleKeyFactories.clear();
return addModuleKeyFactories(factories);
}
/** Returns the currently set module key factories. */
public List<ModuleKeyFactory> getModuleKeyFactories() {
return moduleKeyFactories;
}
public EvaluatorBuilder addResourceReader(ResourceReader reader) {
resourceReaders.add(reader);
return this;
}
public EvaluatorBuilder addResourceReaders(Collection<ResourceReader> readers) {
resourceReaders.addAll(readers);
return this;
}
public EvaluatorBuilder setResourceReaders(Collection<ResourceReader> readers) {
resourceReaders.clear();
return addResourceReaders(readers);
}
/** Returns the currently set resource readers. */
public List<ResourceReader> getResourceReaders() {
return resourceReaders;
}
/**
* Adds the given environment variable, overriding any environment variable previously added under
* the same name.
*
* <p>Pkl code can read environment variables with {@code read("env:<NAME>")}.
*/
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.
*
* <p>Pkl code can read environment variables with {@code read("env:<NAME>")}.
*/
public EvaluatorBuilder addEnvironmentVariables(Map<String, String> envVars) {
environmentVariables.putAll(envVars);
return this;
}
/** Removes any existing environment variables, then adds the given environment variables. */
public EvaluatorBuilder setEnvironmentVariables(Map<String, String> envVars) {
environmentVariables.clear();
return addEnvironmentVariables(envVars);
}
/** Returns the currently set environment variables. */
public Map<String, String> getEnvironmentVariables() {
return environmentVariables;
}
/**
* Adds the given external property, overriding any property previously set under the same name.
*
* <p>Pkl code can read external properties with {@code read("prop:<name>")}.
*/
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.
*
* <p>Pkl code can read external properties with {@code read("prop:<name>")}.
*/
public EvaluatorBuilder addExternalProperties(Map<String, String> properties) {
externalProperties.putAll(properties);
return this;
}
/** Removes any existing external properties, then adds the given properties. */
public EvaluatorBuilder setExternalProperties(Map<String, String> properties) {
externalProperties.clear();
return addExternalProperties(properties);
}
/** Returns the currently set external properties. */
public Map<String, String> 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.
*
* <p>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.
*
* <p>By default, modules support the formats described by {@link OutputFormat}. and fall back to
* {@link OutputFormat#PCF} if no format is specified.
*
* <p>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.
*
* <p>By default, modules support the formats described by {@link OutputFormat}. and fall back to
* {@link OutputFormat#PCF} if no format is specified.
*
* <p>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);
}
}

View File

@@ -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<ModuleKeyFactory> factories,
Collection<ResourceReader> readers,
Map<String, String> environmentVariables,
Map<String, String> 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<String, FileOutput> 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<String, FileOutput>();
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> T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo<T> 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> T doEvaluate(Supplier<T> 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> T doEvaluate(ModuleSource moduleSource, Function<VmTyped, T> 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;
}
}
}

View File

@@ -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();
}

View File

@@ -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.
*
* <p>Will throw {@link PklException} if a normal evaluator error occurs.
*
* <p>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);
}
}
}

View File

@@ -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<String, ?>) 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<String, ?> 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);
}
}
}
}

View File

@@ -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) {}
}

View File

@@ -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() + ')';
}
}

View File

@@ -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<Modifier> modifiers;
private final List<PObject> annotations;
private final String simpleName;
public Member(
@Nullable String docComment,
SourceLocation sourceLocation,
Set<Modifier> modifiers,
List<PObject> 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<Modifier> getModifiers() {
return modifiers;
}
/** Returns the annotations of this member. */
public List<PObject> 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<PObject> annotations;
private final Map<String, PClass> classes;
private final Map<String, TypeAlias> typeAliases;
private final Map<String, URI> imports;
@LateInit private Map<String, PClass> __allClasses;
@LateInit private Map<String, TypeAlias> __allTypeAliases;
/** Constructs a {@code ModuleSchema} instance. */
public ModuleSchema(
URI moduleUri,
String moduleName,
boolean isAmend,
@Nullable ModuleSchema supermodule,
PClass moduleClass,
@Nullable String docComment,
List<PObject> annotations,
Map<String, PClass> classes,
Map<String, TypeAlias> typeAliases,
Map<String, URI> 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.
*
* <p>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<PObject> 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.
*
* <p>Map keys are the identifiers by which imports are accessed within this module. Map values
* are the URIs of the imported modules.
*
* <p>Does not cover import expressions.
*/
public Map<String, URI> getImports() {
return imports;
}
/** Returns the classes defined in this module in declaration order. */
public Map<String, PClass> 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<String, PClass> 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<String, TypeAlias> 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<String, TypeAlias> 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;
}
}

View File

@@ -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.
*
* <p>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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 <code>--format</code> that selects this output format. */
@Override
public String toString() {
return cliArgument;
}
}

View File

@@ -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<TypeParameter> typeParameters;
private final Map<String, Property> properties;
private final Map<String, Method> methods;
private @Nullable PType supertype;
private @Nullable PClass superclass;
private @Nullable Map<String, Property> allProperties;
private @Nullable Map<String, Method> allMethods;
public PClass(
@Nullable String docComment,
SourceLocation sourceLocation,
Set<Modifier> modifiers,
List<PObject> annotations,
PClassInfo<?> classInfo,
List<TypeParameter> typeParameters,
Map<String, Property> properties,
Map<String, Method> 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<TypeParameter> getTypeParameters() {
return typeParameters;
}
public @Nullable PType getSupertype() {
return supertype;
}
public @Nullable PClass getSuperclass() {
return superclass;
}
public Map<String, Property> getProperties() {
return properties;
}
public Map<String, Method> getMethods() {
return methods;
}
public Map<String, Property> getAllProperties() {
if (allProperties == null) {
allProperties = collectAllProperties(this, new LinkedHashMap<>());
}
return allProperties;
}
public Map<String, Method> getAllMethods() {
if (allMethods == null) {
allMethods = collectAllMethods(this, new LinkedHashMap<>());
}
return allMethods;
}
@Override
public void accept(ValueVisitor visitor) {
visitor.visitClass(this);
}
@Override
public <T> T accept(ValueConverter<T> 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<Modifier> modifiers,
List<PObject> 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<Modifier> modifiers,
List<PObject> 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<TypeParameter> typeParameters;
private final Map<String, PType> parameters;
private final PType returnType;
public Method(
PClass owner,
@Nullable String docComment,
SourceLocation sourceLocation,
Set<Modifier> modifiers,
List<PObject> annotations,
String simpleName,
List<TypeParameter> typeParameters,
Map<String, PType> parameters,
PType returnType) {
super(docComment, sourceLocation, modifiers, annotations, simpleName, owner);
this.typeParameters = typeParameters;
this.parameters = parameters;
this.returnType = returnType;
}
public List<TypeParameter> getTypeParameters() {
return typeParameters;
}
public Map<String, PType> 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<String, Property> collectAllProperties(
PClass clazz, Map<String, Property> collector) {
if (clazz.superclass != null) {
collectAllProperties(clazz.superclass, collector);
}
collector.putAll(clazz.properties);
return collector;
}
private Map<String, Method> collectAllMethods(PClass clazz, Map<String, Method> collector) {
if (clazz.superclass != null) {
collectAllMethods(clazz.superclass, collector);
}
collector.putAll(clazz.methods);
return collector;
}
}

View File

@@ -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<T> 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<Void> Any = pklBaseClassInfo("Any", Void.class);
public static final PClassInfo<PNull> Null = pklBaseClassInfo("Null", PNull.class);
public static final PClassInfo<String> String = pklBaseClassInfo("String", String.class);
public static final PClassInfo<Boolean> Boolean = pklBaseClassInfo("Boolean", boolean.class);
public static final PClassInfo<Void> Number = pklBaseClassInfo("Number", Void.class);
public static final PClassInfo<Long> Int = pklBaseClassInfo("Int", long.class);
public static final PClassInfo<Double> Float = pklBaseClassInfo("Float", double.class);
public static final PClassInfo<Duration> Duration = pklBaseClassInfo("Duration", Duration.class);
public static final PClassInfo<DataSize> DataSize = pklBaseClassInfo("DataSize", DataSize.class);
public static final PClassInfo<Pair> Pair = pklBaseClassInfo("Pair", Pair.class);
public static final PClassInfo<Void> Collection = pklBaseClassInfo("Collection", Void.class);
public static final PClassInfo<ArrayList> List = pklBaseClassInfo("List", ArrayList.class);
public static final PClassInfo<LinkedHashSet> Set = pklBaseClassInfo("Set", LinkedHashSet.class);
public static final PClassInfo<LinkedHashMap> Map = pklBaseClassInfo("Map", LinkedHashMap.class);
public static final PClassInfo<PObject> Object = pklBaseClassInfo("Object", PObject.class);
public static final PClassInfo<PObject> Dynamic = pklBaseClassInfo("Dynamic", PObject.class);
public static final PClassInfo<PObject> Typed = pklBaseClassInfo("Typed", PObject.class);
public static final PClassInfo<ArrayList> Listing = pklBaseClassInfo("Listing", ArrayList.class);
public static final PClassInfo<LinkedHashMap> Mapping =
pklBaseClassInfo("Mapping", LinkedHashMap.class);
public static final PClassInfo<PModule> Module = pklBaseClassInfo("Module", PModule.class);
public static final PClassInfo<PClass> Class = pklBaseClassInfo("Class", PClass.class);
public static final PClassInfo<TypeAlias> TypeAlias =
pklBaseClassInfo("TypeAlias", TypeAlias.class);
public static final PClassInfo<Pattern> Regex = pklBaseClassInfo("Regex", Pattern.class);
public static final PClassInfo<PObject> Deprecated =
pklBaseClassInfo("Deprecated", PObject.class);
public static final PClassInfo<PObject> AlsoKnownAs =
pklBaseClassInfo("AlsoKnownAs", PObject.class);
public static final PClassInfo<PObject> Unlisted = pklBaseClassInfo("Unlisted", PObject.class);
public static final PClassInfo<PObject> DocExample =
pklBaseClassInfo("DocExample", PObject.class);
public static final PClassInfo<PObject> PcfRenderDirective =
pklBaseClassInfo("PcfRenderDirective", PObject.class);
public static final PClassInfo<PObject> ModuleInfo =
pklBaseClassInfo("ModuleInfo", PObject.class);
public static final PClassInfo<PObject> Version =
new PClassInfo<>("pkl.semver", "Version", PObject.class, pklSemverUri);
public static final PClassInfo<PObject> Project =
new PClassInfo<>("pkl.Project", "ModuleClass", PObject.class, pklProjectUri);
public static final PClassInfo<Object> 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 <T> PClassInfo<T> forValue(T value) {
if (value instanceof Value) return (PClassInfo<T>) ((Value) value).getClassInfo();
if (value instanceof String) return (PClassInfo<T>) String;
if (value instanceof Boolean) return (PClassInfo<T>) Boolean;
if (value instanceof Long) return (PClassInfo<T>) Int;
if (value instanceof Double) return (PClassInfo<T>) Float;
if (value instanceof List) return (PClassInfo<T>) List;
if (value instanceof Set) return (PClassInfo<T>) Set;
if (value instanceof Map) return (PClassInfo<T>) Map;
if (value instanceof Pattern) return (PClassInfo<T>) 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<T> 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<String, PClassInfo<?>> 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<T> javaClass;
private PClassInfo(String moduleName, String className, Class<T> javaClass, URI moduleUri) {
this.moduleName = moduleName;
this.className = className;
this.moduleUri = moduleUri;
this.qualifiedName = moduleName + "#" + className;
this.javaClass = javaClass;
}
private static <T> PClassInfo<T> pklBaseClassInfo(String className, Class<T> javaType) {
return new PClassInfo<>("pkl.base", className, javaType, pklBaseUri);
}
public URI getModuleUri() {
return moduleUri;
}
}

View File

@@ -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('"', "&quot;")
.withEscape('\'', "&apos;")
.withEscape('<', "&lt;")
.withEscape('>', "&gt;")
.withEscape('&', "&amp;")
.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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
write(LINE_BREAK);
write(
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
write(LINE_BREAK);
write("<plist version=\"1.0\">");
write(LINE_BREAK);
new Visitor().visit(value);
write(LINE_BREAK);
write("</plist>");
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("<string>");
write(charEscaper.escape(value));
write("</string>");
}
@Override
public void visitInt(Long value) {
write("<integer>");
write(value.toString());
write("</integer>");
}
// 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("<real>");
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("</real>");
}
@Override
public void visitBoolean(Boolean value) {
write(value ? "<true/>" : "<false/>");
}
@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("<dict>");
write(LINE_BREAK);
currIndent += indent;
renderedAtLeastOneEntry = true;
}
write(currIndent);
write("<key>");
write(charEscaper.escape((String) key));
write("</key>");
write(LINE_BREAK);
write(currIndent);
visit(value);
write(LINE_BREAK);
}
if (renderedAtLeastOneEntry) {
currIndent = currIndent.substring(0, currIndent.length() - indent.length());
write(currIndent);
write("</dict>");
} else {
write("<dict/>");
}
}
@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("<array/>");
return;
}
write("<array>");
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("</array>");
}
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("<dict>");
write(LINE_BREAK);
currIndent += indent;
renderedAtLeastOneProperty = true;
}
write(currIndent);
write("<key>");
write(charEscaper.escape(entry.getKey()));
write("</key>");
write(LINE_BREAK);
write(currIndent);
visit(value);
write(LINE_BREAK);
}
if (renderedAtLeastOneProperty) {
currIndent = currIndent.substring(0, currIndent.length() - indent.length());
write(currIndent);
write("</dict>");
} else {
write("<dict/>");
}
}
}
private void write(String str) {
try {
writer.write(str);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

View File

@@ -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<String, Object> 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);
}
}

View File

@@ -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> T accept(ValueConverter<T> converter) {
return converter.convertNull();
}
@Override
public PClassInfo<?> getClassInfo() {
return PClassInfo.Null;
}
@Override
public String toString() {
return "null";
}
}

View File

@@ -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<String, Object> properties;
public PObject(PClassInfo<?> classInfo, Map<String, Object> properties) {
this.classInfo = Objects.requireNonNull(classInfo, "classInfo");
this.properties = Objects.requireNonNull(properties, "properties");
}
@Override
public final PClassInfo<?> getClassInfo() {
return classInfo;
}
@Override
public final Map<String, Object> 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> T accept(ValueConverter<T> 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();
}
}

View File

@@ -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<PType> 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<PType> typeArguments;
public Class(PClass pClass, List<PType> 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<PType> 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<String> constraints;
public Constrained(PType baseType, List<String> constraints) {
this.baseType = baseType;
this.constraints = constraints;
}
public PType getBaseType() {
return baseType;
}
public List<String> getConstraints() {
return constraints;
}
}
public static final class Alias extends PType {
private static final long serialVersionUID = 0L;
private final TypeAlias typeAlias;
private final List<PType> typeArguments;
private final PType aliasedType;
public Alias(TypeAlias typeAlias) {
this(typeAlias, List.of(), typeAlias.getAliasedType());
}
public Alias(TypeAlias typeAlias, List<PType> typeArguments, PType aliasedType) {
this.typeAlias = typeAlias;
this.typeArguments = typeArguments;
this.aliasedType = aliasedType;
}
public TypeAlias getTypeAlias() {
return typeAlias;
}
@Override
public List<PType> 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<PType> parameterTypes;
private final PType returnType;
public Function(List<PType> parameterTypes, PType returnType) {
this.parameterTypes = parameterTypes;
this.returnType = returnType;
}
public List<PType> getParameterTypes() {
return parameterTypes;
}
public PType getReturnType() {
return returnType;
}
}
public static final class Union extends PType {
private static final long serialVersionUID = 0L;
private final List<PType> elementTypes;
public Union(List<PType> elementTypes) {
this.elementTypes = elementTypes;
}
public List<PType> 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;
}
}
}

View File

@@ -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<F, S> implements Value, Iterable<Object> {
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<Object> 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> T accept(ValueConverter<T> 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 + ")";
}
}

View File

@@ -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<String, Object> 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('`');
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 + "/";
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<String> {
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);
}
}
}
}

View File

@@ -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. <b>Note:</b> 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<String> modules;
/** Constructs a {@link StandardLibrary}. */
public StandardLibrary(Set<String> modules) {
this.modules = modules;
}
/** The modules of this standard library. */
public Set<String> 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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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}.
*
* <p>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;
}

View File

@@ -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 <B> concrete type of the builder class to maintain covariance in inherited methods
*/
public interface SecurityManagerBuilder<B extends SecurityManagerBuilder<B>> {
B addAllowedModule(Pattern pattern);
B addAllowedModules(Collection<Pattern> patterns);
B setAllowedModules(Collection<Pattern> patterns);
List<Pattern> getAllowedModules();
B addAllowedResource(Pattern pattern);
B addAllowedResources(Collection<Pattern> patterns);
B setAllowedResources(Collection<Pattern> patterns);
List<Pattern> getAllowedResources();
B setRootDir(@Nullable Path rootDir);
@Nullable
Path getRootDir();
SecurityManager build();
}

View File

@@ -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.
*
* <p>{@link SecurityManagerException#getMessage()} is passed to users when errors arise.
*/
public class SecurityManagerException extends Exception {
public SecurityManagerException(String message) {
super(message);
}
}

View File

@@ -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<Pattern> 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<Pattern> 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}.
*
* <p>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.
*
* <p>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<URI, Integer> 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.
*
* <p>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<Pattern> allowedModules,
List<Pattern> allowedResources,
Function<URI, Integer> 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<Pattern> allowedModules;
private final List<Pattern> allowedResources;
private final Function<URI, Integer> trustLevels;
private final @Nullable Path rootDir;
Standard(
List<Pattern> allowedModules,
List<Pattern> allowedResources,
Function<URI, Integer> 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<Pattern> 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<StandardBuilder> {
private final List<Pattern> allowedModules = new ArrayList<>();
private final List<Pattern> allowedResources = new ArrayList<>();
private Path rootDir;
private StandardBuilder() {}
@Override
public StandardBuilder addAllowedModule(Pattern pattern) {
allowedModules.add(pattern);
return this;
}
@Override
public StandardBuilder addAllowedModules(Collection<Pattern> patterns) {
allowedModules.addAll(patterns);
return this;
}
@Override
public StandardBuilder setAllowedModules(Collection<Pattern> patterns) {
allowedModules.clear();
allowedModules.addAll(patterns);
return this;
}
@Override
public List<Pattern> getAllowedModules() {
return allowedModules;
}
@Override
public StandardBuilder addAllowedResource(Pattern pattern) {
allowedResources.add(pattern);
return this;
}
@Override
public StandardBuilder addAllowedResources(Collection<Pattern> patterns) {
allowedResources.addAll(patterns);
return this;
}
@Override
public StandardBuilder setAllowedResources(Collection<Pattern> patterns) {
allowedResources.clear();
allowedResources.addAll(patterns);
return this;
}
@Override
public List<Pattern> 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);
}
}
}

View File

@@ -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<String> 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<String> 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<String> 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;
}
}

View File

@@ -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<StackFrame, StackFrame> {
default StackFrameTransformer andThen(StackFrameTransformer transformer) {
return stackTrace -> transformer.apply(apply(stackTrace));
}
}

View File

@@ -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
}
}

View File

@@ -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<TypeParameter> typeParameters;
@LateInit private PType aliasedType;
public TypeAlias(
@Nullable String docComment,
SourceLocation sourceLocation,
Set<Modifier> modifiers,
List<PObject> annotations,
String simpleName,
String moduleName,
String qualifiedName,
List<TypeParameter> 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<TypeParameter> 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> T accept(ValueConverter<T> converter) {
return converter.convertTypeAlias(this);
}
@Override
public PClassInfo<?> getClassInfo() {
return PClassInfo.TypeAlias;
}
public String toString() {
return getDisplayName();
}
}

View File

@@ -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
}
}

View File

@@ -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.
*
* <p>The following Pkl values aren't represented as {@code Value} but as instances of Java standard
* library classes:
*
* <ul>
* <li>{@code pkl.base#String}: {@link java.lang.String}
* <li>{@code pkl.base#Boolean}: {@link java.lang.Boolean}
* <li>{@code pkl.base#Int}: {@link java.lang.Long}
* <li>{@code pkl.base#Float}: {@link java.lang.Double}
* <li>{@code pkl.base#List}: {@link java.util.List}
* <li>{@code pkl.base#Set}: {@link java.util.Set}
* <li>{@code pkl.base#Map}: {@link java.util.Map}
* <li>{@code pkl.base#Listing}: {@link java.util.List}
* <li>{@code pkl.base#Mapping}: {@link java.util.Map}
* <li>{@code pkl.base#Regex}: {@link java.util.regex.Pattern}
* </ul>
*/
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> T accept(ValueConverter<T> converter);
/** Returns information about the Pkl class associated with this {@code Value}. */
PClassInfo<?> getClassInfo();
}

View File

@@ -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> {
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);
}
}
}

View File

@@ -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}.
*
* <p>If {@code useMultilineStrings} is {@code true}, string values containing newline characters
* are formatted as multiline string literals.
*
* <p>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).
*
* <p>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:
*
* <ul>
* <li>What is the maximum number of consecutive pound characters that follow a multiline
* quote ({@code """})?
* <li>What is the maximum number of consecutive pound characters that follow a single line
* quote ({@code "})?
* <li>Are there newline characters in the string?
* </ul>
*
* This is used to assist with rendering custom delimited strings (e.g. {@code #"..."#}).
*
* <p>Algorithm:
*
* <ol>
* <li>Determine the current token context (backlash, single line quote, multiline quote,
* other).
* <li>If there is a current token context, count the number of pound characters succeeding
* that token.
* <li>Keep track of the maximum number of pound characters for each token type.
* </ol>
*/
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,
}
}
}

View File

@@ -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.
*
* <p>Some renderers impose restrictions on which types of values can be rendered as document.
*
* <p>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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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).
*
* <p>This class guarantees that valid semantic version numbers are handled correctly, but does
* <em>not</em> 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<Version> {
// 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<Version> 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.
*
* <p>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.
*
* <p>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<Version> 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<Identifier> {
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);
}
}
}

View File

@@ -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<String, ?>) 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<String, ?> 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()));
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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
}

View File

@@ -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<ExpressionNode, ExpressionNode> 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.
*
* <p>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();
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<Modifier> 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;
}
}

View File

@@ -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<T> extends PklParserBaseVisitor<T> {
protected final Source source;
protected AbstractAstBuilder(Source source) {
this.source = source;
}
protected abstract VmExceptionBuilder exceptionBuilder();
protected String doVisitSingleLineConstantStringPart(List<Token> 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<? extends ModifierContext> 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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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.
*
* <p>Gathers the following:
*
* <ul>
* <li>amends/extends URI's
* <li>import declarations
* <li>import expressions
* <li>read expressions
* </ul>
*/
public class ImportsAndReadsParser
extends AbstractAstBuilder<@Nullable List<Pair<String, SourceSection>>> {
/** Parses a module, and collects all imports and reads. */
public static @Nullable List<Pair<String, SourceSection>> 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<Pair<String, SourceSection>> visitModuleExtendsOrAmendsClause(
ModuleExtendsOrAmendsClauseContext ctx) {
var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts);
var sourceSection = createSourceSection(ctx.stringConstant());
return Collections.singletonList(Pair.of(importStr, sourceSection));
}
@Override
public List<Pair<String, SourceSection>> visitImportClause(ImportClauseContext ctx) {
var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts);
var sourceSection = createSourceSection(ctx.stringConstant());
return Collections.singletonList(Pair.of(importStr, sourceSection));
}
@Override
public List<Pair<String, SourceSection>> visitImportExpr(ImportExprContext ctx) {
var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts);
var sourceSection = createSourceSection(ctx.stringConstant());
return Collections.singletonList(Pair.of(importStr, sourceSection));
}
@Override
public List<Pair<String, SourceSection>> 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<Pair<String, SourceSection>> aggregateResult(
@Nullable List<Pair<String, SourceSection>> aggregate,
@Nullable List<Pair<String, SourceSection>> nextResult) {
if (aggregate == null || aggregate.isEmpty()) {
return nextResult;
}
if (nextResult == null || nextResult.isEmpty()) {
return aggregate;
}
var ret = new ArrayList<Pair<String, SourceSection>>(aggregate.size() + nextResult.size());
ret.addAll(aggregate);
ret.addAll(nextResult);
return ret;
}
}

View File

@@ -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<TypeParameter> typeParameters,
Function<ClassScope, ObjectMember> nodeFactory) {
return doEnter(
new ClassScope(
currentScope,
name,
toQualifiedName(name),
FrameDescriptor.newBuilder(),
typeParameters),
nodeFactory);
}
public ObjectMember enterTypeAlias(
Identifier name,
List<TypeParameter> typeParameters,
Function<TypeAliasScope, ObjectMember> nodeFactory) {
return doEnter(
new TypeAliasScope(
currentScope,
name,
toQualifiedName(name),
FrameDescriptor.newBuilder(),
typeParameters),
nodeFactory);
}
public <T> T enterMethod(
Identifier name,
ConstLevel constLevel,
Builder frameDescriptorBuilder,
List<TypeParameter> typeParameters,
Function<MethodScope, T> nodeFactory) {
return doEnter(
new MethodScope(
currentScope,
name,
toQualifiedName(name),
constLevel,
frameDescriptorBuilder,
typeParameters),
nodeFactory);
}
public <T> T enterLambda(
FrameDescriptor.Builder frameDescriptorBuilder, Function<LambdaScope, T> 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> T enterProperty(
Identifier name, ConstLevel constLevel, Function<PropertyScope, T> nodeFactory) {
return doEnter(
new PropertyScope(
currentScope, name, toQualifiedName(name), constLevel, FrameDescriptor.newBuilder()),
nodeFactory);
}
public <T> T enterEntry(
@Nullable ExpressionNode keyNode, // null for listing elements
Function<EntryScope, T> nodeFactory) {
var qualifiedName = currentScope.getQualifiedName() + currentScope.getNextEntryName(keyNode);
return doEnter(
new EntryScope(currentScope, qualifiedName, FrameDescriptor.newBuilder()), nodeFactory);
}
public <T> T enterCustomThisScope(Function<CustomThisScope, T> nodeFactory) {
return doEnter(
new CustomThisScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory);
}
public <T> T enterAnnotationScope(Function<AnnotationScope, T> nodeFactory) {
return doEnter(
new AnnotationScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory);
}
public <T> T enterObjectScope(Function<ObjectScope, T> nodeFactory) {
return doEnter(new ObjectScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory);
}
private <T, S extends Scope> T doEnter(S scope, Function<S, T> 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<Identifier> 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.
*
* <p>A const scope is a lexical scope on the right-hand side of a const property.
*
* <pre>{@code
* const foo = new {
* bar {
* baz // <-- depth == 1
* }
* }
* }</pre>
*/
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.
*
* <p>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<Identifier> getForGeneratorVariables() {
return forGeneratorVariables;
}
private String getNextLambdaName() {
return "<function#" + (++skipLambdaScopes().lambdaCount) + ">";
}
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<TypeParameter> typeParameters;
public TypeParameterizableScope(
Scope parent,
Identifier name,
String qualifiedName,
ConstLevel constLevel,
Builder frameDescriptorBuilder,
List<TypeParameter> 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<TypeParameter> 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<TypeParameter> 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<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder, typeParameters);
}
}
/**
* A scope where {@code this} has a special meaning (type constraint, object member predicate).
*
* <p>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);
}
}
}

View File

@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.core.ast.builder;
import org.pkl.core.util.NonnullByDefault;

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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<? extends VmValue> 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<? extends VmValue> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<? extends VmValue> 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<? extends VmValue> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.core.ast.expression.binary;
import org.pkl.core.util.NonnullByDefault;

View File

@@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More