move parser out of pkl-core (#1024)

This commit is contained in:
Islon Scherer
2025-03-18 20:23:53 +01:00
committed by GitHub
parent 1cd0549bd6
commit aad530b9a8
80 changed files with 904 additions and 737 deletions
-387
View File
@@ -1,387 +0,0 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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?
;
-254
View File
@@ -1,254 +0,0 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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'
;
File diff suppressed because it is too large Load Diff
@@ -1,54 +0,0 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.parser
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class LexerTest {
@Test
fun isRegularIdentifier() {
assertThat(Lexer.isRegularIdentifier("pigeon")).isTrue
assertThat(Lexer.isRegularIdentifier("_pigeon")).isTrue
assertThat(Lexer.isRegularIdentifier("f_red")).isTrue
assertThat(Lexer.isRegularIdentifier("\$pigeon")).isTrue
assertThat(Lexer.isRegularIdentifier("f\$red")).isTrue
assertThat(Lexer.isRegularIdentifier("जावास्क्रिप्ट")).isTrue
assertThat(Lexer.isRegularIdentifier("this")).isFalse
assertThat(Lexer.isRegularIdentifier("😀")).isFalse
}
@Test
fun maybeQuoteIdentifier() {
assertThat(Lexer.maybeQuoteIdentifier("pigeon")).isEqualTo("pigeon")
assertThat(Lexer.maybeQuoteIdentifier("_pigeon")).isEqualTo("_pigeon")
assertThat(Lexer.maybeQuoteIdentifier("\$pigeon")).isEqualTo("\$pigeon")
assertThat(Lexer.maybeQuoteIdentifier("जावास्क्रिप्ट")).isEqualTo("जावास्क्रिप्ट")
assertThat(Lexer.maybeQuoteIdentifier("this")).isEqualTo("`this`")
assertThat(Lexer.maybeQuoteIdentifier("😀")).isEqualTo("`😀`")
}
@Test
fun `lexer keywords are sorted`() {
assertThat(Lexer.KEYWORDS).isSortedAccordingTo { a, b -> a.compareTo(b.name) }
}
}
@@ -1,103 +0,0 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.parser
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.extension
import org.junit.jupiter.api.Test
import org.pkl.commons.walk
class ParserComparisonTest : ParserComparisonTestInterface {
@Test
fun testAsPrecedence() {
compare("prop = 3 + bar as Int * 5")
}
@Test
fun testStringInterpolation() {
compare(
"""
prop = "\(bar)"
prop2 = "foo \(bar)"
prop3 = "\(bar) foo"
prop4 = "foo \(bar + baz) foo"
"""
.trimIndent()
)
compare(
"""
prop = ""${'"'}
\(bar)
""${'"'}
prop2 = ""${'"'}
foo \(bar)
""${'"'}
prop3 = ""${'"'}
\(bar) foo
""${'"'}
prop4 = ""${'"'}
foo \(bar + baz) foo
""${'"'}
"""
.trimIndent()
)
}
override fun getSnippets(): List<Path> {
return Path("src/test/files/LanguageSnippetTests/input")
.walk()
.filter { path ->
val pathStr = path.toString().replace("\\", "/")
path.extension == "pkl" &&
!exceptions.any { pathStr.endsWith(it) } &&
!regexExceptions.any { it.matches(pathStr) }
}
.toList()
}
companion object {
// tests that are not syntactically valid Pkl
private val exceptions =
setOf(
"stringError1.pkl",
"annotationIsNotExpression2.pkl",
"amendsRequiresParens.pkl",
"errors/parser18.pkl",
"errors/nested1.pkl",
"errors/invalidCharacterEscape.pkl",
"errors/invalidUnicodeEscape.pkl",
"errors/unterminatedUnicodeEscape.pkl",
"errors/keywordNotAllowedHere1.pkl",
"errors/keywordNotAllowedHere2.pkl",
"errors/keywordNotAllowedHere3.pkl",
"errors/keywordNotAllowedHere4.pkl",
"errors/moduleWithHighMinPklVersionAndParseErrors.pkl",
"errors/underscore.pkl",
"notAUnionDefault.pkl",
"multipleDefaults.pkl",
)
private val regexExceptions =
setOf(
Regex(".*/errors/delimiters/.*"),
Regex(".*/errors/parser\\d+\\.pkl"),
Regex(".*/parser/.*"),
)
}
}
@@ -1,112 +0,0 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.parser
import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.io.path.readText
import org.antlr.v4.runtime.ANTLRInputStream
import org.antlr.v4.runtime.CommonTokenStream
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
import org.pkl.core.parser.antlr.PklLexer
import org.pkl.core.parser.antlr.PklParser
@Execution(ExecutionMode.CONCURRENT)
interface ParserComparisonTestInterface {
@Test
@Execution(ExecutionMode.CONCURRENT)
fun compareSnippetTests() {
SoftAssertions.assertSoftly { softly ->
getSnippets()
.parallelStream()
.map { Pair(it.pathString, it.readText()) }
.forEach { (path, snippet) ->
try {
compare(snippet, path, softly)
} catch (e: ParserError) {
softly.fail("path: $path. Message: ${e.message}", e)
}
}
}
}
@Test
@Execution(ExecutionMode.CONCURRENT)
fun compareSnippetTestsSpans() {
SoftAssertions.assertSoftly { softly ->
getSnippets()
.parallelStream()
.map { Pair(it.pathString, it.readText()) }
.forEach { (path, snippet) ->
try {
compareSpans(snippet, path, softly)
} catch (e: ParserError) {
softly.fail("path: $path. Message: ${e.message}", e)
}
}
}
}
fun getSnippets(): List<Path>
fun compare(code: String, path: String? = null, softly: SoftAssertions? = null) {
val (sexp, antlrExp) = renderBoth(code)
when {
(path != null && softly != null) ->
softly.assertThat(sexp).`as`("path: $path").isEqualTo(antlrExp)
else -> assertThat(sexp).isEqualTo(antlrExp)
}
}
fun compareSpans(code: String, path: String, softly: SoftAssertions) {
// Our ANTLR grammar always start doc comment spans in the beginning of the line,
// even though they may have leading spaces.
// This is a regression, but it's a bugfix
if (path.endsWith("annotation1.pkl")) return
val parser = Parser()
val mod = parser.parseModule(code)
val lexer = PklLexer(ANTLRInputStream(code))
val antlr = PklParser(CommonTokenStream(lexer))
val antlrMod = antlr.module()
val comparer = SpanComparison(path, softly)
comparer.compare(mod, antlrMod)
}
fun renderBoth(code: String): Pair<String, String> = Pair(renderCode(code), renderANTLRCode(code))
companion object {
private fun renderCode(code: String): String {
val parser = Parser()
val mod = parser.parseModule(code)
val renderer = SexpRenderer()
return renderer.render(mod)
}
private fun renderANTLRCode(code: String): String {
val lexer = PklLexer(ANTLRInputStream(code))
val parser = PklParser(CommonTokenStream(lexer))
val mod = parser.module()
val renderer = ANTLRSexpRenderer()
return renderer.render(mod)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,464 +0,0 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.parser
import org.antlr.v4.runtime.ParserRuleContext
import org.antlr.v4.runtime.tree.TerminalNode
import org.assertj.core.api.SoftAssertions
import org.pkl.core.parser.antlr.PklParser.*
import org.pkl.core.parser.syntax.*
import org.pkl.core.parser.syntax.Annotation
import org.pkl.core.parser.syntax.Expr.AmendsExpr
import org.pkl.core.parser.syntax.Expr.BinaryOperatorExpr
import org.pkl.core.parser.syntax.Expr.FunctionLiteralExpr
import org.pkl.core.parser.syntax.Expr.IfExpr
import org.pkl.core.parser.syntax.Expr.ImportExpr
import org.pkl.core.parser.syntax.Expr.LetExpr
import org.pkl.core.parser.syntax.Expr.LogicalNotExpr
import org.pkl.core.parser.syntax.Expr.MultiLineStringLiteralExpr
import org.pkl.core.parser.syntax.Expr.NewExpr
import org.pkl.core.parser.syntax.Expr.NonNullExpr
import org.pkl.core.parser.syntax.Expr.ParenthesizedExpr
import org.pkl.core.parser.syntax.Expr.QualifiedAccessExpr
import org.pkl.core.parser.syntax.Expr.ReadExpr
import org.pkl.core.parser.syntax.Expr.SingleLineStringLiteralExpr
import org.pkl.core.parser.syntax.Expr.SubscriptExpr
import org.pkl.core.parser.syntax.Expr.SuperAccessExpr
import org.pkl.core.parser.syntax.Expr.SuperSubscriptExpr
import org.pkl.core.parser.syntax.Expr.ThrowExpr
import org.pkl.core.parser.syntax.Expr.TraceExpr
import org.pkl.core.parser.syntax.Expr.TypeCastExpr
import org.pkl.core.parser.syntax.Expr.TypeCheckExpr
import org.pkl.core.parser.syntax.Expr.UnaryMinusExpr
import org.pkl.core.parser.syntax.Expr.UnqualifiedAccessExpr
import org.pkl.core.parser.syntax.ObjectMember.ForGenerator
import org.pkl.core.parser.syntax.ObjectMember.MemberPredicate
import org.pkl.core.parser.syntax.ObjectMember.ObjectElement
import org.pkl.core.parser.syntax.ObjectMember.ObjectEntry
import org.pkl.core.parser.syntax.ObjectMember.ObjectMethod
import org.pkl.core.parser.syntax.ObjectMember.ObjectProperty
import org.pkl.core.parser.syntax.ObjectMember.ObjectSpread
import org.pkl.core.parser.syntax.ObjectMember.WhenGenerator
import org.pkl.core.parser.syntax.Parameter.TypedIdentifier
import org.pkl.core.parser.syntax.Type.ConstrainedType
import org.pkl.core.parser.syntax.Type.DeclaredType
import org.pkl.core.parser.syntax.Type.FunctionType
import org.pkl.core.parser.syntax.Type.NullableType
import org.pkl.core.parser.syntax.Type.ParenthesizedType
import org.pkl.core.parser.syntax.Type.StringConstantType
import org.pkl.core.parser.syntax.Type.UnionType
class SpanComparison(val path: String, private val softly: SoftAssertions) {
fun compare(module: Module, modCtx: ModuleContext) {
compareSpan(module, modCtx)
if (module.decl !== null) {
compareModuleDecl(module.decl!!, modCtx.moduleDecl())
}
module.imports.zip(modCtx.importClause()).forEach { (i1, i2) -> compareImport(i1, i2) }
module.classes.zip(modCtx.clazz()).forEach { (class1, class2) -> compareClass(class1, class2) }
module.typeAliases.zip(modCtx.typeAlias()).forEach { (ta1, ta2) -> compareTypealias(ta1, ta2) }
module.properties.zip(modCtx.classProperty()).forEach { (prop1, prop2) ->
compareProperty(prop1, prop2)
}
module.methods.zip(modCtx.classMethod()).forEach { (m1, m2) -> compareMethod(m1, m2) }
}
private fun compareModuleDecl(node: ModuleDecl, ctx: ModuleDeclContext) {
compareSpan(node, ctx)
compareDocComment(node.docComment, ctx.DocComment())
node.annotations.zip(ctx.annotation()).forEach { (a1, a2) -> compareAnnotation(a1, a2) }
val header = ctx.moduleHeader()
node.modifiers.zip(header.modifier()).forEach { (m1, m2) -> compareSpan(m1, m2) }
compareQualifiedIdentifier(node.name, header.qualifiedIdentifier())
compareExtendsOrAmendsClause(node.extendsOrAmendsDecl, header.moduleExtendsOrAmendsClause())
}
private fun compareImport(node: ImportClause, ctx: ImportClauseContext) {
compareSpan(node, ctx)
compareSpan(node.importStr, ctx.stringConstant())
compareSpan(node.alias, ctx.Identifier())
}
private fun compareClass(node: Class, ctx: ClazzContext) {
compareSpan(node, ctx)
compareDocComment(node.docComment, ctx.DocComment())
node.annotations.zip(ctx.annotation()).forEach { (a1, a2) -> compareAnnotation(a1, a2) }
val header = ctx.classHeader()
node.modifiers.zip(header.modifier()).forEach { (m1, m2) -> compareSpan(m1, m2) }
compareSpan(node.classKeyword, header.CLASS())
compareSpan(node.name, header.Identifier())
compareTypeParameterList(node.typeParameterList, header.typeParameterList())
compareType(node.superClass, header.type())
compareClassBody(node.body, ctx.classBody())
compareSpan(node.headerSpan, header)
}
private fun compareTypealias(node: TypeAlias, ctx: TypeAliasContext) {
compareSpan(node, ctx)
compareDocComment(node.docComment, ctx.DocComment())
node.annotations.zip(ctx.annotation()).forEach { (a1, a2) -> compareAnnotation(a1, a2) }
val header = ctx.typeAliasHeader()
node.modifiers.zip(header.modifier()).forEach { (m1, m2) -> compareSpan(m1, m2) }
compareSpan(node.typealiasKeyword, header.TYPE_ALIAS())
compareSpan(node.name, header.Identifier())
compareTypeParameterList(node.typeParameterList, header.typeParameterList())
compareType(node.type, ctx.type())
compareSpan(node.headerSpan, header)
}
private fun compareProperty(node: ClassProperty, ctx: ClassPropertyContext) {
if (node.docComment === null) {
compareSpan(node, ctx)
}
compareDocComment(node.docComment, ctx.DocComment())
node.annotations.zip(ctx.annotation()).forEach { (a1, a2) -> compareAnnotation(a1, a2) }
node.modifiers.zip(ctx.modifier()).forEach { (m1, m2) -> compareSpan(m1, m2) }
compareSpan(node.name, ctx.Identifier())
compareTypeAnnotation(node.typeAnnotation, ctx.typeAnnotation())
compareExpr(node.expr, ctx.expr())
node.bodyList.zip(ctx.objectBody()).forEach { (b1, b2) -> compareObjectBody(b1, b2) }
}
private fun compareMethod(node: ClassMethod, ctx: ClassMethodContext) {
compareSpan(node, ctx)
compareDocComment(node.docComment, ctx.DocComment())
node.annotations.zip(ctx.annotation()).forEach { (a1, a2) -> compareAnnotation(a1, a2) }
val header = ctx.methodHeader()
compareSpan(node.headerSpan, header)
node.modifiers.zip(header.modifier()).forEach { (m1, m2) -> compareSpan(m1, m2) }
compareSpan(node.name, header.Identifier())
compareTypeParameterList(node.typeParameterList, header.typeParameterList())
compareParameterList(node.parameterList, header.parameterList())
compareTypeAnnotation(node.typeAnnotation, header.typeAnnotation())
compareExpr(node.expr, ctx.expr())
}
private fun compareAnnotation(node: Annotation, ctx: AnnotationContext) {
compareSpan(node, ctx)
compareType(node.type, ctx.type())
compareObjectBody(node.body, ctx.objectBody())
}
private fun compareParameterList(node: ParameterList?, ctx: ParameterListContext?) {
if (node === null) return
compareSpan(node, ctx!!)
node.parameters.zip(ctx.parameter()).forEach { (p1, p2) -> compareParameter(p1, p2) }
}
private fun compareParameter(node: Parameter?, ctx: ParameterContext?) {
if (node === null) return
compareSpan(node, ctx!!)
if (node is TypedIdentifier) {
val tident = ctx.typedIdentifier()
compareSpan(node.identifier, tident.Identifier())
compareTypeAnnotation(node.typeAnnotation, tident.typeAnnotation())
}
}
private fun compareTypeParameterList(node: TypeParameterList?, ctx: TypeParameterListContext?) {
if (node === null) return
compareSpan(node, ctx!!)
node.parameters.zip(ctx.typeParameter()).forEach { (p1, p2) -> compareTypeParameter(p1, p2) }
}
private fun compareTypeParameter(node: TypeParameter, ctx: TypeParameterContext) {
compareSpan(node, ctx)
compareSpan(node.identifier, ctx.Identifier())
}
private fun compareClassBody(node: ClassBody?, ctx: ClassBodyContext?) {
if (node === null) return
compareSpan(node, ctx!!)
node.properties.zip(ctx.classProperty()).forEach { (p1, p2) -> compareProperty(p1, p2) }
node.methods.zip(ctx.classMethod()).forEach { (m1, m2) -> compareMethod(m1, m2) }
}
private fun compareTypeAnnotation(node: TypeAnnotation?, ctx: TypeAnnotationContext?) {
if (node === null) return
compareSpan(node, ctx!!)
compareSpan(node.type, ctx.type())
}
private fun compareObjectBody(node: ObjectBody?, ctx: ObjectBodyContext?) {
if (node === null) return
compareSpan(node, ctx!!)
node.parameters.zip(ctx.parameter()).forEach { (p1, p2) -> compareParameter(p1, p2) }
node.members.zip(ctx.objectMember()).forEach { (m1, m2) -> compareObjectMember(m1, m2) }
}
private fun compareType(node: Type?, actx: TypeContext?) {
if (node === null) return
val ctx = if (actx is DefaultUnionTypeContext) actx.type() else actx
compareSpan(node, ctx!!)
when (node) {
is StringConstantType ->
compareSpan(node.str, (ctx as StringLiteralTypeContext).stringConstant())
is DeclaredType -> {
val decl = ctx as DeclaredTypeContext
compareQualifiedIdentifier(node.name, decl.qualifiedIdentifier())
compareTypeArgumentList(node.args, ctx.typeArgumentList())
}
is ParenthesizedType -> compareType(node.type, (ctx as ParenthesizedTypeContext).type())
is NullableType -> compareType(node.type, (ctx as NullableTypeContext).type())
is ConstrainedType -> {
val cons = ctx as ConstrainedTypeContext
compareType(node.type, cons.type())
node.exprs.zip(cons.expr()).forEach { (e1, e2) -> compareExpr(e1, e2) }
}
is UnionType -> {
val flattened = ANTLRSexpRenderer.flattenUnion(ctx as UnionTypeContext)
node.types.zip(flattened).forEach { (t1, t2) -> compareType(t1, t2) }
}
is FunctionType -> {
val func = ctx as FunctionTypeContext
node.args.zip(func.ps).forEach { (t1, t2) -> compareType(t1, t2) }
compareType(node.ret, func.r)
}
else -> {}
}
}
private fun compareObjectMember(node: ObjectMember, ctx: ObjectMemberContext) {
compareSpan(node, ctx)
when (node) {
is ObjectElement -> compareExpr(node.expr, (ctx as ObjectElementContext).expr())
is ObjectProperty -> {
ctx as ObjectPropertyContext
node.modifiers.zip(ctx.modifier()).forEach { (m1, m2) -> compareSpan(m1, m2) }
compareSpan(node.identifier, ctx.Identifier())
compareTypeAnnotation(node.typeAnnotation, ctx.typeAnnotation())
compareExpr(node.expr, ctx.expr())
if (node.bodyList.isNotEmpty()) {
node.bodyList.zip(ctx.objectBody()).forEach { (b1, b2) -> compareObjectBody(b1, b2) }
}
}
is ObjectMethod -> {
ctx as ObjectMethodContext
val header = ctx.methodHeader()
node.modifiers.zip(header.modifier()).forEach { (m1, m2) -> compareSpan(m1, m2) }
compareSpan(node.functionKeyword, header.FUNCTION())
compareSpan(node.identifier, header.Identifier())
compareTypeParameterList(node.typeParameterList, header.typeParameterList())
compareParameterList(node.paramList, header.parameterList())
compareTypeAnnotation(node.typeAnnotation, header.typeAnnotation())
compareExpr(node.expr, ctx.expr())
compareSpan(node.headerSpan(), header)
}
is MemberPredicate -> {
ctx as MemberPredicateContext
compareExpr(node.pred, ctx.k)
compareExpr(node.expr, ctx.v)
if (node.bodyList.isNotEmpty()) {
node.bodyList.zip(ctx.objectBody()).forEach { (b1, b2) -> compareObjectBody(b1, b2) }
}
}
is ObjectEntry -> {
ctx as ObjectEntryContext
compareExpr(node.key, ctx.k)
compareExpr(node.value, ctx.v)
if (node.bodyList.isNotEmpty()) {
node.bodyList.zip(ctx.objectBody()).forEach { (b1, b2) -> compareObjectBody(b1, b2) }
}
}
is ObjectSpread -> compareExpr(node.expr, (ctx as ObjectSpreadContext).expr())
is WhenGenerator -> {
ctx as WhenGeneratorContext
compareExpr(node.predicate, ctx.expr())
compareObjectBody(node.thenClause, ctx.b1)
compareObjectBody(node.elseClause, ctx.b2)
}
is ForGenerator -> {
ctx as ForGeneratorContext
compareParameter(node.p1, ctx.t1)
compareParameter(node.p2, ctx.t2)
compareExpr(node.expr, ctx.expr())
compareObjectBody(node.body, ctx.objectBody())
}
}
}
private fun compareExpr(node: Expr?, ctx: ExprContext?) {
if (node === null) return
compareSpan(node, ctx!!)
when (node) {
is SingleLineStringLiteralExpr -> {
node.parts.zip((ctx as SingleLineStringLiteralContext).singleLineStringPart()).forEach {
(s1, s2) ->
compareSpan(s1, s2)
}
}
is MultiLineStringLiteralExpr -> {
// only compare interpolated expressions
val exprs = node.parts.filterIsInstance<StringPart.StringInterpolation>()
val antlrExprs =
(ctx as MultiLineStringLiteralContext).multiLineStringPart().mapNotNull { it.expr() }
exprs.zip(antlrExprs).forEach { (s1, s2) -> compareExpr(s1.expr, s2) }
}
is ThrowExpr -> compareExpr(node.expr, (ctx as ThrowExprContext).expr())
is TraceExpr -> compareExpr(node.expr, (ctx as TraceExprContext).expr())
is ImportExpr -> compareSpan(node.importStr, (ctx as ImportExprContext).stringConstant())
is ReadExpr -> compareExpr(node.expr, (ctx as ReadExprContext).expr())
is UnqualifiedAccessExpr -> {
ctx as UnqualifiedAccessExprContext
compareSpan(node.identifier, ctx.Identifier())
compareArgumentList(node.argumentList, ctx.argumentList())
}
is QualifiedAccessExpr -> {
ctx as QualifiedAccessExprContext
compareExpr(node.expr, ctx.expr())
compareSpan(node.identifier, ctx.Identifier())
compareArgumentList(node.argumentList, ctx.argumentList())
}
is SuperAccessExpr -> {
ctx as SuperAccessExprContext
compareSpan(node.identifier, ctx.Identifier())
compareArgumentList(node.argumentList, ctx.argumentList())
}
is SuperSubscriptExpr -> compareExpr(node.arg, (ctx as SuperSubscriptExprContext).expr())
is SubscriptExpr -> {
ctx as SubscriptExprContext
compareExpr(node.expr, ctx.l)
compareExpr(node.arg, ctx.r)
}
is IfExpr -> {
ctx as IfExprContext
compareExpr(node.cond, ctx.c)
compareExpr(node.then, ctx.l)
compareExpr(node.els, ctx.r)
}
is LetExpr -> {
ctx as LetExprContext
compareParameter(node.parameter, ctx.parameter())
compareExpr(node.bindingExpr, ctx.l)
compareExpr(node.expr, ctx.r)
}
is FunctionLiteralExpr -> {
ctx as FunctionLiteralContext
compareParameterList(node.parameterList, ctx.parameterList())
compareExpr(node.expr, ctx.expr())
}
is ParenthesizedExpr -> compareExpr(node.expr, (ctx as ParenthesizedExprContext).expr())
is NewExpr -> {
ctx as NewExprContext
compareType(node.type, ctx.type())
compareObjectBody(node.body, ctx.objectBody())
}
is AmendsExpr -> {
ctx as AmendExprContext
compareExpr(node.expr, ctx.expr())
compareObjectBody(node.body, ctx.objectBody())
}
is NonNullExpr -> compareExpr(node.expr, (ctx as NonNullExprContext).expr())
is UnaryMinusExpr -> compareExpr(node.expr, (ctx as UnaryMinusExprContext).expr())
is LogicalNotExpr -> compareExpr(node.expr, (ctx as LogicalNotExprContext).expr())
is BinaryOperatorExpr -> {
val (l, r) =
when (ctx) {
is ExponentiationExprContext -> ctx.l to ctx.r
is MultiplicativeExprContext -> ctx.l to ctx.r
is AdditiveExprContext -> ctx.l to ctx.r
is ComparisonExprContext -> ctx.l to ctx.r
is EqualityExprContext -> ctx.l to ctx.r
is LogicalAndExprContext -> ctx.l to ctx.r
is LogicalOrExprContext -> ctx.l to ctx.r
is PipeExprContext -> ctx.l to ctx.r
is NullCoalesceExprContext -> ctx.l to ctx.r
else -> throw RuntimeException("unreacheable code")
}
compareExpr(node.left, l)
compareExpr(node.right, r)
}
is TypeCheckExpr -> {
ctx as TypeTestExprContext
compareExpr(node.expr, ctx.expr())
compareType(node.type, ctx.type())
}
is TypeCastExpr -> {
ctx as TypeTestExprContext
compareExpr(node.expr, ctx.expr())
compareType(node.type, ctx.type())
}
else -> {}
}
}
private fun compareArgumentList(node: ArgumentList?, ctx: ArgumentListContext?) {
if (node === null) return
compareSpan(node, ctx!!)
node.arguments.zip(ctx.expr()).forEach { (e1, e2) -> compareExpr(e1, e2) }
}
private fun compareTypeArgumentList(node: TypeArgumentList?, ctx: TypeArgumentListContext?) {
if (node === null) return
compareSpan(node, ctx!!)
node.types.zip(ctx.type()).forEach { (t1, t2) -> compareType(t1, t2) }
}
private fun compareQualifiedIdentifier(
node: QualifiedIdentifier?,
ctx: QualifiedIdentifierContext?,
) {
if (node === null) return
compareSpan(node, ctx!!)
node.identifiers.zip(ctx.Identifier()).forEach { (id1, id2) -> compareSpan(id1, id2) }
}
private fun compareExtendsOrAmendsClause(
node: ExtendsOrAmendsClause?,
ctx: ModuleExtendsOrAmendsClauseContext?,
) {
if (node === null) return
compareSpan(node, ctx!!)
compareSpan(node.url, ctx.stringConstant())
}
private fun compareSpan(node: Node?, ctx: ParserRuleContext) {
if (node != null) {
compareSpan(node, ctx.start.startIndex, ctx.stop.stopIndex)
}
}
private fun compareSpan(node: Node?, ctx: TerminalNode?) {
if (node != null) {
compareSpan(node, ctx!!.symbol.startIndex, ctx.symbol.stopIndex)
}
}
private fun compareSpan(node: Node, charIndex: Int, tokenStop: Int) {
compareSpan(node.span(), charIndex, tokenStop)
}
private fun compareSpan(span: Span, ctx: ParserRuleContext) {
compareSpan(span, ctx.start.startIndex, ctx.stop.stopIndex)
}
private fun compareSpan(span: Span, charIndex: Int, tokenStop: Int) {
val length = tokenStop - charIndex + 1
softly.assertThat(span.charIndex).`as`("$span, index for path: $path").isEqualTo(charIndex)
softly.assertThat(span.length).`as`("$span, length for path: $path").isEqualTo(length)
}
private fun compareDocComment(node: Node?, ctx: TerminalNode?) {
if (node == null) return
val charIndex = ctx!!.symbol.startIndex
// for some reason antlr's doc coments are off by one
val length = ctx.symbol.stopIndex - charIndex
val span = node.span()
softly.assertThat(span.charIndex).`as`("$span, index for path: $path").isEqualTo(charIndex)
softly.assertThat(span.length).`as`("$span, length for path: $path").isEqualTo(length)
}
}
@@ -1,41 +0,0 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.parser
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class SpanTest {
@Test
fun `endWith test`() {
var span1 = Span(10, 20)
var span2 = Span(20, 20)
assertThat(span1.endWith(span2)).isEqualTo(Span(10, 30))
span1 = Span(10, 20)
span2 = Span(0, 40)
assertThat(span1.endWith(span2)).isEqualTo(Span(10, 30))
span1 = Span(10, 30)
span2 = Span(20, 20)
assertThat(span1.endWith(span2)).isEqualTo(Span(10, 30))
span1 = Span(10, 30)
span2 = Span(20, 5)
assertThat(span1.endWith(span2)).isEqualTo(Span(10, 15))
}
}