diff --git a/orx-expression-evaluator-typed/README.md b/orx-expression-evaluator-typed/README.md new file mode 100644 index 00000000..83024e8c --- /dev/null +++ b/orx-expression-evaluator-typed/README.md @@ -0,0 +1,13 @@ +# orx-expression-evaluator-typed + +Tools to evaluate strings containing typed mathematical expressions. + +# Expression evaluator + +Supported types: + * `Double` + * `String` + * `Vector2` + * `Vector3` + * `Vector4` + * `ColorRGBa` \ No newline at end of file diff --git a/orx-expression-evaluator-typed/build.gradle.kts b/orx-expression-evaluator-typed/build.gradle.kts new file mode 100644 index 00000000..dc4eca28 --- /dev/null +++ b/orx-expression-evaluator-typed/build.gradle.kts @@ -0,0 +1,31 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinCompile + +plugins { + org.openrndr.extra.convention.`kotlin-multiplatform` +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.antlr.kotlin.runtime) + implementation(libs.openrndr.application) + implementation(libs.openrndr.math) + implementation(libs.kotlin.coroutines) + implementation(project(":orx-property-watchers")) + implementation(project(":orx-noise")) + implementation(project(":orx-expression-evaluator")) + } + } + val jvmDemo by getting { + dependencies { + implementation(project(":orx-jvm:orx-gui")) + } + } + val jvmTest by getting { + dependencies { + implementation(libs.kluent) + } + } + } +} diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/CompiledFunctions.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/CompiledFunctions.kt new file mode 100644 index 00000000..b29493c3 --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/CompiledFunctions.kt @@ -0,0 +1,37 @@ +package org.openrndr.extra.expressions.typed + +import org.antlr.v4.kotlinruntime.tree.ParseTreeWalker +import org.openrndr.extra.expressions.ExpressionException + +fun 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 + } +} diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function1.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function1.kt new file mode 100644 index 00000000..0ef205cd --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function1.kt @@ -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): ((Array) -> 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 -> it.invoke(x[0]) } } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function2.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function2.kt new file mode 100644 index 00000000..5f53a42a --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function2.kt @@ -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): ((Array) -> 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 -> it.invoke(x[0], x[1]) } } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function3.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function3.kt new file mode 100644 index 00000000..d2345144 --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function3.kt @@ -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): ((Array) -> 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 -> + it.invoke(x[0], x[1], x[2]) + } + } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function4.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function4.kt new file mode 100644 index 00000000..122dbb69 --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Function4.kt @@ -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): ((Array) -> 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 -> it.invoke(x[0], x[1], x[2], x[3]) } } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/MemberFunctions.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/MemberFunctions.kt new file mode 100644 index 00000000..e4dd44ee --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/MemberFunctions.kt @@ -0,0 +1,11 @@ +package org.openrndr.extra.expressions.typed + +fun String.memberFunctions(n: String): ((Array) -> 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 + } +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Properties.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Properties.kt new file mode 100644 index 00000000..4782094f --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/Properties.kt @@ -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") + } +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/TypedExpressions.kt b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/TypedExpressions.kt new file mode 100644 index 00000000..bf2bd718 --- /dev/null +++ b/orx-expression-evaluator-typed/src/commonMain/kotlin/typed/TypedExpressions.kt @@ -0,0 +1,685 @@ +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.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 = emptyMap(), + val functions1: Map = emptyMap(), + val functions2: Map = emptyMap(), + val functions3: Map = emptyMap(), + val functions4: Map = emptyMap(), + val functions5: Map = 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() + val functionStack = ArrayDeque<(Array) -> Any>() + val propertyStack = ArrayDeque() + + val idTypeStack = ArrayDeque() + var lastExpressionResult: Any? = null + + val exceptionStack = ArrayDeque() + + 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 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 = + when (name) { + "random" -> { _ -> Double.uniform(0.0, 1.0) } + else -> functions.functions0[name]?.let { { _: Array -> 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 = + dispatchFunction1(name, functions.functions1) + ?: errorValue( + "unresolved function: '${name}(x0)'" + ) { _ -> error("this is the error function") } + functionStack.push(function) + } + + IDType.FUNCTION2 -> { + val function: (Array) -> 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 = + dispatchFunction3(name, functions.functions3) + ?: errorValue( + "unresolved function: '${name}(x0)'" + ) { _ -> error("this is the error function") } + functionStack.push(function) + } + + IDType.FUNCTION4 -> { + val function: (Array) -> 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() +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/jvmTest/kotlin/typed/TestTypedCompiledExpression.kt b/orx-expression-evaluator-typed/src/jvmTest/kotlin/typed/TestTypedCompiledExpression.kt new file mode 100644 index 00000000..91904842 --- /dev/null +++ b/orx-expression-evaluator-typed/src/jvmTest/kotlin/typed/TestTypedCompiledExpression.kt @@ -0,0 +1,64 @@ +package typed + +import org.openrndr.extra.expressions.typed.compileFunction1OrNull +import org.openrndr.extra.noise.uniform +import org.openrndr.math.Vector2 +import kotlin.test.Test +import kotlin.test.assertEquals + +class TestTypedCompiledExpression { + + @Test + fun testStringLiteral() { + run { + val c = compileFunction1OrNull(""""hoi"""", "t")!! + val v = c(0.0) + assertEquals("hoi", v) + } + run { + val c = compileFunction1OrNull(""""hoi" + " " + "doei" * t""", "t")!! + val v = c(2.0) + assertEquals("hoi doeidoei", v) + } + + run { + val c = compileFunction1OrNull(""""hoi".take(t)""", "t")!! + val v = c(2.0) + assertEquals("ho", v) + } + } + + @Test + fun testComparison() { + run { + val c = compileFunction1OrNull("""t == t""", "t")!! + val v = c(0.0) + assertEquals(1.0, v) + } + } + + @Test + fun testFunction1() { + run { + val c = compileFunction1OrNull("x + 3.0", "x")!! + assertEquals(1.0 + 3.0, c(1.0)) + assertEquals(2.0 + 3.0, c(2.0)) + } + run { + val c = compileFunction1OrNull("x.x + x.y", "x")!! + assertEquals(1.0 + 3.0, c(Vector2(1.0, 3.0))) + assertEquals(2.0 + 3.0, c(Vector2(2.0, 3.0))) + } + run { + val c = compileFunction1OrNull("x.x + x.y", "x")!! + val start = System.currentTimeMillis() + for (i in 0 until 1000) { + val r0 = Double.uniform(0.0, 1.0) + val r1 = Double.uniform(0.0, 1.0) + assertEquals(r0 + r1, c(Vector2(r0, r1))) + } + val end = System.currentTimeMillis() + println("that took ${end - start}") + } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator-typed/src/jvmTest/kotlin/typed/TestTypedExpression.kt b/orx-expression-evaluator-typed/src/jvmTest/kotlin/typed/TestTypedExpression.kt new file mode 100644 index 00000000..608744ed --- /dev/null +++ b/orx-expression-evaluator-typed/src/jvmTest/kotlin/typed/TestTypedExpression.kt @@ -0,0 +1,122 @@ +package typed + +import org.junit.jupiter.api.Assertions.assertEquals +import org.openrndr.extra.expressions.typed.evaluateTypedExpression +import org.openrndr.math.Vector2 +import kotlin.test.Test + +class TestTypedExpression { + + @Test + fun testTernary() { + println("result is: ${evaluateTypedExpression("2.0 > 0.5 ? 1.3 : 0.7")}") + } + + @Test + fun testJoin() { + assertEquals(0.0, evaluateTypedExpression("1.0 && 0.0")) + assertEquals(1.0, evaluateTypedExpression("1.0 && 1.0")) + assertEquals(0.0, evaluateTypedExpression("0.0 && 0.0")) + assertEquals(0.0, evaluateTypedExpression("0.0 && 1.0")) + + assertEquals(1.0, evaluateTypedExpression("1.0 || 0.0")) + assertEquals(1.0, evaluateTypedExpression("1.0 || 1.0")) + assertEquals(0.0, evaluateTypedExpression("0.0 || 0.0")) + assertEquals(1.0, evaluateTypedExpression("0.0 || 1.0")) + + assertEquals(1.0, evaluateTypedExpression("(0.0 || 1.0) && (1.0 || 0.0)")) + } + + @Test + fun testNegate() { + assertEquals(0.0, evaluateTypedExpression("!1.0")) + assertEquals(1.0, evaluateTypedExpression("!0.0")) + assertEquals(1.0, evaluateTypedExpression("!!2.0")) + } + + @Test + fun testTyped() { + println(evaluateTypedExpression("vec2(1.0, 1.0) + vec2(1.0, 1.0)")) + println(evaluateTypedExpression("vec3(1.0, 1.0, 1.0) + vec3(2.0, 3.0, 4.0)")) + println(evaluateTypedExpression("vec3(1.0, 1.0, 1.0) * vec3(2.0, 3.0, 4.0)")) + println(evaluateTypedExpression("translate(vec3(1.0, 0.0, 0.0)) * mat4(vec4(1,0,0,0), vec4(0,1,0,0), vec4(0,0,1,0), vec4(0.0, 0.0, 0.0, 1.0))")) + println(evaluateTypedExpression("(translate(vec3(1.0, 0.0, 0.0)) * vec4(0.0, 0.0, 0.0, 1.0)).xyz")) + } + + fun Map.function(): (String) -> Any? { + return fun(p: String): Any? { + val v = this[p] + if (v is Map<*, *>) { + return (v as Map).function() + } else { + return v + } + } + } + + @Test + fun testPropref() { + println(evaluateTypedExpression("a.b.c", constants = mapOf("a" to mapOf("b" to mapOf("c" to 8.0))).function())) + println( + evaluateTypedExpression( + "a.yx.yx.normalized * -5.0", + constants = mapOf("a" to Vector2(1.0, 2.0)).function() + ) + ) + println( + evaluateTypedExpression( + "vec2(2.0, 3.0).normalized", + constants = mapOf("a" to Vector2(1.0, 2.0)).function() + ) + ) + } + + @Test + fun testMethodCall() { + println( + evaluateTypedExpression( + "a.b.c(5.0) + a.b.sum(3.0, 5.0)", + constants = mapOf( + "a" to + mapOf( + "b" to + mapOf( + "c" to { x: Double -> x * 5.0 }, + "sum" to { x: Double, y: Double -> x + y } + ) + ) + ).function() + ) + ) + } + + @Suppress("NAME_SHADOWING") + @Test + fun testMethodCallF() { + println( + evaluateTypedExpression( + "vec2(2.0, 3.0) * (a.b.c(5.0) + a.b.sum(3.0, 5.0))", + constants = { name: String -> + when (name) { + "a" -> { name: String -> + when (name) { + "b" -> { name: String -> + when (name) { + "c" -> { x: Double -> x * 5.0 } + "sum" -> { x: Double, y: Double -> x + y } + else -> null + } + } + + else -> null + } + } + + else -> null + } + } + ) + ) + } + +} \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/antlr/KeyLangLexer.g4 b/orx-expression-evaluator/src/commonMain/antlr/KeyLangLexer.g4 index e56588cb..4a8283a6 100644 --- a/orx-expression-evaluator/src/commonMain/antlr/KeyLangLexer.g4 +++ b/orx-expression-evaluator/src/commonMain/antlr/KeyLangLexer.g4 @@ -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); diff --git a/orx-expression-evaluator/src/commonMain/antlr/KeyLangParser.g4 b/orx-expression-evaluator/src/commonMain/antlr/KeyLangParser.g4 index 166bbf5a..491544ae 100644 --- a/orx-expression-evaluator/src/commonMain/antlr/KeyLangParser.g4 +++ b/orx-expression-evaluator/src/commonMain/antlr/KeyLangParser.g4 @@ -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; \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/CompiledFunctions.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/CompiledFunctions.kt new file mode 100644 index 00000000..b29493c3 --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/CompiledFunctions.kt @@ -0,0 +1,37 @@ +package org.openrndr.extra.expressions.typed + +import org.antlr.v4.kotlinruntime.tree.ParseTreeWalker +import org.openrndr.extra.expressions.ExpressionException + +fun 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 + } +} diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/Function1.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function1.kt new file mode 100644 index 00000000..0ef205cd --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function1.kt @@ -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): ((Array) -> 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 -> it.invoke(x[0]) } } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/Function2.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function2.kt new file mode 100644 index 00000000..5f53a42a --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function2.kt @@ -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): ((Array) -> 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 -> it.invoke(x[0], x[1]) } } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/Function3.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function3.kt new file mode 100644 index 00000000..d2345144 --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function3.kt @@ -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): ((Array) -> 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 -> + it.invoke(x[0], x[1], x[2]) + } + } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/Function4.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function4.kt new file mode 100644 index 00000000..122dbb69 --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/Function4.kt @@ -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): ((Array) -> 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 -> it.invoke(x[0], x[1], x[2], x[3]) } } + } +} \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/MemberFunctions.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/MemberFunctions.kt new file mode 100644 index 00000000..e4dd44ee --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/MemberFunctions.kt @@ -0,0 +1,11 @@ +package org.openrndr.extra.expressions.typed + +fun String.memberFunctions(n: String): ((Array) -> 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 + } +} \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/Properties.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/Properties.kt new file mode 100644 index 00000000..4782094f --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/Properties.kt @@ -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") + } +} \ No newline at end of file diff --git a/orx-expression-evaluator/src/commonMain/kotlin/typed/TypedExpressions.kt b/orx-expression-evaluator/src/commonMain/kotlin/typed/TypedExpressions.kt new file mode 100644 index 00000000..d9fb67c7 --- /dev/null +++ b/orx-expression-evaluator/src/commonMain/kotlin/typed/TypedExpressions.kt @@ -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.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 = emptyMap(), + val functions1: Map = emptyMap(), + val functions2: Map = emptyMap(), + val functions3: Map = emptyMap(), + val functions4: Map = emptyMap(), + val functions5: Map = 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() + val functionStack = ArrayDeque<(Array) -> Any>() + val propertyStack = ArrayDeque() + + val idTypeStack = ArrayDeque() + var lastExpressionResult: Any? = null + + val exceptionStack = ArrayDeque() + + 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 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 = + when (name) { + "random" -> { _ -> Double.uniform(0.0, 1.0) } + else -> functions.functions0[name]?.let { { _: Array -> 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 = + dispatchFunction1(name, functions.functions1) + ?: errorValue( + "unresolved function: '${name}(x0)'" + ) { _ -> error("this is the error function") } + functionStack.push(function) + } + + IDType.FUNCTION2 -> { + val function: (Array) -> 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 = + dispatchFunction3(name, functions.functions3) + ?: errorValue( + "unresolved function: '${name}(x0)'" + ) { _ -> error("this is the error function") } + functionStack.push(function) + } + + IDType.FUNCTION4 -> { + val function: (Array) -> 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() +} + diff --git a/orx-expression-evaluator/src/jvmTest/kotlin/TestExpressionErrors.kt b/orx-expression-evaluator/src/jvmTest/kotlin/TestExpressionErrors.kt index 0367377c..ac3bcbcd 100644 --- a/orx-expression-evaluator/src/jvmTest/kotlin/TestExpressionErrors.kt +++ b/orx-expression-evaluator/src/jvmTest/kotlin/TestExpressionErrors.kt @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index dbbfd50e..08b2cb8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,6 +29,7 @@ include( "orx-easing", "orx-envelopes", "orx-expression-evaluator", + "orx-expression-evaluator-typed", "orx-fcurve", "orx-fft", "orx-jvm:orx-file-watcher",