Skip to content

Commit

Permalink
[orx-keyframer] Add support for backticked identifier names, replace …
Browse files Browse the repository at this point in the history
…spek tests
  • Loading branch information
edwinRNDR committed Jul 28, 2022
1 parent 225c285 commit 5fa4e72
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 254 deletions.
8 changes: 7 additions & 1 deletion orx-jvm/orx-keyframer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ dependencies {
implementation libs.gson
implementation(libs.kotlin.reflect)

testImplementation(libs.kotlin.test)

demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-jvm:orx-panel"))

Expand All @@ -50,4 +52,8 @@ dependencies {
}

tasks.getByName("compileKotlin").dependsOn("generateGrammarSource")
tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource")
tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource")

test {
useJUnitPlatform()
}
38 changes: 2 additions & 36 deletions orx-jvm/orx-keyframer/src/main/antlr/KeyLangLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ DECIMAL : 'Decimal';
STRING : 'String';

// Identifiers
ID : [$_]*[a-zA-Z][A-Za-z0-9_]* ;
ID : [$_]*[a-zA-Z][A-Za-z0-9_]* | '`'[$_]*[A-Za-z0-9_-]*'`';
FUNCTION_ID : [$_]*[a-z][A-Za-z0-9_]* ;

// Literals
Expand All @@ -38,6 +38,7 @@ ASSIGN : '=' ;
LPAREN : '(' ;
RPAREN : ')' ;


COMMA : ',' ;

STRING_OPEN : '"' -> pushMode(MODE_IN_STRING);
Expand All @@ -51,40 +52,5 @@ ESCAPE_SLASH : '\\\\' ;
ESCAPE_NEWLINE : '\\n' ;
ESCAPE_SHARP : '\\#' ;
STRING_CLOSE : '"' -> popMode ;
INTERPOLATION_OPEN : '#{' -> pushMode(MODE_IN_INTERPOLATION) ;
STRING_CONTENT : ~["\n\r\t\\#]+ ;
STR_UNMATCHED : . -> type(UNMATCHED) ;
mode MODE_IN_INTERPOLATION;
INTERPOLATION_CLOSE : '}' -> popMode ;
INTERP_WS : [\t ]+ -> channel(WHITESPACE), type(WS) ;
// Keywords
INTERP_AS : 'as'-> type(AS) ;
INTERP_INT : 'Int'-> type(INT) ;
INTERP_DECIMAL : 'Decimal'-> type(DECIMAL) ;
INTERP_STRING : 'String'-> type(STRING) ;
// Literals
INTERP_INTLIT : ('0'|[1-9][0-9]*) -> type(INTLIT) ;
INTERP_DECLIT : ('0'|[1-9][0-9]*) '.' [0-9]+ -> type(DECLIT) ;
// Operators
INTERP_PLUS : '+' -> type(PLUS) ;
INTERP_MINUS : '-' -> type(MINUS) ;
INTERP_ASTERISK : '*' -> type(ASTERISK) ;
INTERP_DIVISION : '/' -> type(DIVISION) ;
INTERP_PERCENTAGE : '%' -> type(PERCENTAGE) ;
INTERP_ASSIGN : '=' -> type(ASSIGN) ;
INTERP_LPAREN : '(' -> type(LPAREN) ;
INTERP_RPAREN : ')' -> type(RPAREN) ;
// Identifiers
INTERP_ID : [_]*[a-z][A-Za-z0-9_]* -> type(ID);
INTERP_STRING_OPEN : '"' -> type(STRING_OPEN), pushMode(MODE_IN_STRING);
INTERP_UNMATCHED : . -> type(UNMATCHED) ;
2 changes: 1 addition & 1 deletion orx-jvm/orx-keyframer/src/main/antlr/KeyLangParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package org.openrndr.extra.keyframer.antlr;

options { tokenVocab=KeyLangLexer; }

miniCalcFile : lines=line+ ;
keyLangFile : lines=line+ ;

line : statement (NEWLINE | EOF) ;

Expand Down
51 changes: 30 additions & 21 deletions orx-jvm/orx-keyframer/src/main/kotlin/Expressions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -248,31 +248,31 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
doubleStack.push(node.text.toDouble())
}
if (type == KeyLangParser.ID) {

val name = node.text.replace("`","")
@Suppress("DIVISION_BY_ZERO")
when (val idType = idTypeStack.pop()) {
IDType.VARIABLE -> doubleStack.push(
when (val name = node.text) {
when (name) {
"PI" -> PI
else -> variables[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
}
)

IDType.FUNCTION0 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
when (name) {
"random" -> { _ -> Double.uniform(0.0, 1.0) }
else -> functions.functions0[candidate]?.let { { _: DoubleArray -> it.invoke() } }
else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } }
?: errorValue(
"unresolved function: '${candidate}()'"
"unresolved function: '${name}()'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}

IDType.FUNCTION1 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
when (name) {
"sqrt" -> { x -> sqrt(x[0]) }
"radians" -> { x -> Math.toRadians(x[0]) }
"degrees" -> { x -> Math.toDegrees(x[0]) }
Expand All @@ -287,60 +287,69 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
"floor" -> { x -> floor(x[0]) }
"ceil" -> { x -> ceil(x[0]) }
"saturate" -> { x -> x[0].coerceIn(0.0, 1.0) }
else -> functions.functions1[candidate]?.let { { x: DoubleArray -> it.invoke(x[0]) } }
else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } }
?: errorValue(
"unresolved function: '${candidate}(x0)'"
"unresolved function: '${name}(x0)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION2 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
when (name) {
"max" -> { x -> max(x[0], x[1]) }
"min" -> { x -> min(x[0], x[1]) }
"pow" -> { x -> x[0].pow(x[1]) }
"atan2" -> { x -> atan2(x[0], x[1]) }
"random" -> { x -> Double.uniform(x[0], x[1]) }
"length" -> { x -> Vector2(x[0], x[1]).length }
else -> functions.functions2[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } }
else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1)'"
"unresolved function: '${name}(x0, x1)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION3 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
when (name) {
"mix" -> { x -> mix(x[0], x[1], x[2]) }
"min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() }
"smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) }
"length" -> { x -> Vector3(x[0], x[1], x[2]).length }
else -> functions.functions3[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2]) } }
else -> functions.functions3[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2)'"
"unresolved function: '${name}(x0, x1, x2)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION4 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
else -> functions.functions4[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3]) } }
when (name) {
"min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() }
else -> functions.functions4[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2, x3)'"
"unresolved function: '${name}(x0, x1, x2, x3)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}

IDType.FUNCTION5 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
when (name) {
"min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() }
"map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) }
else -> functions.functions5[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3], x[4]) } }
else -> functions.functions5[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3], x[4]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2, x3, x4)'"
"unresolved function: '${name}(x0, x1, x2, x3, x4)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
Expand Down Expand Up @@ -374,7 +383,7 @@ fun evaluateExpression(
}
})

val root = parser.miniCalcFile()
val root = parser.keyLangFile()
val listener = ExpressionListener(functions)
listener.variables.putAll(variables)
try {
Expand Down
67 changes: 31 additions & 36 deletions orx-jvm/orx-keyframer/src/test/kotlin/TestExpressionErrors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,50 @@ import org.amshove.kluent.`with message`
import org.amshove.kluent.invoking
import org.openrndr.extra.keyframer.ExpressionException
import org.openrndr.extra.keyframer.evaluateExpression
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.lang.IllegalStateException
import kotlin.test.Test

object TestExpressionErrors : Spek({
class TestExpressionErrors {

describe("an expression with non-sensible writing") {
@Test
fun `an expression with non-sensible writing`() {
val expression = ")("
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"
}
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"

}

describe("an expression with equality instead of assign") {
@Test
fun `an expression with equality instead of assign`() {
val expression = "a == 5"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: 'a == 5'; [line: 1, character: 3 , near: [@3,3:3='=',<19>,1:3] ]"
}
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: 'a == 5'; [line: 1, character: 3 , near: [@3,3:3='=',<19>,1:3] ]"

}

describe("an expression trying to reassign a number") {
@Test
fun `an expression trying to reassign a number`() {
val expression = "3 = 5"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: '3 = 5'; [line: 1, character: 2 , near: [@2,2:2='=',<19>,1:2] ]"
}
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: '3 = 5'; [line: 1, character: 2 , near: [@2,2:2='=',<19>,1:2] ]"
}

describe("an expression that uses non-existing functions") {
@Test
fun `an expression that uses non-existing functions`() {
val expression = "notExisting(5)"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting(5)': unresolved function: 'notExisting(x0)'"
}
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting(5)': unresolved function: 'notExisting(x0)'"

}

describe("an expression that uses non-existing variables") {
@Test
fun `an expression that uses non-existing variables`() {
val expression = "notExisting + 4"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting+4': unresolved variable: 'notExisting'"
}
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting+4': unresolved variable: 'notExisting'"
}

})
}
Loading

0 comments on commit 5fa4e72

Please sign in to comment.