Add orx-expression-evaluator-typed

This commit is contained in:
Edwin Jakobs
2024-06-10 14:41:57 +02:00
parent f43a322cb0
commit e8ed9ebbe8
24 changed files with 2484 additions and 37 deletions

View File

@@ -6,14 +6,6 @@ channels { WHITESPACE }
NEWLINE : '\r\n' | '\r' | '\n' ;
WS : [\t ]+ -> channel(WHITESPACE) ;
// Keywords
INPUT : 'input' ;
VAR : 'var' ;
PRINT : 'print';
AS : 'as';
INT : 'Int';
DECIMAL : 'Decimal';
STRING : 'String';
// Identifiers
ID : [$_]*[a-zA-Z][A-Za-z0-9_]* | '`'[$_]*[A-Za-z0-9_-]*'`';
@@ -34,8 +26,21 @@ ASSIGN : '=' ;
LPAREN : '(' ;
RPAREN : ')' ;
QUESTION_MARK : '?' ;
COLON : ':' ;
COMMA : ',' ;
DOT : '.' ;
EQ : '==' ;
LT : '<' ;
LTEQ : '<=' ;
GT : '>=' ;
GTEQ : '>' ;
AND : '&&' ;
OR : '||' ;
NOT : '!' ;
STRING_OPEN : '"' -> pushMode(MODE_IN_STRING);

View File

@@ -7,22 +7,16 @@ keyLangFile : lines=line+ ;
line : statement (NEWLINE | EOF) ;
statement : inputDeclaration # inputDeclarationStatement
| varDeclaration # varDeclarationStatement
| assignment # assignmentStatement
| print # printStatement
| expression # expressionStatement ;
print : PRINT LPAREN expression RPAREN ;
inputDeclaration : INPUT type name=ID ;
varDeclaration : VAR assignment ;
assignment : ID ASSIGN expression ;
statement :
expression # expressionStatement ;
expression : INTLIT # intLiteral
| DECLIT # decimalLiteral
| expression DOT ID LPAREN RPAREN # memberFunctionCall0Expression
| expression DOT ID LPAREN expression RPAREN # memberFunctionCall1Expression
| expression DOT ID LPAREN expression COMMA expression RPAREN # memberFunctionCall2Expression
| expression DOT ID LPAREN expression COMMA expression COMMA expression RPAREN # memberFunctionCall3Expression
| expression DOT ID LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN # memberFunctionCall4Expression
| ID LPAREN RPAREN # functionCall0Expression
| ID LPAREN expression RPAREN # functionCall1Expression
| ID LPAREN expression COMMA expression RPAREN # functionCall2Expression
@@ -30,13 +24,15 @@ expression : INTLIT # int
| ID LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN # functionCall4Expression
| ID LPAREN expression COMMA expression COMMA expression COMMA expression COMMA expression RPAREN # functionCall5Expression
| ID # valueReference
| STRING_OPEN (parts+=stringLiteralContent)* STRING_CLOSE # stringLiteral
| expression DOT ID # propReference
| LPAREN expression RPAREN # parenExpression
| MINUS expression # minusExpression
| NOT expression # negateExpression
| expression operator=(DIVISION|ASTERISK|PERCENTAGE) expression # binaryOperation1
| expression operator=(PLUS|MINUS) expression # binaryOperation2;
type : DECIMAL # decimal
| INT # integer
| STRING # string ;
| expression operator=(PLUS|MINUS) expression # binaryOperation2
| expression operator=(EQ|LT|LTEQ|GT|GTEQ) expression # comparisonOperation
| expression operator=(AND|OR) expression # joinOperation
| expression QUESTION_MARK expression COLON expression # ternaryExpression;
stringLiteralContent : STRING_CONTENT;

View File

@@ -0,0 +1,37 @@
package org.openrndr.extra.expressions.typed
import org.antlr.v4.kotlinruntime.tree.ParseTreeWalker
import org.openrndr.extra.expressions.ExpressionException
fun <T0, R> compileFunction1OrNull(
expression: String,
parameter0: String,
constants: (String)->Any? = { null },
functions: TypedFunctionExtensions = TypedFunctionExtensions.EMPTY
): ((T0) -> R)? {
require(constants(parameter0) == null) {
"${parameter0} is in constants with value '${constants(parameter0)}"
}
try {
val root = org.openrndr.extra.expressions.typed.expressionRoot(expression)
var varP0: T0? = null
val variables = fun(p : String) : Any? {
return if (p == parameter0) {
varP0
} else {
constants(p)
}
}
val listener = TypedExpressionListener(functions, variables)
return { p0 ->
varP0 = p0
ParseTreeWalker.DEFAULT.walk(listener, root)
listener.lastExpressionResult as? R ?: error("no result")
}
} catch (e: ExpressionException) {
return null
}
}

View File

@@ -0,0 +1,140 @@
package org.openrndr.extra.expressions.typed
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.math.transforms.scale
import org.openrndr.math.transforms.translate
import kotlin.math.abs as abs_
import kotlin.math.cos as cos_
import kotlin.math.sin as sin_
import kotlin.math.sqrt as sqrt_
internal fun vec2(x: Any): Vector2 {
require(x is Double)
return Vector2(x, x)
}
internal fun vec3(x: Any): Vector3 {
require(x is Double)
return Vector3(x, x, x)
}
internal fun vec4(x: Any): Vector4 {
require(x is Double)
return Vector4(x, x, x, x)
}
internal fun rgba(x: Any): ColorRGBa {
return when (x) {
is Double -> ColorRGBa(x, x, x, 1.0)
is Vector3 -> ColorRGBa(x.x, x.y, x.z, 1.0)
is Vector4 -> ColorRGBa(x.x, x.y, x.z, x.w)
else -> error("type not supported ${x::class.simpleName}")
}
}
internal fun cos(x: Any): Any {
return when (x) {
is Double -> cos_(x)
is Vector2 -> x.map { cos_(it) }
is Vector3 -> x.map { cos_(it) }
is Vector4 -> x.map { cos_(it) }
else -> error("type not supported ${x::class.simpleName}")
}
}
internal fun sin(x: Any): Any {
return when (x) {
is Double -> sin_(x)
is Vector2 -> x.map { sin_(it) }
is Vector3 -> x.map { sin_(it) }
is Vector4 -> x.map { sin_(it) }
else -> error("type not supported ${x::class.simpleName}")
}
}
internal fun normalize(x: Any): Any {
return when (x) {
is Vector2 -> x.normalized
is Vector3 -> x.normalized
is Vector4 -> x.normalized
else -> error("type not supported ${x::class.simpleName}")
}
}
internal fun inverse(x: Any): Any {
return when (x) {
is Matrix44 -> x.inversed
else -> error("type not supported ${x::class.simpleName}")
}
}
internal fun transpose(x: Any): Any {
return when (x) {
is Matrix44 -> x.transposed
else -> error("type not supported ${x::class.simpleName}")
}
}
fun abs(x: Any): Any {
return when (x) {
is Double -> abs_(x)
is Vector2 -> x.map { abs_(it) }
is Vector3 -> x.map { abs_(it) }
is Vector4 -> x.map { abs_(it) }
else -> error("type not supported ${x::class.simpleName}")
}
}
internal fun scale(scale: Any): Matrix44 {
@Suppress("NAME_SHADOWING") val scale = when (scale) {
is Double -> Vector3(scale, scale, scale)
is Vector2 -> scale.xy1
is Vector3 -> scale
else -> error("unsupported axis argument")
}
return Matrix44.scale(scale)
}
internal fun sqrt(x: Any): Any {
return when (x) {
is Double -> sqrt_(x)
is Vector2 -> x.map { sqrt_(it) }
is Vector3 -> x.map { sqrt_(it) }
is Vector4 -> x.map { sqrt_(it) }
else -> error("type not supported ${x::class.simpleName}")
}
}
internal fun translate(translation: Any): Matrix44 {
@Suppress("NAME_SHADOWING") val translation = when (translation) {
is Vector2 -> translation.xy0
is Vector3 -> translation
else -> error("unsupported axis argument")
}
return Matrix44.translate(translation)
}
internal fun dispatchFunction1(name: String, functions: Map<String, TypedFunction1>): ((Array<Any>) -> Any)? {
return when (name) {
"vec2" -> { x -> vec2(x[0]) }
"vec3" -> { x -> vec3(x[0]) }
"vec4" -> { x -> vec4(x[0]) }
"cos" -> { x -> cos(x[0]) }
"sin" -> { x -> sin(x[0]) }
"sqrt" -> { v -> sqrt(v[0]) }
"abs" -> { v -> abs(v[0]) }
"scale" -> { x -> scale(x[0]) }
"translate" -> { x -> translate(x[0]) }
"transpose" -> { x -> transpose(x[0]) }
"inverse" -> { x -> inverse(x[0]) }
"normalize" -> { x -> normalize(x[0]) }
else -> functions[name]?.let { { x: Array<Any> -> it.invoke(x[0]) } }
}
}

View File

@@ -0,0 +1,73 @@
package org.openrndr.extra.expressions.typed
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.math.transforms.rotate
import org.openrndr.math.transforms.translate
import org.openrndr.math.min as min_
import org.openrndr.math.max as max_
import kotlin.math.max as max_
import kotlin.math.min as min_
internal fun rotate(axis: Any, angleInDegrees:Any): Matrix44 {
require(angleInDegrees is Double)
@Suppress("NAME_SHADOWING") val axis = when(axis) {
is Vector2 -> axis.xy0
is Vector3 -> axis
else -> error("unsupported axis argument")
}
return Matrix44.rotate(axis, angleInDegrees)
}
internal fun min(x: Any, y: Any): Any {
return when {
x is Double && y is Double -> min_(x, y)
x is Vector2 && y is Vector2 -> min_(x, y)
x is Vector3 && y is Vector3 -> min_(x, y)
x is Vector4 && y is Vector4 -> min_(x, y)
else -> error("unsupported arguments")
}
}
internal fun max(x: Any, y: Any): Any {
return when {
x is Double && y is Double -> max_(x, y)
x is Vector2 && y is Vector2 -> max_(x, y)
x is Vector3 && y is Vector3 -> max_(x, y)
x is Vector4 && y is Vector4 -> max_(x, y)
else -> error("unsupported arguments")
}
}
internal fun vec2(x: Any, y: Any): Vector2 {
require(x is Double)
require(y is Double)
return Vector2(x, y)
}
internal fun vec3(x: Any, y: Any): Vector3 = when {
x is Double && y is Vector2 -> {
Vector3(x, y.x, y.y)
}
x is Vector2 && y is Double -> {
Vector3(x.x, x.y, y)
}
else -> {
error("unsupported arguments, '$x' (${x::class}) '$y' (${y::class}")
}
}
internal fun dispatchFunction2(name: String, functions: Map<String, TypedFunction2>): ((Array<Any>) -> Any)? {
return when (name) {
"min" -> { x -> min(x[0], x[1]) }
"max" -> { x -> max(x[0], x[1]) }
"vec2" -> { x -> vec2(x[0], x[1]) }
"vec3" -> { x -> vec3(x[0], x[1]) }
"rotate" -> { x -> rotate(x[0], x[1]) }
else -> functions[name]?.let { { x: Array<Any> -> it.invoke(x[0], x[1]) } }
}
}

View File

@@ -0,0 +1,43 @@
package org.openrndr.extra.expressions.typed
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.math.mix as mix_
internal fun mix(x: Any, y: Any, f: Any): Any {
return when {
x is Double && y is Double && f is Double -> mix_(x, y, f)
x is Vector2 && y is Vector2 && f is Double -> mix_(x, y, f)
x is Vector3 && y is Vector3 && f is Double -> mix_(x, y, f)
x is Vector4 && y is Vector4 && f is Double -> mix_(x, y, f)
else -> error("unsupported arguments")
}
}
internal fun vec3(x: Any, y: Any, z: Any): Vector3 {
require(x is Double && y is Double && z is Double)
return Vector3(x, y, z)
}
internal fun vec4(x: Any, y: Any, z: Any): Vector4 {
return when {
x is Vector2 && y is Double && z is Double -> Vector4(x.x, x.y, y, z)
x is Double && y is Vector2 && z is Double -> Vector4(x, y.x, y.y, z)
x is Double && y is Double && z is Vector2 -> Vector4(x, y, z.x, z.y)
else -> error("unsupported arguments")
}
}
internal fun dispatchFunction3(name: String, functions: Map<String, TypedFunction3>): ((Array<Any>) -> Any)? {
return when (name) {
"vec3" -> { x -> vec3(x[0], x[1], x[2]) }
"vec4" -> { x -> vec4(x[0], x[1], x[2]) }
"mix" -> { x -> mix(x[0], x[1], x[2]) }
else -> functions[name]?.let {
{ x: Array<Any> ->
it.invoke(x[0], x[1], x[2])
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.openrndr.extra.expressions.typed
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector4
internal fun vec4(x: Any, y: Any, z: Any, w: Any): Vector4 {
require(x is Double && y is Double && z is Double && w is Double)
return Vector4(x, y, z, w)
}
internal fun mat4(x: Any, y: Any, z: Any, w: Any): Matrix44 {
require(x is Vector4 && y is Vector4 && z is Vector4 && w is Vector4)
return Matrix44.fromColumnVectors(x, y, z, w)
}
internal fun rgba(r: Any, g: Any, b: Any, a: Any): ColorRGBa {
require(r is Double && g is Double && b is Double && a is Double)
return ColorRGBa(r, g, b, a)
}
internal fun dispatchFunction4(name: String, functions: Map<String, TypedFunction4>): ((Array<Any>) -> Any)? {
return when (name) {
"vec4" -> { x -> vec4(x[0], x[1], x[2], x[3]) }
"mat4" -> { x -> mat4(x[0], x[1], x[2], x[3]) }
"rgba" -> { x -> rgba(x[0], x[1], x[2], x[3]) }
else -> functions[name]?.let { { x: Array<Any> -> it.invoke(x[0], x[1], x[2], x[3]) } }
}
}

View File

@@ -0,0 +1,11 @@
package org.openrndr.extra.expressions.typed
fun String.memberFunctions(n: String): ((Array<Any>) -> Any)? {
return when (n) {
"take" -> { n -> this.take((n[0] as Number).toInt()) }
"drop" -> { n -> this.drop((n[0] as Number).toInt()) }
"takeLast" -> { n -> this.takeLast((n[0] as Number).toInt()) }
"dropLast" -> { n -> this.takeLast((n[0] as Number).toInt()) }
else -> null
}
}

View File

@@ -0,0 +1,92 @@
package org.openrndr.extra.expressions.typed
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
internal fun String.property(property: String): Any {
return when (property) {
"length" -> this.length.toDouble()
"uppercase" -> this.uppercase()
"lowercase" -> this.lowercase()
"reversed" -> this.reversed()
else -> error("unknown property '$property'")
}
}
internal fun Vector2.property(property: String): Any {
return when (property) {
"x" -> x
"y" -> y
"xx" -> xx
"yx" -> yx
"yy" -> yy
"xy" -> this
"xxx" -> Vector3(x, x, x)
"xxy" -> Vector3(x, x, y)
"length" -> length
"normalized" -> normalized
else -> error("unknown property '$property")
}
}
internal fun Vector3.property(property: String): Any {
return when (property) {
"x" -> x
"y" -> y
"z" -> z
"xx" -> Vector2(x, x)
"yx" -> Vector2(y, x)
"yy" -> Vector2(y, y)
"xy" -> Vector2(x, y)
"zx" -> Vector2(z, x)
"xz" -> Vector2(x, z)
"xxx" -> Vector3(x, x, x)
"xxy" -> Vector3(x, x, y)
"length" -> length
"normalized" -> normalized
else -> error("unknown property '$property")
}
}
internal fun Vector4.property(property: String): Any {
return when (property) {
"x" -> x
"y" -> y
"z" -> z
"xx" -> Vector2(x, x)
"yx" -> Vector2(y, x)
"yy" -> Vector2(y, y)
"xy" -> Vector2(x, y)
"zx" -> Vector2(z, x)
"xz" -> Vector2(x, z)
"xyz" -> Vector3(x, y, z)
"xxy" -> Vector3(x, x, y)
"length" -> length
"normalized" -> normalized
else -> error("unknown property '$property")
}
}
internal fun ColorRGBa.property(property: String): Any {
return when (property) {
"r" -> r
"g" -> g
"b" -> b
"a" -> alpha
"linear" -> toLinear()
"srgb" -> toSRGB()
else -> error("unknown property '$property")
}
}
internal fun Matrix44.property(property: String): Any {
return when (property) {
"inversed" -> inversed
"transposed" -> transposed
else -> error("unknown property '$property")
}
}

View File

@@ -0,0 +1,686 @@
package org.openrndr.extra.expressions.typed
import org.antlr.v4.kotlinruntime.*
import org.antlr.v4.kotlinruntime.tree.ParseTreeWalker
import org.antlr.v4.kotlinruntime.tree.TerminalNode
import org.openrndr.collections.pop
import org.openrndr.collections.push
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.expressions.parser.KeyLangLexer
import org.openrndr.extra.expressions.parser.KeyLangParser
import org.openrndr.extra.expressions.parser.KeyLangParserBaseListener
import org.openrndr.extra.noise.uniform
import org.openrndr.math.*
import kotlin.math.*
typealias TypedFunction0 = () -> Any
typealias TypedFunction1 = (Any) -> Any
typealias TypedFunction2 = (Any, Any) -> Any
typealias TypedFunction3 = (Any, Any, Any) -> Any
typealias TypedFunction4 = (Any, Any, Any, Any) -> Any
typealias TypedFunction5 = (Any, Any, Any, Any, Any) -> Any
private fun ArrayDeque<Any>.pushChecked(item: Any) {
// require(item is Double || item is Vector2 || item is Vector3 || item is Vector4 || item is Map<*, *> || item is Matrix44) {
//
// "$item ${item::class}"
// }
push(item)
}
class TypedFunctionExtensions(
val functions0: Map<String, TypedFunction0> = emptyMap(),
val functions1: Map<String, TypedFunction1> = emptyMap(),
val functions2: Map<String, TypedFunction2> = emptyMap(),
val functions3: Map<String, TypedFunction3> = emptyMap(),
val functions4: Map<String, TypedFunction4> = emptyMap(),
val functions5: Map<String, TypedFunction5> = emptyMap()
) {
companion object {
val EMPTY = TypedFunctionExtensions()
}
}
internal enum class IDType {
VARIABLE,
PROPERTY,
MEMBER_FUNCTION0,
MEMBER_FUNCTION1,
MEMBER_FUNCTION2,
MEMBER_FUNCTION3,
FUNCTION0,
FUNCTION1,
FUNCTION2,
FUNCTION3,
FUNCTION4,
FUNCTION5
}
internal class TypedExpressionListener(
val functions: TypedFunctionExtensions = TypedFunctionExtensions.EMPTY,
val constants: (String) -> Any? = { null }
) :
KeyLangParserBaseListener() {
val valueStack = ArrayDeque<Any>()
val functionStack = ArrayDeque<(Array<Any>) -> Any>()
val propertyStack = ArrayDeque<String>()
val idTypeStack = ArrayDeque<IDType>()
var lastExpressionResult: Any? = null
val exceptionStack = ArrayDeque<ExpressionException>()
override fun exitExpressionStatement(ctx: KeyLangParser.ExpressionStatementContext) {
ifError {
throw ExpressionException("error in evaluation of '${ctx.text}': ${it.message ?: ""}")
}
val result = valueStack.pop()
lastExpressionResult = result
}
override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) {
val op = valueStack.pop()
valueStack.pushChecked(
when (op) {
is Double -> -op
is Vector3 -> -op
is Vector2 -> -op
is Vector4 -> -op
is Matrix44 -> op * -1.0
else -> error("unsupported type")
}
)
}
override fun exitBinaryOperation1(ctx: KeyLangParser.BinaryOperation1Context) {
ifError {
pushError(it.message ?: "")
return
}
val right = valueStack.pop()
val left = valueStack.pop()
val result = when (val operator = ctx.operator?.type) {
KeyLangLexer.Tokens.ASTERISK -> when {
left is Double && right is Double -> left * right
left is Vector2 && right is Vector2 -> left * right
left is Vector2 && right is Double -> left * right
left is Vector3 && right is Vector3 -> left * right
left is Vector3 && right is Double -> left * right
left is Vector4 && right is Vector4 -> left * right
left is Vector4 && right is Double -> left * right
left is Matrix44 && right is Matrix44 -> left * right
left is Matrix44 && right is Vector4 -> left * right
left is Matrix44 && right is Double -> left * right
left is ColorRGBa && right is Double -> left * right
left is String && right is Double -> left.repeat(right.roundToInt())
else -> error("unsupported operands for * operator left:${left::class} right:${right::class}")
}
KeyLangLexer.Tokens.DIVISION -> when {
left is Double && right is Double -> left / right
left is Vector2 && right is Vector2 -> left / right
left is Vector2 && right is Double -> left / right
left is Vector3 && right is Vector3 -> left / right
left is Vector3 && right is Double -> left / right
left is Vector4 && right is Vector4 -> left / right
left is Vector4 && right is Double -> left / right
left is ColorRGBa && right is Double -> left / right
else -> error("unsupported operands for - operator left:${left::class} right:${right::class}")
}
KeyLangLexer.Tokens.PERCENTAGE -> when {
left is Double && right is Double -> left.mod(right)
left is Vector2 && right is Vector2 -> left.mod(right)
left is Vector3 && right is Vector3 -> left.mod(right)
left is Vector4 && right is Vector4 -> left.mod(right)
else -> error("unsupported operands for - operator left:${left::class} right:${right::class}")
}
else -> error("operator '$operator' not implemented")
}
valueStack.pushChecked(result)
}
@Suppress("IMPLICIT_CAST_TO_ANY")
override fun exitBinaryOperation2(ctx: KeyLangParser.BinaryOperation2Context) {
ifError {
pushError(it.message ?: "")
return
}
val right = valueStack.pop()
val left = valueStack.pop()
val result = when (val operator = ctx.operator?.type) {
KeyLangLexer.Tokens.PLUS -> when {
left is Double && right is Double -> left + right
left is Vector2 && right is Vector2 -> left + right
left is Vector3 && right is Vector3 -> left + right
left is Vector4 && right is Vector4 -> left + right
left is Matrix44 && right is Matrix44 -> left + right
left is ColorRGBa && right is ColorRGBa -> left + right
left is String && right is String -> left + right
else -> error("unsupported operands for + operator left:${left::class} right:${right::class}")
}
KeyLangLexer.Tokens.MINUS -> when {
left is Double && right is Double -> left - right
left is Vector2 && right is Vector2 -> left - right
left is Vector3 && right is Vector3 -> left - right
left is Vector4 && right is Vector4 -> left - right
left is Matrix44 && right is Matrix44 -> left - right
left is ColorRGBa && right is ColorRGBa -> left - right
else -> error("unsupported operands for - operator left:${left::class} right:${right::class}")
}
else -> error("operator '$operator' not implemented")
}
valueStack.pushChecked(result)
}
override fun exitJoinOperation(ctx: KeyLangParser.JoinOperationContext) {
val right = (valueStack.pop() as Double).roundToInt()
val left = (valueStack.pop() as Double).roundToInt()
val result = when (val operator = ctx.operator?.type) {
KeyLangLexer.Tokens.AND -> right != 0 && left != 0
KeyLangLexer.Tokens.OR -> right != 0 || left != 0
else -> error("operator '$operator' not implemented")
}
valueStack.pushChecked(if (result) 1.0 else 0.0)
}
override fun exitComparisonOperation(ctx: KeyLangParser.ComparisonOperationContext) {
val right = valueStack.pop()
val left = valueStack.pop()
val result = when (val operator = ctx.operator?.type) {
KeyLangLexer.Tokens.EQ -> when {
left is Double && right is Double -> left == right
left is Vector2 && right is Vector2 -> left == right
left is Vector3 && right is Vector3 -> left == right
left is Vector4 && right is Vector4 -> left == right
left is ColorRGBa && right is ColorRGBa -> left == right
left is String && right is String -> left == right
else -> error("unsupported operands for == operator left:${left::class} right:${right::class}")
}
KeyLangLexer.Tokens.LTEQ -> when {
left is Double && right is Double -> left <= right
else -> error("unsupported operands for <= operator left:${left::class} right:${right::class}")
}
KeyLangLexer.Tokens.LT -> when {
left is Double && right is Double -> left < right
else -> error("unsupported operands for < operator left:${left::class} right:${right::class}")
}
KeyLangLexer.Tokens.GTEQ -> when {
left is Double && right is Double -> left >= right
else -> error("unsupported operands for >= operator left:${left::class} right:${right::class}")
}
KeyLangLexer.Tokens.GT -> when {
left is Double && right is Double -> left > right
else -> error("unsupported operands for > operator left:${left::class} right:${right::class}")
}
else -> error("operator '$operator' not implemented")
}
valueStack.pushChecked(if (result) 1.0 else 0.0)
}
override fun exitNegateExpression(ctx: KeyLangParser.NegateExpressionContext) {
val operand = (valueStack.pop() as Double).roundToInt()
valueStack.pushChecked(if (operand == 0) 1.0 else 0.0)
}
override fun exitTernaryExpression(ctx: KeyLangParser.TernaryExpressionContext) {
val right = valueStack.pop()
val left = valueStack.pop()
val comp = valueStack.pop()
val result = when (comp) {
is Double -> if (comp.roundToInt() != 0) left else right
else -> error("can't compare")
}
valueStack.pushChecked(result)
}
override fun enterValueReference(ctx: KeyLangParser.ValueReferenceContext) {
idTypeStack.push(IDType.VARIABLE)
}
override fun enterMemberFunctionCall0Expression(ctx: KeyLangParser.MemberFunctionCall0ExpressionContext) {
idTypeStack.push(IDType.MEMBER_FUNCTION1)
}
override fun exitMemberFunctionCall0Expression(ctx: KeyLangParser.MemberFunctionCall0ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
valueStack.pushChecked(functionStack.pop().invoke(emptyArray()))
}
override fun enterMemberFunctionCall1Expression(ctx: KeyLangParser.MemberFunctionCall1ExpressionContext) {
idTypeStack.push(IDType.MEMBER_FUNCTION1)
}
override fun exitMemberFunctionCall1Expression(ctx: KeyLangParser.MemberFunctionCall1ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
valueStack.pushChecked(functionStack.pop().invoke(arrayOf(valueStack.pop())))
}
override fun enterMemberFunctionCall2Expression(ctx: KeyLangParser.MemberFunctionCall2ExpressionContext) {
idTypeStack.push(IDType.MEMBER_FUNCTION2)
}
override fun exitMemberFunctionCall2Expression(ctx: KeyLangParser.MemberFunctionCall2ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val argument1 = valueStack.pop()
val argument0 = valueStack.pop()
valueStack.pushChecked(functionStack.pop().invoke(arrayOf(argument0, argument1)))
}
override fun enterMemberFunctionCall3Expression(ctx: KeyLangParser.MemberFunctionCall3ExpressionContext) {
idTypeStack.push(IDType.MEMBER_FUNCTION3)
}
override fun exitMemberFunctionCall3Expression(ctx: KeyLangParser.MemberFunctionCall3ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val argument2 = valueStack.pop()
val argument1 = valueStack.pop()
val argument0 = valueStack.pop()
valueStack.pushChecked(functionStack.pop().invoke(arrayOf(argument0, argument1, argument2)))
}
override fun enterFunctionCall0Expression(ctx: KeyLangParser.FunctionCall0ExpressionContext) {
idTypeStack.push(IDType.FUNCTION0)
}
override fun exitFunctionCall0Expression(ctx: KeyLangParser.FunctionCall0ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val result = function.invoke(arrayOf())
valueStack.pushChecked(result)
}
override fun enterFunctionCall1Expression(ctx: KeyLangParser.FunctionCall1ExpressionContext) {
idTypeStack.push(IDType.FUNCTION1)
}
override fun exitFunctionCall1Expression(ctx: KeyLangParser.FunctionCall1ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument = valueStack.pop()
val result = function.invoke(arrayOf(argument))
valueStack.pushChecked(result)
}
override fun enterFunctionCall2Expression(ctx: KeyLangParser.FunctionCall2ExpressionContext) {
idTypeStack.push(IDType.FUNCTION2)
}
override fun exitFunctionCall2Expression(ctx: KeyLangParser.FunctionCall2ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument1 = valueStack.pop()
val argument0 = valueStack.pop()
val result = function.invoke(arrayOf(argument0, argument1))
valueStack.pushChecked(result)
}
override fun enterFunctionCall3Expression(ctx: KeyLangParser.FunctionCall3ExpressionContext) {
idTypeStack.push(IDType.FUNCTION3)
}
override fun exitFunctionCall3Expression(ctx: KeyLangParser.FunctionCall3ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument2 = valueStack.pop()
val argument1 = valueStack.pop()
val argument0 = valueStack.pop()
val result = function.invoke(arrayOf(argument0, argument1, argument2))
valueStack.pushChecked(result)
}
override fun enterFunctionCall4Expression(ctx: KeyLangParser.FunctionCall4ExpressionContext) {
idTypeStack.push(IDType.FUNCTION4)
}
override fun exitFunctionCall4Expression(ctx: KeyLangParser.FunctionCall4ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument3 = valueStack.pop()
val argument2 = valueStack.pop()
val argument1 = valueStack.pop()
val argument0 = valueStack.pop()
val result = function.invoke(arrayOf(argument0, argument1, argument2, argument3))
valueStack.pushChecked(result)
}
override fun enterFunctionCall5Expression(ctx: KeyLangParser.FunctionCall5ExpressionContext) {
idTypeStack.push(IDType.FUNCTION5)
}
override fun exitFunctionCall5Expression(ctx: KeyLangParser.FunctionCall5ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument4 = valueStack.pop()
val argument3 = valueStack.pop()
val argument2 = valueStack.pop()
val argument1 = valueStack.pop()
val argument0 = valueStack.pop()
val result = function.invoke(arrayOf(argument0, argument1, argument2, argument3, argument4))
valueStack.pushChecked(result)
}
private fun <T> errorValue(message: String, value: T): T {
pushError(message)
return value
}
private fun pushError(message: String) {
exceptionStack.push(ExpressionException(message))
}
private inline fun ifError(f: (e: Throwable) -> Unit) {
if (exceptionStack.isNotEmpty()) {
val e = exceptionStack.pop()
f(e)
}
}
override fun enterPropReference(ctx: KeyLangParser.PropReferenceContext) {
idTypeStack.push(IDType.PROPERTY)
}
override fun exitPropReference(ctx: KeyLangParser.PropReferenceContext) {
val root = valueStack.pop()
var current = root
val property = propertyStack.pop()
@Suppress("UNCHECKED_CAST")
current = when (current) {
is Map<*, *> -> current[property] ?: error("property '$property' not found")
is Function<*> -> (current as ((String) -> Any?)).invoke(property)
?: error("property '$property' not found")
is Vector2 -> current.property(property)
is Vector3 -> current.property(property)
is Vector4 -> current.property(property)
is ColorRGBa -> current.property(property)
is Matrix44 -> current.property(property)
else -> error("can't look up: ${current::class} '$current', root:'$root' ${ctx.text} ")
}
valueStack.push(current)
}
override fun visitTerminal(node: TerminalNode) {
val type = node.symbol.type
if (type == KeyLangParser.Tokens.INTLIT) {
valueStack.pushChecked(node.text.toDouble())
} else if (type == KeyLangParser.Tokens.DECLIT) {
valueStack.pushChecked(node.text.toDouble())
} else if (type == KeyLangParser.Tokens.STRING_CONTENT) {
valueStack.pushChecked(node.text)
} else if (type == KeyLangParser.Tokens.ID) {
val name = node.text.replace("`", "")
@Suppress("DIVISION_BY_ZERO")
when (val idType = idTypeStack.pop()) {
IDType.VARIABLE -> valueStack.pushChecked(
when (name) {
"PI" -> PI
else -> constants(name) ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
}
)
IDType.PROPERTY -> propertyStack.push(name)
IDType.FUNCTION0 -> {
val function: (Array<Any>) -> Any =
when (name) {
"random" -> { _ -> Double.uniform(0.0, 1.0) }
else -> functions.functions0[name]?.let { { _: Array<Any> -> it.invoke() } }
?: errorValue(
"unresolved function: '${name}()'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.MEMBER_FUNCTION0,
IDType.MEMBER_FUNCTION1,
IDType.MEMBER_FUNCTION2,
IDType.MEMBER_FUNCTION3 -> {
val receiver = valueStack.pop()
when (receiver) {
is String -> {
functionStack.push(
receiver.memberFunctions(name)
?: error("no member function '$receiver.$name()'")
)
}
is ColorRGBa -> {
when (idType) {
IDType.MEMBER_FUNCTION1 -> {
functionStack.push(when (name) {
"shade" -> { x -> receiver.shade(x[0] as Double) }
"opacify" -> { x -> receiver.opacify(x[0] as Double) }
else -> error("no member function '$receiver.$name()'")
})
}
else -> error("no member function $idType '$receiver.$name()")
}
}
is Function<*> -> {
@Suppress("UNCHECKED_CAST")
receiver as (String) -> Any
@Suppress("UNCHECKED_CAST") val function =
receiver.invoke(name) ?: error("no such function $name")
when (idType) {
IDType.MEMBER_FUNCTION0 -> {
function as () -> Any
functionStack.push({ function() })
}
IDType.MEMBER_FUNCTION1 -> {
function as (Any) -> Any
functionStack.push({ x -> function(x[0]) })
}
IDType.MEMBER_FUNCTION2 -> {
function as (Any, Any) -> Any
functionStack.push({ x -> function(x[0], x[1]) })
}
IDType.MEMBER_FUNCTION3 -> {
function as (Any, Any, Any) -> Any
functionStack.push({ x -> function(x[0], x[1], x[2]) })
}
else -> error("unreachable")
}
}
else -> error("receiver '${receiver}' not supported")
}
}
IDType.FUNCTION1 -> {
val function: (Array<Any>) -> Any =
dispatchFunction1(name, functions.functions1)
?: errorValue(
"unresolved function: '${name}(x0)'"
) { _ -> error("this is the error function") }
functionStack.push(function)
}
IDType.FUNCTION2 -> {
val function: (Array<Any>) -> Any =
dispatchFunction2(name, functions.functions2)
?: errorValue(
"unresolved function: '${name}(x0, x1)'"
) { _ -> error("this is the error function") }
functionStack.push(function)
}
IDType.FUNCTION3 -> {
val function: (Array<Any>) -> Any =
dispatchFunction3(name, functions.functions3)
?: errorValue(
"unresolved function: '${name}(x0)'"
) { _ -> error("this is the error function") }
functionStack.push(function)
}
IDType.FUNCTION4 -> {
val function: (Array<Any>) -> Any =
dispatchFunction4(name, functions.functions4)
?: errorValue(
"unresolved function: '${name}(x0)'"
) { _ -> error("this is the error function") }
functionStack.push(function)
}
else -> error("unsupported id-type $idType")
}
}
}
}
class ExpressionException(message: String) : RuntimeException(message)
fun evaluateTypedExpression(
expression: String,
constants: (String) -> Any? = { null },
functions: TypedFunctionExtensions = TypedFunctionExtensions.EMPTY
): Any? {
val lexer = KeyLangLexer(CharStreams.fromString(expression))
val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() {
override fun syntaxError(
recognizer: Recognizer<*, *>,
offendingSymbol: Any?,
line: Int,
charPositionInLine: Int,
msg: String,
e: RecognitionException?
) {
throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
}
})
val root = parser.keyLangFile()
val listener = TypedExpressionListener(functions, constants)
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
} catch (e: ExpressionException) {
throw ExpressionException(e.message ?: "")
}
return listener.lastExpressionResult
}
fun compileTypedExpression(
expression: String,
constants: (String) -> Any? = { null },
functions: TypedFunctionExtensions = TypedFunctionExtensions.EMPTY
): () -> Any {
val lexer = KeyLangLexer(CharStreams.fromString(expression))
val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() {
override fun syntaxError(
recognizer: Recognizer<*, *>,
offendingSymbol: Any?,
line: Int,
charPositionInLine: Int,
msg: String,
e: RecognitionException?
) {
throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
}
})
val root = parser.keyLangFile()
val listener = TypedExpressionListener(functions, constants)
return {
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
} catch (e: ExpressionException) {
throw ExpressionException(e.message ?: "")
}
listener.lastExpressionResult ?: error("no result")
}
}
internal fun expressionRoot(expression: String): KeyLangParser.KeyLangFileContext {
val lexer = KeyLangLexer(CharStreams.fromString(expression))
val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() {
override fun syntaxError(
recognizer: Recognizer<*, *>,
offendingSymbol: Any?,
line: Int,
charPositionInLine: Int,
msg: String,
e: RecognitionException?
) {
throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
}
})
return parser.keyLangFile()
}

View File

@@ -13,25 +13,17 @@ class TestExpressionErrors {
val expression = ")("
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"
} `should throw` ExpressionException::class
}
@Test
fun `an expression with equality instead of assign`() {
val expression = "a == 5"
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] ]"
}
@Test
fun `an expression trying to reassign a number`() {
val expression = "3 = 5"
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] ]"
} `should throw` ExpressionException::class
}
@Test