From e6a5b21e5d00c33c6422636dad597445396c5cc9 Mon Sep 17 00:00:00 2001 From: Zhirkevich Alexander Y Date: Tue, 30 Jul 2024 17:57:41 +0300 Subject: [PATCH] separate scripting --- settings.gradle.kts | 1 + skriptie/build.gradle.kts | 16 + .../ecmascript/EcmascriptInterpreter.kt | 748 ++++++++++++++++++ .../skriptie/ecmascript/Expression.kt | 23 + .../ecmascript/ExpressionInterpreter.kt | 5 + .../skriptie/ecmascript/ExtensionContext.kt | 9 + .../skriptie/ecmascript/GlobalContext.kt | 18 + .../ecmascript/InterpretationContext.kt | 47 ++ .../skriptie/ecmascript/Script.kt | 8 + .../skriptie/ecmascript/ScriptContext.kt | 85 ++ .../skriptie/ecmascript/ScriptEngine.kt | 17 + .../skriptie/ecmascript/Undefined.kt | 7 + .../operations/LazyConstExpression.kt | 24 + .../skriptie/ecmascript/operations/Object.kt | 39 + .../skriptie/ecmascript/operations/OpUtil.kt | 38 + .../ecmascript/operations/keywords/OpBlock.kt | 39 + .../operations/keywords/OpBooleans.kt | 19 + .../operations/keywords/OpEquals.kt | 23 + .../operations/keywords/OpFunction.kt | 53 ++ .../operations/keywords/OpIfCondition.kt | 27 + .../ecmascript/operations/keywords/OpLoops.kt | 88 +++ .../operations/keywords/OpReturn.kt | 8 + .../operations/keywords/OpTryCatch.kt | 70 ++ .../ecmascript/operations/random/OpNoise.kt | 17 + .../operations/random/OpRandomNumber.kt | 38 + .../operations/random/OpSetRandomSeed.kt | 16 + .../ecmascript/operations/random/OpSmooth.kt | 88 +++ .../operations/random/OpTemporalWiggle.kt | 52 ++ .../ecmascript/operations/random/OpWiggle.kt | 173 ++++ .../ecmascript/operations/value/OpAssign.kt | 35 + .../operations/value/OpAssignByIndex.kt | 63 ++ .../ecmascript/operations/value/OpCompare.kt | 40 + .../ecmascript/operations/value/OpConstant.kt | 7 + .../operations/value/OpGetVariable.kt | 21 + .../ecmascript/operations/value/OpIndex.kt | 19 + .../operations/value/OpMakeArray.kt | 11 + .../ecmascript/operations/value/OpVar.kt | 21 + .../ecmascript/operations/value/ScriptUtil.kt | 288 +++++++ .../alexzhirkevich/skriptie/javascript/JS.kt | 27 + .../skriptie/javascript/JSScriptContext.kt | 10 + .../skriptie/javascript/JsExtensionContext.kt | 40 + .../skriptie/javascript/JsGlobalContext.kt | 216 +++++ .../skriptie/javascript/iterable/JsIndexOf.kt | 37 + .../skriptie/javascript/math/JsMath.kt | 203 +++++ .../javascript/number/JsNumberContext.kt | 28 + .../javascript/number/JsToPrecision.kt | 61 ++ .../skriptie/javascript/string/JsString.kt | 158 ++++ .../javascript/string/JsStringContext.kt | 136 ++++ .../commonTest/kotlin/js/AddExpressionTest.kt | 93 +++ .../kotlin/js/AssignExpressionTest.kt | 26 + .../kotlin/js/BooleanExpressionsTest.kt | 60 ++ .../kotlin/js/ConditionExpressionTest.kt | 39 + .../kotlin/js/CustomFunctionsTest.kt | 78 ++ .../commonTest/kotlin/js/DivExpressionTest.kt | 59 ++ .../src/commonTest/kotlin/js/JsTestUtil.kt | 24 + .../kotlin/js/LoopExpressionsTest.kt | 35 + .../kotlin/js/MathExpressionTest.kt | 40 + .../commonTest/kotlin/js/ModExpressionTest.kt | 15 + .../commonTest/kotlin/js/MulExpressionTest.kt | 42 + .../commonTest/kotlin/js/NumberFormatTest.kt | 18 + .../commonTest/kotlin/js/SubExpressionTest.kt | 62 ++ .../kotlin/js/SyntaxExpressionsTest.kt | 95 +++ 62 files changed, 3903 insertions(+) create mode 100644 skriptie/build.gradle.kts create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/EcmascriptInterpreter.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Expression.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExpressionInterpreter.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExtensionContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/GlobalContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/InterpretationContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Script.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptEngine.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Undefined.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/LazyConstExpression.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/Object.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/OpUtil.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBlock.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBooleans.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpEquals.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpFunction.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpIfCondition.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpLoops.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpReturn.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpTryCatch.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpNoise.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpRandomNumber.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSetRandomSeed.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSmooth.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpTemporalWiggle.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpWiggle.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssign.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssignByIndex.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpCompare.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpConstant.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpGetVariable.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpIndex.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpMakeArray.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpVar.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/ScriptUtil.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JS.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSScriptContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsExtensionContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsGlobalContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/iterable/JsIndexOf.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/math/JsMath.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsNumberContext.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsToPrecision.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsString.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsStringContext.kt create mode 100644 skriptie/src/commonTest/kotlin/js/AddExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/AssignExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/BooleanExpressionsTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/ConditionExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/CustomFunctionsTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/DivExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/JsTestUtil.kt create mode 100644 skriptie/src/commonTest/kotlin/js/LoopExpressionsTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/MathExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/ModExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/MulExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/NumberFormatTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/SubExpressionTest.kt create mode 100644 skriptie/src/commonTest/kotlin/js/SyntaxExpressionsTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d123b78..1cf26ab9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ include(":compottie") include(":compottie-dot") include(":compottie-network") include(":compottie-resources") +include(":skriptie") include(":example:desktopApp") include(":example:webApp") include(":example:androidapp") diff --git a/skriptie/build.gradle.kts b/skriptie/build.gradle.kts new file mode 100644 index 00000000..1b720b77 --- /dev/null +++ b/skriptie/build.gradle.kts @@ -0,0 +1,16 @@ + +plugins { + id("module.android") + id("module.multiplatform") +} + +kotlin { + + sourceSets { + commonMain.dependencies { + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/EcmascriptInterpreter.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/EcmascriptInterpreter.kt new file mode 100644 index 00000000..56d83539 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/EcmascriptInterpreter.kt @@ -0,0 +1,748 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.FunctionParam +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpBlock +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpBoolean +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpDoWhileLoop +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpEquals +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpFunction +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpFunctionExec +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpIfCondition +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpNot +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpReturn +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpTryCatch +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpWhileLoop +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpAssign +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpAssignByIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpCompare +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpConstant +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpEqualsComparator +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpGreaterComparator +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpLessComparator +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpMakeArray +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpVar +import io.github.alexzhirkevich.skriptie.ecmascript.ExtensionContext +import io.github.alexzhirkevich.skriptie.ecmascript.GlobalContext +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.Delegate +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.OpGetVariable +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +internal val EXPR_DEBUG_PRINT_ENABLED = false + +internal enum class LogicalContext { + And, Or, Compare +} + + +internal class EcmascriptInterpreter( + private val expr : String, + private val scriptContext: C, + private val globalContext : GlobalContext, + private val extensionContext : ExtensionContext +) : ExpressionInterpreter { + + private var pos = -1 + private var ch: Char = ' ' + + override fun interpret(): Expression { + val expressions = buildList { + pos = -1 + ch = ' ' + if (EXPR_DEBUG_PRINT_ENABLED) { + println("Parsing $expr") + } + nextChar() + do { + while (eat(';')) { + } + if (pos >= expr.length) { + break + } + +// x = parseAssignment(if (x is InterpretationContext) x else globalContext) + add(parseAssignment(globalContext)) + } while (pos < expr.length) + + require(pos <= expr.length) { + "Unexpected Lottie expression $expr" + } + } + return OpBlock( + expressions = expressions, + scoped = false + ).also { + pos = -1 + ch = ' ' + if (EXPR_DEBUG_PRINT_ENABLED) { + println("Expression parsed: $expr") + } + } + } + + private fun prepareNextChar(){ + while (ch.skip() && pos < expr.length){ + nextChar() + } + } + + private fun nextChar() { + ch = if (++pos < expr.length) expr[pos] else ' ' + } + + private fun prevChar() { + ch = if (--pos > 0 && pos < expr.length) expr[pos] else ' ' + } + + private fun Char.skip() : Boolean = this == ' ' || this == '\n' + + private fun eat(charToEat: Char): Boolean { + while (ch.skip() && pos < expr.length) + nextChar() + + if (ch == charToEat) { + nextChar() + return true + } + return false + } + + private fun nextCharIs(condition: (Char) -> Boolean): Boolean { + var i = pos + + while (i < expr.length) { + if (condition(expr[i])) + return true + if (expr[i].skip()) + i++ + else return false + } + return false + } + + private fun eatSequence(seq : String): Boolean { + + val p = pos + val c = ch + + if (seq.isEmpty()) + return true + + if (!eat(seq[0])) { + return false + } + + return if (expr.indexOf(seq, startIndex = pos-1) == pos-1){ + pos += seq.length -1 + ch = expr[pos.coerceIn(expr.indices)] + true + } else { + pos = p + ch = c + false + } + } + + private fun nextSequenceIs(seq : String): Boolean { + + val p = pos + val c = ch + + if (seq.isEmpty()) + return true + + if (!eat(seq[0])) { + return false + } + + return if (expr.indexOf(seq, startIndex = pos-1) == pos-1){ + pos = p + ch = c + true + } else { + pos = p + ch = c + false + } + } + + private fun parseAssignment(context: Expression): Expression { + var x = parseExpressionOp(context) + if (EXPR_DEBUG_PRINT_ENABLED){ + println("Parsing assignment for $x") + } + while (true) { + prepareNextChar() + x = when { + eatSequence("+=") -> parseAssignmentValue(x, globalContext::sum) + eatSequence("-=") -> parseAssignmentValue(x, globalContext::sub) + eatSequence("*=") -> parseAssignmentValue(x, globalContext::mul) + eatSequence("/=") -> parseAssignmentValue(x, globalContext::div) + eatSequence("%=") -> parseAssignmentValue(x, globalContext::mod) + eat('=') -> parseAssignmentValue(x, null) + x.isAssignable() && eatSequence("++") -> Delegate(x, globalContext::inc) + x.isAssignable() && eatSequence("--") -> Delegate(x, globalContext::dec) + else -> return x + } + } + } + + private fun parseAssignmentValue(x : Expression, merge : ((Any, Any) -> Any)? = null) = when { + x is OpIndex && x.variable is OpGetVariable -> OpAssignByIndex( + variableName = x.variable.name, + scope = x.variable.assignmentType, + index = x.index, + assignableValue = parseAssignment(globalContext), + merge = merge + ).also { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("parsing assignment with index for ${x.variable.name}") + } + } + + x is OpGetVariable -> OpAssign( + variableName = x.name, + assignableValue = parseAssignment(globalContext), + type = x.assignmentType, + merge = merge + ).also { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("parsing assignment for ${x.name} in ${it.type} scope") + } + } + + else -> error("Invalid assignment") + } + + private fun parseExpressionOp(context: Expression, logicalContext: LogicalContext? = null): Expression { + var x = parseTermOp(context) + while (true) { + prepareNextChar() + x = when { + logicalContext != LogicalContext.Compare && eatSequence("&&") -> + OpBoolean(parseExpressionOp(globalContext, LogicalContext.And),x, Boolean::and) + logicalContext == null && eatSequence("||") -> + OpBoolean(parseExpressionOp(globalContext, LogicalContext.Or),x, Boolean::or) + eatSequence("<=") -> OpCompare(x, parseExpressionOp(globalContext, LogicalContext.Compare)) { a, b -> + OpLessComparator(a, b) || OpEqualsComparator(a, b) + } + eatSequence("<") -> OpCompare(x, parseExpressionOp(globalContext, LogicalContext.Compare), OpLessComparator) + eatSequence(">=") -> OpCompare(x, parseExpressionOp(globalContext, LogicalContext.Compare)) { a, b -> + OpGreaterComparator(a, b) || OpEqualsComparator(a, b) + } + eatSequence(">") -> OpCompare(x, parseExpressionOp(globalContext, LogicalContext.Compare), OpGreaterComparator) + eatSequence("===") -> OpEquals(x, parseExpressionOp(globalContext, LogicalContext.Compare), true) + eatSequence("==") -> OpEquals(x, parseExpressionOp(globalContext, LogicalContext.Compare), false) + eatSequence("!==") -> OpNot(OpEquals(x, parseExpressionOp(globalContext, LogicalContext.Compare), false)) + eatSequence("!=") -> OpNot(OpEquals(x, parseExpressionOp(globalContext, LogicalContext.Compare), true)) + !nextSequenceIs("++") && !nextSequenceIs("+=") && eat('+') -> + Delegate(x, parseTermOp(globalContext), globalContext::sum) + !nextSequenceIs("--") && !nextSequenceIs("-=") && eat('-') -> + Delegate(x, parseTermOp(globalContext),globalContext::sub) + else -> return x + } + } + } + + private fun parseTermOp(context: Expression): Expression { + var x = parseFactorOp(context) + while (true) { + prepareNextChar() + x = when { + !nextSequenceIs("*=") && eat('*') -> Delegate( + x, + parseFactorOp(globalContext), + globalContext::mul + ) + + !nextSequenceIs("/=") && eat('/') -> Delegate( + x, + parseFactorOp(globalContext), + globalContext::div + ) + + !nextSequenceIs("%=") && eat('%') -> Delegate( + x, + parseFactorOp(globalContext), + globalContext::mod + ) + + else -> return x + } + } + } + + private fun parseFactorOp(context: Expression): Expression { + val parsedOp = when { + context is GlobalContext && eatSequence("++") -> { + val start = pos + val variable = parseFactorOp(globalContext) + require(variable.isAssignable()){ + "Unexpected '++' as $start" + } + Delegate(variable, globalContext::inc) + } + + context is GlobalContext && eatSequence("--") -> { + val start = pos + val variable = parseFactorOp(globalContext) + require(variable.isAssignable()){ + "Unexpected '--' as $start" + } + Delegate(variable, globalContext::dec) + } + + context is GlobalContext && eat('+') -> + Delegate(parseFactorOp(context)) { it } + + context is GlobalContext && eat('-') -> + Delegate(parseFactorOp(context), globalContext::neg) + + context is GlobalContext && !nextSequenceIs("!=") && eat('!') -> + OpNot(parseExpressionOp(context)) + + context is GlobalContext && eat('(') -> { + parseExpressionOp(context).also { + require(eat(')')) { + "Bad expression: Missing ')'" + } + } + } + + context is GlobalContext && nextCharIs { it.isDigit() || it == '.' } -> { + if (EXPR_DEBUG_PRINT_ENABLED) { + print("making const number... ") + } + var numberFormat = NumberFormat.Dec + var isFloat = nextCharIs { it == '.' } + val startPos = pos + do { + nextChar() + when(ch.lowercaseChar()){ + '.' -> { + if (isFloat) { + break + } + isFloat = true + } + NumberFormat.Hex.prefix -> { + check(numberFormat == NumberFormat.Dec && !isFloat) { + "Invalid number at pos $startPos" + } + numberFormat = NumberFormat.Hex + } + NumberFormat.Oct.prefix -> { + check(numberFormat == NumberFormat.Dec && !isFloat) { + "Invalid number at pos $startPos" + } + numberFormat = NumberFormat.Oct + } + NumberFormat.Bin.prefix -> { + if (numberFormat == NumberFormat.Hex) { + continue + } + check(numberFormat == NumberFormat.Dec && !isFloat) { + "Invalid number at pos $startPos" + } + numberFormat = NumberFormat.Bin + } + } + } while (ch.lowercaseChar().let { + it in numberFormat.alphabet || it in NumberFormatIndicators + }) + + val num = expr.substring(startPos, pos).let { + if (it.endsWith('.')) { + prevChar() + isFloat = false + } + if (isFloat) { + it.toDouble() + } + else { + it.trimEnd('.') + .let { n -> numberFormat.prefix?.let(n::substringAfter) ?: n } + .toULong(numberFormat.radix) + .toLong() + } + } + if (EXPR_DEBUG_PRINT_ENABLED) { + println(num) + } + OpConstant(num) + } + + context is GlobalContext && nextCharIs('\''::equals) || nextCharIs('"'::equals) -> { + if (EXPR_DEBUG_PRINT_ENABLED) { + print("making const string... ") + } + val c = ch + val startPos = pos + do { + nextChar() + } while (!eat(c)) + val str = expr.substring(startPos, pos).drop(1).dropLast(1) + if (EXPR_DEBUG_PRINT_ENABLED) { + println(str) + } + OpConstant(str) + } + + context is GlobalContext && eat('[') -> { // make array + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making array... ") + } + val arrayArgs = buildList { + do { + if (eat(']')) { // empty list + return@buildList + } + add(parseExpressionOp(context)) + } while (eat(',')) + require(eat(']')) { + "Bad expression: missing ]" + } + } + OpMakeArray(arrayArgs) + } + + context !is GlobalContext && eat('[') -> { // index + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making index... ") + } + OpIndex(context, parseExpressionOp(globalContext)).also { + require(eat(']')) { + "Bad expression: Missing ']'" + } + } + } + + + ch.isFun() -> { + + val startPos = pos + do { + nextChar() + } while ( + pos < expr.length && ch.isFun() && !(isReserved(expr.substring(startPos, pos)) && ch == ' ') + ) + + val func = expr.substring(startPos, pos).trim() + + parseFunction(context, func) + } + + else -> error("Unsupported Lottie expression: $expr") + } + + return parsedOp.finish() + } + + private fun Expression.finish() : Expression { + return when { + // inplace function invocation + this is InterpretationContext<*> && nextCharIs { it == '(' } -> { + parseFunction(this, null) + } + // begin condition || property || index + this is OpVar<*> + || eat('.') + || nextCharIs('['::equals) -> + parseFactorOp(this) // continue with receiver + + else -> this + } + } + + private fun parseFunctionArgs(name : String?): List>? { + + if (!nextCharIs('('::equals)){ + return null + } + return buildList { + when { + eat('(') -> { + if (eat(')')){ + return@buildList //empty args + } + do { + add(parseAssignment(globalContext)) + } while (eat(',')) + + require(eat(')')) { + "Bad expression:Missing ')' after argument to $name" + } + } + } + } + } + + private fun parseFunction(context: Expression, func : String?) : Expression { + + return when (func) { + "var", "let", "const" -> { + OpVar( + when (func) { + "var" -> VariableType.Var + "let" -> VariableType.Let + else -> VariableType.Const + } + ) + } + "true" -> OpConstant(true) + "false" -> OpConstant(false) + "function" -> parseFunctionDefinition() + "while" -> { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making while loop") + } + + OpWhileLoop( + condition = parseWhileCondition(), + body = parseBlock() + ) + } + "do" -> { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making do/while loop") + } + + val body = parseBlock() + + check(body is OpBlock){ + "Invalid do/while syntax" + } + + check(eatSequence("while")){ + "Missing while condition in do/while block" + } + val condition = parseWhileCondition() + + OpDoWhileLoop( + condition = condition, + body = body + ) + } + + "if" -> { + + if (EXPR_DEBUG_PRINT_ENABLED) { + print("parsing if...") + } + + val onTrue = parseBlock() + + val onFalse = if (eatSequence("else")) { + parseBlock() + } else null + + OpIfCondition( + condition = parseExpressionOp(globalContext), + onTrue = onTrue, + onFalse = onFalse + ) + } + + "return" -> { + val expr = parseExpressionOp(globalContext) + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making return with $expr") + } + OpReturn(expr) + } + "try" -> { + val tryBlock = parseBlock(requireBlock = true) + val catchBlock = if (eatSequence("catch")) { + + if (eat('(')) { + val start = pos + while (!eat(')') && pos < expr.length) { + //nothing + } + expr.substring(start, pos).trim() to parseBlock( + scoped = false, + requireBlock = true + ) + } else { + null to parseBlock(requireBlock = true) + } + } + else null + + val finallyBlock = if (eatSequence("finally")){ + parseBlock(requireBlock = true) + } else null + + OpTryCatch( + tryBlock = tryBlock, + catchVariableName = catchBlock?.first, + catchBlock = catchBlock?.second, + finallyBlock = finallyBlock + ) + } + + else -> { + val args = parseFunctionArgs(func) + + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making fun $func") + } + + return when (context) { + is InterpretationContext -> context.interpret(func, args) + ?: (if (args != null && func != null && this.scriptContext.getVariable(func) is OpFunction<*>) { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("parsed call for defined function $func") + } + OpFunctionExec(func, args) + } else null) + ?: unresolvedReference( + ref = func ?: "null", + obj = context::class.simpleName + ?.substringAfter("Op") + ?.substringBefore("Context") + ) + + else -> extensionContext.interpret(context, func,args) + ?: unresolvedReference(func ?: "null") + } + } + } + } + + private fun parseWhileCondition(): Expression { + check(eat('(')){ + "Missing while loop condition" + } + + val condition = parseExpressionOp(globalContext) + + check(eat(')')){ + "Missing closing ')' in loop condition" + } + return condition + } + + private fun parseFunctionDefinition() : Expression { + val start = pos + + while (ch != '(') { + nextChar() + } + + val name = expr.substring(start, pos).trim() + + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making defined function $name") + } + + val args = parseFunctionArgs(name).let { args -> + args?.map { + when (it) { + is OpGetVariable -> FunctionParam(name = it.name, default = null) + is OpAssign -> FunctionParam( + name = it.variableName, + default = it.assignableValue + ) + + else -> error("Invalid function declaration at $start") + } + } + } + + checkNotNull(args){ + "Missing function args" + } + + + check(nextCharIs('{'::equals)) { + "Missing function body at $pos" + } + + + val block = parseBlock( + scoped = false // function scope will be used + ) + + this.scriptContext.setVariable( + name = name, + value = OpFunction( + name = name, + parameters = args, + body = block + ), + type = VariableType.Const + ) + if (EXPR_DEBUG_PRINT_ENABLED) { + println("registered function $name") + } + return OpConstant(Undefined) + } + + private fun parseBlock(scoped : Boolean = true, requireBlock : Boolean = false): Expression { + val list = buildList { + if (eat('{')) { + while (!eat('}') && pos < expr.length) { + add(parseAssignment(globalContext)) + eat(';') + } + } else { + if (requireBlock){ + error("Unexpected token at $pos: block start was expected") + } + add(parseAssignment(globalContext)) + } + } + return OpBlock(list, scoped) + } +} + + +@OptIn(ExperimentalContracts::class) +internal fun checkArgsNotNull(args : List<*>?, func : String) { + contract { + returns() implies (args != null) + } + checkNotNull(args){ + "$func call was missing" + } +} + +@OptIn(ExperimentalContracts::class) +internal fun checkArgs(args : List<*>?, count : Int, func : String) { + contract { + returns() implies (args != null) + } + checkNotNull(args){ + "$func call was missing" + } + require(args.size == count){ + "$func takes $count arguments, but ${args.size} got" + } +} + +private val funMap = (('a'..'z').toList() + ('A'..'Z').toList() + '$' + '_' ).associateBy { it } + +private fun Char.isFun() = isDigit() || funMap[this] != null + + +private enum class NumberFormat( + val radix : Int, + val alphabet : String, + val prefix : Char? +) { + Dec(10, ".0123456789", null), + Hex(16, "0123456789abcdef", 'x'), + Oct(8, "01234567", 'o'), + Bin(2, "01", 'b') +} + +private val NumberFormatIndicators = NumberFormat.entries.mapNotNull { it.prefix } + +private val reservedKeywords = setOf( + "function","return","do","while","for" +) + +private fun isReserved(keyword : String) = keyword in reservedKeywords diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Expression.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Expression.kt new file mode 100644 index 00000000..2ce07cc3 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Expression.kt @@ -0,0 +1,23 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpIndex +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.OpGetVariable + +public interface Expression { + + public operator fun invoke(context: C) : Any +} + +internal fun Expression( + block : (C) -> Any +) : Expression = object : Expression { + + override fun invoke(context: C): Any { + return block(context) + } +} + +internal fun Expression<*>.isAssignable() : Boolean { + return this is OpGetVariable && assignmentType == null || + this is OpIndex && variable is OpGetVariable +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExpressionInterpreter.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExpressionInterpreter.kt new file mode 100644 index 00000000..e55389a5 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExpressionInterpreter.kt @@ -0,0 +1,5 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions + +internal interface ExpressionInterpreter { + fun interpret() : Expression +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExtensionContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExtensionContext.kt new file mode 100644 index 00000000..a68ec726 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ExtensionContext.kt @@ -0,0 +1,9 @@ +package io.github.alexzhirkevich.skriptie.ecmascript + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +public interface ExtensionContext { + + public fun interpret(parent: Expression, op: String?, args: List>?): Expression? +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/GlobalContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/GlobalContext.kt new file mode 100644 index 00000000..e1027f71 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/GlobalContext.kt @@ -0,0 +1,18 @@ +package io.github.alexzhirkevich.skriptie.ecmascript + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.InterpretationContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +public interface GlobalContext : InterpretationContext { + + public fun sum(a : Any, b : Any) : Any + public fun sub(a : Any, b : Any) : Any + public fun mul(a : Any, b : Any) : Any + public fun div(a : Any, b : Any) : Any + public fun mod(a : Any, b : Any) : Any + + public fun inc(a : Any) : Any + public fun dec(a : Any) : Any + + public fun neg(a : Any) : Any +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/InterpretationContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/InterpretationContext.kt new file mode 100644 index 00000000..8fc91d25 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/InterpretationContext.kt @@ -0,0 +1,47 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpAssign + + +public interface InterpretationContext : Expression { + + override fun invoke(context: C): Any = this + + public fun interpret(callable: String?, args: List>?): Expression? +} + +internal fun List>.argForNameOrIndex( + index : Int, + vararg name : String, +) : Expression? { + + forEach { op -> + if (op is OpAssign && name.any { op.variableName == it }) { + return op.assignableValue + } + } + + return argAtOrNull(index) +} + +internal fun List>.argAt( + index : Int, +) : Expression { + + return get(index).let { + if (it is OpAssign) + it.assignableValue + else it + } +} + +internal fun List>.argAtOrNull( + index : Int, +) : Expression? { + + return getOrNull(index).let { + if (it is OpAssign) + it.assignableValue + else it + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Script.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Script.kt new file mode 100644 index 00000000..3f27bc37 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Script.kt @@ -0,0 +1,8 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions + +import io.github.alexzhirkevich.skriptie.ecmascript.ScriptEngine + +public fun interface Script { + public operator fun invoke(engine: ScriptEngine): Any +} + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptContext.kt new file mode 100644 index 00000000..abbedf7b --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptContext.kt @@ -0,0 +1,85 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference + + +public enum class VariableType { + Var, Let, Const +} + +public interface ScriptContext { + + + public fun getVariable(name: String): Any? + + public fun setVariable(name: String, value: Any, type: VariableType?) + + public fun withScope( + extraVariables: Map> = emptyMap(), + block : (ScriptContext) -> Any + ) : Any + + public fun reset() +} + +private class BlockScriptContext( + private val parent : ScriptContext +) : BaseScriptContext() { + + override fun getVariable(name: String): Any? { + return if (name in variables) { + super.getVariable(name) + } else { + parent.getVariable(name) + } + } + + override fun setVariable(name: String, value: Any, type: VariableType?) { + when { + type == VariableType.Var -> parent.setVariable(name, value, type) + type != null || name in variables -> super.setVariable(name, value, type) + else -> parent.setVariable(name, value, type) + } + } +} + +public abstract class BaseScriptContext : ScriptContext { + + protected val variables: MutableMap> = mutableMapOf() + + private val child by lazy { + BlockScriptContext(this) + } + + override fun setVariable(name: String, value: Any, type: VariableType?) { + if (type == null && name !in variables) { + unresolvedReference(name) + } + if (type != null && name in variables) { + error("Identifier '$name' is already declared") + } + if (type == null && variables[name]?.first == VariableType.Const) { + error("TypeError: Assignment to constant variable ('$name')") + } + variables[name] = (type ?: variables[name]?.first)!! to value + } + + override fun getVariable(name: String): Any? { + return variables[name]?.second + } + + final override fun withScope( + extraVariables: Map>, + block: (ScriptContext) -> Any + ) : Any { + child.reset() + extraVariables.forEach { (n, v) -> + child.setVariable(n, v.second, v.first) + } + return block(child) + } + + override fun reset(){ + variables.clear() + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptEngine.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptEngine.kt new file mode 100644 index 00000000..50db3ee7 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ScriptEngine.kt @@ -0,0 +1,17 @@ +package io.github.alexzhirkevich.skriptie.ecmascript + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Script +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +public interface ScriptEngine { + + public val context : C + + public fun compile(script : String) : Script + + public fun reset() +} + +public fun ScriptEngine.invoke(script: String) : Any { + return compile(script).invoke(this) +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Undefined.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Undefined.kt new file mode 100644 index 00000000..b3858802 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Undefined.kt @@ -0,0 +1,7 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpConstant + +internal data object Undefined + +internal val OpUndefined = OpConstant(Undefined) diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/LazyConstExpression.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/LazyConstExpression.kt new file mode 100644 index 00000000..8b43ce6e --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/LazyConstExpression.kt @@ -0,0 +1,24 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression + +internal class LazyConstExpression( + val init: Expression +) : Expression { + + private var value : Any? = null + private var initialized : Boolean = false + + override fun invoke( + context: ScriptContext + ): Any { + if (initialized){ + return value!! + } + + value = init(context) + initialized = true + return value!! + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/Object.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/Object.kt new file mode 100644 index 00000000..3d0a8592 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/Object.kt @@ -0,0 +1,39 @@ +//package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations +// +//import io.github.alexzhirkevich.compottie.internal.AnimationState +//import io.github.alexzhirkevich.compottie.internal.animation.RawProperty +//import io.github.alexzhirkevich.compottie.internal.animation.expressions.EvaluationContext +//import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +//import io.github.alexzhirkevich.compottie.internal.animation.expressions.ExpressionContext +//import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +// +//internal class Object( +// private val properties : Map +//) : ExpressionContext { +// +// private val obj = mutableMapOf() +// +// override fun interpret(callable: String?, args: List?): Expression? { +// return when { +// callable == null -> null +// args == null -> field(callable) +// else -> func(callable, args) +// } +// } +// +// override fun invoke( +// property: RawProperty, +// context: EvaluationContext, +// state: AnimationState +// ): Any = this +// +// private fun field(name: String): Expression { +// return Expression { _, _, _ -> obj[name] ?: Undefined } +// } +// +// private fun func(name: String, args: List): Expression { +// return Expression { property, context, state -> +// obj[name] +// } +// } +//} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/OpUtil.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/OpUtil.kt new file mode 100644 index 00000000..452f0967 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/OpUtil.kt @@ -0,0 +1,38 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +internal fun unresolvedReference(ref : String, obj : String? = null) : Nothing = + if (obj != null) + error("Unresolved reference '$ref' for $obj") + else error("Unresolved reference: $ref") + + +internal fun Expression.cast(block: (T) -> R) : Expression = + Expression { block(invoke(it) as T) } + +internal fun Expression.withCast(block: T.( + context: C, +) -> R) : Expression = Expression { + block(invoke(it) as T, it) +} + +internal operator fun Any.get(index : Int) : Any { + return checkNotNull(tryGet(index)){ + "Index $index out of bounds of $this length" + } +} + +internal fun Any.tryGet(index : Int) : Any? { + return when (this){ + is Map<*,*> -> { + (this as Map).get(index) + } + is List<*> -> this.getOrNull(index) + is Array<*> -> this.getOrNull(index) + is CharSequence -> this.getOrNull(index) + else -> error("Can't get value by index from $this") + }!! +} + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBlock.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBlock.kt new file mode 100644 index 00000000..1b3587b4 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBlock.kt @@ -0,0 +1,39 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined + +internal class OpBlock( + val expressions: List>, + private val scoped : Boolean, +) : Expression { + + override fun invoke(context: C): Any { + return if (scoped) { + context.withScope { + invokeInternal(it as C) + } + } else { + invokeInternal(context) + } + } + + private fun invokeInternal(context: C): Any { + if (expressions.isEmpty()) { + return Undefined + } + + if (expressions.size > 1) { + repeat(expressions.size - 1) { + val expr = expressions[it] + val res = expr(context) + + if (expr is OpReturn) { + return res + } + } + } + return expressions.last().invoke(context) + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBooleans.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBooleans.kt new file mode 100644 index 00000000..4579d384 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpBooleans.kt @@ -0,0 +1,19 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +internal fun OpNot( + condition : Expression +) = Expression{ + !(condition(it) as Boolean) +} + +internal fun OpBoolean( + a : Expression, + b : Expression, + op : (Boolean, Boolean) -> Boolean, +) = Expression { + op(!a(it).isFalse(), !b(it).isFalse()) +} + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpEquals.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpEquals.kt new file mode 100644 index 00000000..3d16dc68 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpEquals.kt @@ -0,0 +1,23 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +internal fun OpEquals( + a : Expression, + b : Expression, + isTyped : Boolean +) = Expression { + OpEqualsImpl(a(it), b(it), isTyped) +} + +internal fun OpEqualsImpl(a : Any, b : Any, typed : Boolean) : Boolean { + + return when { + a is Number && b is Number -> a.toDouble() == b.toDouble() + typed || a::class == b::class -> a == b + a is String && b is Number -> a.toDoubleOrNull() == b.toDouble() + b is String && a is Number -> b.toDoubleOrNull() == a.toDouble() + else -> a.toString() == b.toString() + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpFunction.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpFunction.kt new file mode 100644 index 00000000..3c28af87 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpFunction.kt @@ -0,0 +1,53 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableType +import io.github.alexzhirkevich.compottie.internal.animation.expressions.argForNameOrIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.fastForEachIndexed + +internal class FunctionParam( + val name : String, + val default : Expression? +) + +internal class OpFunction( + val name : String, + private val parameters : List>, + private val body : Expression +) { + private val arguments = mutableMapOf>() + + fun invoke( + args: List>, + context: C, + ): Any { + arguments.clear() + parameters.fastForEachIndexed { i, p -> + arguments[p.name] = Pair( + VariableType.Let, + requireNotNull(args.argForNameOrIndex(i, p.name) ?: p.default) { + "'${p.name}' argument of '$name' function is missing" + }.invoke(context) + ) + } + + return context.withScope(arguments){ + body.invoke(it as C) + } + } +} + +internal fun OpFunctionExec( + name : String, + parameters : List>, +) = Expression { + val function = it.getVariable(name) as? OpFunction + ?: unresolvedReference(name) + + function.invoke( + args = parameters, + context = it, + ) +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpIfCondition.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpIfCondition.kt new file mode 100644 index 00000000..283f26bf --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpIfCondition.kt @@ -0,0 +1,27 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpConstant + +internal class OpIfCondition( + val condition : Expression = OpConstant(true), + val onTrue : Expression? = null, + val onFalse : Expression? = null +) : Expression { + + override fun invoke( + context: C + ): Undefined { + val expr = if (condition(context) as Boolean){ + onTrue + } else { + onFalse + } + + expr?.invoke(context) + + return Undefined + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpLoops.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpLoops.kt new file mode 100644 index 00000000..8311baa3 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpLoops.kt @@ -0,0 +1,88 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableType +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpAssign + +internal class OpForLoop( + private val assignment : OpAssign?, + private val increment: Expression?, + private val comparison : Expression?, + private val body: Expression +) : Expression { + + override fun invoke( + context: C + ): Any { + + if (comparison == null) { + loop( + condition = true, + context = context, + ) + } else { + TODO("for loop") + } + + return Undefined + } + + private fun loop( + condition: Boolean, + context: C, + ) { + val block = { ctx: C -> + while (condition) { + body.invoke(ctx) + increment?.invoke(ctx) + } + } + + if (assignment?.type == VariableType.Let || assignment?.type == VariableType.Const) { + context.withScope( + extraVariables = mapOf( + Pair( + assignment.variableName, + Pair( + assignment.type, + assignment.assignableValue.invoke(context,) + ) + ) + ), + ) { block(it as C) } + } else { + assignment?.invoke(context) + context.withScope { block(it as C) } + } + } +} + + +internal fun OpDoWhileLoop( + condition : Expression, + body : OpBlock +) = Expression { + do { + body.invoke(it) + } while (!condition.invoke(it).isFalse()) +} + + +internal fun OpWhileLoop( + condition : Expression, + body : Expression +) = Expression { + while (!condition.invoke(it).isFalse()){ + body.invoke(it) + } +} + +internal fun Any.isFalse() : Boolean { + return this == false + || this is Number && toDouble() == 0.0 + || this is CharSequence && isEmpty() + || this is Undefined +} + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpReturn.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpReturn.kt new file mode 100644 index 00000000..e61322b2 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpReturn.kt @@ -0,0 +1,8 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +internal class OpReturn( + val value : Expression +) : Expression by value \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpTryCatch.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpTryCatch.kt new file mode 100644 index 00000000..14694a8c --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/keywords/OpTryCatch.kt @@ -0,0 +1,70 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableType + +internal fun OpTryCatch( + tryBlock : Expression, + catchVariableName : String?, + catchBlock : Expression?, + finallyBlock : Expression?, +) = when { + catchBlock != null && finallyBlock != null -> + TryCatchFinally(tryBlock, catchVariableName, catchBlock, finallyBlock) + + catchBlock != null -> TryCatch(tryBlock, catchVariableName, catchBlock) + finallyBlock != null -> TryFinally(tryBlock, finallyBlock) + else -> error("SyntaxError: Missing catch or finally after try") +} + +private fun TryCatchFinally( + tryBlock : Expression, + catchVariableName : String?, + catchBlock : Expression, + finallyBlock : Expression, +) = Expression { + try { + tryBlock(it) + } catch (t: Throwable) { + if (catchVariableName != null) { + it.withScope(mapOf(catchVariableName to (VariableType.Const to t))) { + catchBlock(it as C) + } + } else { + catchBlock(it) + } + } finally { + finallyBlock(it) + } +} + +private fun TryCatch( + tryBlock : Expression, + catchVariableName : String?, + catchBlock : Expression +) = Expression { + try { + tryBlock(it) + } catch (t: Throwable) { + if (catchVariableName != null) { + it.withScope(mapOf(catchVariableName to (VariableType.Const to t))) { + catchBlock(it as C) + } + } else { + catchBlock(it) + } + } +} + + +private fun TryFinally( + tryBlock : Expression, + finallyBlock : Expression, +) = Expression { + try { + tryBlock(it) + } finally { + finallyBlock(it) + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpNoise.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpNoise.kt new file mode 100644 index 00000000..6c7815e2 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpNoise.kt @@ -0,0 +1,17 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random + +import io.github.alexzhirkevich.compottie.internal.animation.Vec2 +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression + +internal fun OpNoise( + time: Expression +) = Expression { property, context, state -> + when (val time = time.invoke(context)){ + is Number -> context.random.noise(time.toFloat()) + is Vec2 -> Vec2( + context.random.noise(time.x), + context.random.noise(time.y) + ) + else -> error("noise() takes single float or vector argument but $time got") + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpRandomNumber.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpRandomNumber.kt new file mode 100644 index 00000000..82fd11f8 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpRandomNumber.kt @@ -0,0 +1,38 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression + +internal fun OpRandomNumber( + minValOrArray1 : Expression? = null, + minValOrArray2 : Expression? = null, + isGauss : Boolean = false +) = Expression { property, context, state -> + with(context.random) { + when { + minValOrArray1 == null && minValOrArray2 == null -> + if (isGauss) gaussRandom() else random() + + minValOrArray2 == null && minValOrArray1 != null -> + if (isGauss) { + gaussRandom(minValOrArray1.invoke(context)) + } else { + random(minValOrArray1.invoke(context)) + } + + minValOrArray2 != null && minValOrArray1 != null -> + if (isGauss) { + gaussRandom( + minValOrArray1.invoke(context), + minValOrArray2.invoke(context), + ) + } else { + random( + minValOrArray1.invoke(context), + minValOrArray2.invoke(context), + ) + } + + else -> error("Invalid parameters for random()") + } + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSetRandomSeed.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSetRandomSeed.kt new file mode 100644 index 00000000..fd92c2a9 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSetRandomSeed.kt @@ -0,0 +1,16 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined + +internal fun OpSetRandomSeed( + seed : Expression, + timeless : Expression? = null +) = Expression { property, context, state -> + context.random.setSeed( + seed = (seed(context) as Number).toInt(), + timeless = (timeless?.invoke(context) as? Boolean) ?: false + ) + + Undefined +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSmooth.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSmooth.kt new file mode 100644 index 00000000..caf2339b --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpSmooth.kt @@ -0,0 +1,88 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random + +import io.github.alexzhirkevich.compottie.internal.AnimationState +import io.github.alexzhirkevich.compottie.internal.animation.RawKeyframeProperty +import io.github.alexzhirkevich.compottie.internal.animation.RawProperty +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.time.OpGetTime + +internal fun OpSmooth( + prop : Expression, + width : Expression? = null, + samples : Expression? = null, + time : Expression? = null +) = Expression { property, context, state -> + val prop = prop(context) as RawProperty + val width = (width?.invoke(context) as Number?)?.toFloat() ?: .4f + val samples = (samples?.invoke(context) as Number?)?.toInt() ?: 5 + val time = (time?.invoke(context) as Number?)?.toFloat() + + if (time == null) { + smooth(prop, state, width ?: .4f, samples ?: 5) + } else { + state.onTime(time) { + smooth(prop, it, width ?: .4f, samples ?: 5) + } + } +} + +@Suppress("unchecked_cast") +private fun smooth( + prop : RawProperty, + state: AnimationState, + width : Float, + samples : Int +) : Any { + + if (prop !is RawKeyframeProperty<*, *> || samples <= 1) { + return prop.raw(state) + } + + val width = width/2f + + val currentTime = OpGetTime.invoke(state) + val initTime = currentTime - width + val endTime = currentTime + width + val sampleFrequency = endTime-initTime + + var value : Any? = null + + repeat (samples) { i -> + val sampleValue = state.onTime(initTime + i * sampleFrequency, prop::raw) + + when { + value is Number? && sampleValue is Number -> { + value = if (value == null){ + sampleValue + } else { + (value as Number).toFloat() + sampleValue.toFloat() + } + } + value is List<*>? && sampleValue is List<*> -> { + val v = value + + if (v == null){ + value = sampleValue.toMutableList() + } else { + v as MutableList + sampleValue as List + + for (i in v.indices) { + v[i] = v[i].toFloat() + sampleValue[i].toFloat() + } + } + } + } + } + + when (val v = value) { + is Number -> value = v.toFloat() / samples + is MutableList<*> -> { + v as MutableList + repeat(v.lastIndex) { + v[it] = v[it] / samples + } + } + } + return value!! +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpTemporalWiggle.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpTemporalWiggle.kt new file mode 100644 index 00000000..39b9b18e --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpTemporalWiggle.kt @@ -0,0 +1,52 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random + +import io.github.alexzhirkevich.compottie.internal.AnimationState +import io.github.alexzhirkevich.compottie.internal.animation.RawProperty +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.time.OpGetTime + +internal class OpTemporalWiggle( + private val freq: Expression, + private val amp: Expression, + private val octaves: Expression? = null, + private val ampMult: Expression? = null, + private val time: Expression? = null, +) : Expression { + + private var lastChange: MutableMap = mutableMapOf() + private var wiggle: MutableMap = mutableMapOf() + private var prevWigle: MutableMap = mutableMapOf() + + override fun invoke( + context: ScriptContext + ): Any { + return if (time == null) { + wiggle(property, context, state) + } else { + state.onTime((time.invoke(context) as Number).toFloat()) { + wiggle(property, context, it) + } + } + } + + + private fun wiggle( + property: RawProperty, + context: ScriptContext, + state: AnimationState + ): Any { + return OpWiggle.invoke( + property = OpGetTime::invoke, + freq = (freq(context) as Number).toFloat(), + amp = (amp(context) as Number).toFloat(), + octaves = (octaves?.invoke(context) as Number?)?.toInt(), + ampMult = (ampMult?.invoke(context) as Number?)?.toFloat(), + time = (time?.invoke(context) as? Number)?.toFloat(), + state = state, + lastChange = lastChange, + wiggle = wiggle, + prevWigle = prevWigle + ) + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpWiggle.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpWiggle.kt new file mode 100644 index 00000000..96b49b11 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/random/OpWiggle.kt @@ -0,0 +1,173 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random + +import androidx.compose.ui.geometry.Offset +import io.github.alexzhirkevich.compottie.internal.AnimationState +import io.github.alexzhirkevich.compottie.internal.animation.RawProperty +import io.github.alexzhirkevich.compottie.internal.animation.Vec2 +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.time.OpGetTime +import kotlin.math.abs +import kotlin.math.pow +import kotlin.random.Random + +internal class OpWiggle( + private val property: Expression, + private val freq : Expression, + private val amp : Expression, + private val octaves : Expression? = null, + private val ampMult : Expression? = null, + private val time : Expression? = null, +) : Expression { + + private var lastChange: MutableMap = mutableMapOf() + private var wiggle: MutableMap = mutableMapOf() + private var prevWigle: MutableMap = mutableMapOf() + + + override fun invoke( + context: ScriptContext + ): Any { + val t = time.takeIf { it !is OpGetTime } + return if (t == null) { + wiggle(property, context, state) + } else { + state.onTime((t(context) as Number).toFloat()){ + wiggle(property, context, it) + } + } + } + + + private fun wiggle( + property: RawProperty, + context: ScriptContext, + state: AnimationState + ) : Any { + val prop = property(property, context, state) as RawProperty + + return invoke( + property = prop::raw, + freq = (freq(context) as Number).toFloat(), + amp = (amp(context) as Number).toFloat(), + octaves = (octaves?.invoke(context) as Number?)?.toInt(), + ampMult = (ampMult?.invoke(context) as Number?)?.toFloat(), + time = (time?.invoke(context) as? Number)?.toFloat(), + state = state, + lastChange = lastChange, + wiggle = wiggle, + prevWigle = prevWigle + ) + } + + companion object { + + fun invoke( + property: (AnimationState) -> Any, + freq : Float, + amp : Float, + octaves : Int?, + ampMult : Float?, + time : Float?, + state: AnimationState, + lastChange: MutableMap, + wiggle: MutableMap, + prevWigle: MutableMap + ) : Any { + return if (time == null) { + wiggle( + value = property(state), + freq = freq, + amp = amp, + octaves = octaves ?: 1, + ampMult = ampMult ?: .5f, + state = state, + lastChange = lastChange, + wiggle = wiggle, + prevWigle = prevWigle + ) + } else { + state.onTime(time) { + wiggle( + value = property(it), + freq = freq, + amp = amp, + octaves = octaves ?: 1, + ampMult = ampMult ?: .5f, + state = it, + lastChange = lastChange, + wiggle = wiggle, + prevWigle = prevWigle + ) + } + } + } + + private fun wiggle( + value: Any, + freq : Float, + amp : Float, + octaves : Int, + ampMult : Float, + state: AnimationState, + lastChange: MutableMap, + wiggle: MutableMap, + prevWigle: MutableMap + ) : Any { + + var value = value + + repeat(octaves) { + val octAmp = amp / (if (it == 0) 1f else ampMult.pow(it)) + val octFreq = freq * (if (it == 0) 1f else ampMult.pow(it)) + + val octLast = lastChange[it] + + val frameTime = 1000f / octFreq + val elapsedTime = abs(state.time.inWholeMilliseconds - (octLast ?: 0)).toFloat() + + val progress = if (octLast == null || elapsedTime > frameTime) { + lastChange[it] = state.time.inWholeMilliseconds + + when (value) { + is Float -> { + prevWigle[it] = (wiggle[it] as? Float) ?: 0f + wiggle[it] = -octAmp + Random.nextFloat() * 2 * octAmp + } + + is Vec2 -> { + prevWigle[it] = (wiggle[it] as? Vec2) ?: Vec2.Zero + wiggle[it] = Offset( + -octAmp + Random.nextFloat() * 2 * octAmp, + -octAmp + Random.nextFloat() * 2 * octAmp, + ) + } + + else -> error("${value::class} can't be wiggled") + } + 0f + } else { + (elapsedTime / frameTime).coerceIn(0f, 1f) + } + + val p = prevWigle[it] + val c = wiggle[it] + + when { + value is Float && p is Float && c is Float -> value += androidx.compose.ui.util.lerp( + p, + c, + progress + ) + value is Vec2 && p is Vec2 && c is Vec2 -> value += androidx.compose.ui.geometry.lerp( + p, + c, + progress + ) + else -> error("${value::class} can't be wiggled") + } + } + return value + } + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssign.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssign.kt new file mode 100644 index 00000000..54dc874f --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssign.kt @@ -0,0 +1,35 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableType + +internal class OpAssign( + val type : VariableType? = null, + val variableName : String, + val assignableValue : Expression, + private val merge : ((Any, Any) -> Any)? +) : Expression { + + override fun invoke(context: C): Any { + val v = assignableValue.invoke(context) + + val current = context.getVariable(variableName) + + check(merge == null || current != null) { + "Cant modify $variableName as it is undefined" + } + + val value = if (current != null && merge != null) { + merge.invoke(current, v) + } else v + + context.setVariable( + name = variableName, + value = value, + type = type + ) + + return value + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssignByIndex.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssignByIndex.kt new file mode 100644 index 00000000..e5bdf3fd --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpAssignByIndex.kt @@ -0,0 +1,63 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableType + +internal class OpAssignByIndex( + private val variableName : String, + private val scope : VariableType?, + private val index : Expression, + private val assignableValue : Expression, + private val merge : ((Any, Any) -> Any)? +) : Expression { + + override tailrec fun invoke(context: C): Undefined { + val v = assignableValue.invoke(context) + val current = context.getVariable(variableName) + + check(merge == null || current != null) { + "Cant modify $variableName as it is undefined" + } + + if (current == null) { + context.setVariable(variableName, mutableListOf(), scope) + return invoke(context) + } else { + val i = index.invoke(context) + + val index = checkNotNull(i as? Number) { + "Unexpected index: $i" + }.toInt() + + when (current) { + + is MutableList<*> -> { + current as MutableList + + while (current.lastIndex < index) { + current.add(Undefined) + } + + val c = current[index] + + current[index] = if (current[index] !is Undefined && merge != null){ + merge.invoke(c,v) + } else { + v + } + } + + is List<*> -> { + context.setVariable(variableName, current.toMutableList(), scope) + return invoke(context) + } + + else -> error("Can't assign '$current' by index ($index)") + } + } + + return Undefined + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpCompare.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpCompare.kt new file mode 100644 index 00000000..db2a9725 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpCompare.kt @@ -0,0 +1,40 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.keywords.OpEqualsImpl + +internal fun OpCompare( + a : Expression, + b : Expression, + comparator : (Comparable<*>, Comparable<*>) -> Any +) = Expression { + comparator( + a(it) as Comparable<*>, + b(it) as Comparable<*> + ) +} + +internal val OpGreaterComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> + if (a is Number && b is Number) { + a.toDouble() > b.toDouble() + } else { + a.toString() > b.toString() + } +} + +internal val OpLessComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> + if (a is Number && b is Number) { + a.toDouble() < b.toDouble() + } else { + a.toString() < b.toString() + } +} + +internal val OpTypedEqualsComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> + OpEqualsImpl(a, b, true) +} + +internal val OpEqualsComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> + OpEqualsImpl(a, b, false) +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpConstant.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpConstant.kt new file mode 100644 index 00000000..5c245f04 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpConstant.kt @@ -0,0 +1,7 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext + +internal fun OpConstant(value: Any) = + Expression { value } \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpGetVariable.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpGetVariable.kt new file mode 100644 index 00000000..b8172f0a --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpGetVariable.kt @@ -0,0 +1,21 @@ +package io.github.alexzhirkevich.skriptie.ecmascript.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableType + +internal class OpGetVariable( + val name : String, + val assignmentType : VariableType? = null +) : Expression { + + override fun invoke( + context: C, + ): Any { + return if (assignmentType != null) { + context.setVariable(name, 0f, assignmentType) + } else checkNotNull(context.getVariable(name)) { + "Undefined variable: $name" + } + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpIndex.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpIndex.kt new file mode 100644 index 00000000..729e5aae --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpIndex.kt @@ -0,0 +1,19 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.tryGet + +internal class OpIndex( + val variable : Expression, + val index : Expression, +) : Expression { + + override fun invoke(context: C): Any { + val v = variable(context) + val idx = (index.invoke(context) as Number).toInt() + + return v.tryGet(idx) ?: Undefined + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpMakeArray.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpMakeArray.kt new file mode 100644 index 00000000..32eb866a --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpMakeArray.kt @@ -0,0 +1,11 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.fastMap + +internal fun OpMakeArray( + items : List> +) = Expression { context -> + items.fastMap { it(context) }.toMutableList() +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpVar.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpVar.kt new file mode 100644 index 00000000..8a7ad8a2 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/OpVar.kt @@ -0,0 +1,21 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.InterpretationContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableType +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.OpGetVariable + +internal class OpVar( + val scope : VariableType +) : Expression, InterpretationContext { + + override fun interpret(callable: String?, args: List>?): Expression? { + return if (callable == null) + OpConstant(Undefined) + else OpGetVariable(callable, assignmentType = scope) + } + + override fun invoke(context: C) = Undefined +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/ScriptUtil.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/ScriptUtil.kt new file mode 100644 index 00000000..250d9cb4 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/operations/value/ScriptUtil.kt @@ -0,0 +1,288 @@ +package io.github.alexzhirkevich.skriptie.ecmascript.operations.value + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +internal fun Delegate(a : Expression, b : Expression, op : (Any, Any) -> Any) = Expression { + op(a(it),b(it)) +} + +internal fun Delegate(a : Expression, op : (Any) -> Any) = Expression { + op(a(it)) +} + + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastForEach(action: (T) -> Unit) { + contract { callsInPlace(action) } + for (index in indices) { + val item = get(index) + action(item) + } +} + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastForEachReversed(action: (T) -> Unit) { + contract { callsInPlace(action) } + for (index in indices.reversed()) { + val item = get(index) + action(item) + } +} + + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastForEachIndexed(action: (Int, T) -> Unit) { + contract { callsInPlace(action) } + for (index in indices) { + val item = get(index) + action(index, item) + } +} + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastAll(predicate: (T) -> Boolean): Boolean { + contract { callsInPlace(predicate) } + fastForEach { if (!predicate(it)) return false } + return true +} + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastAny(predicate: (T) -> Boolean): Boolean { + contract { callsInPlace(predicate) } + fastForEach { if (predicate(it)) return true } + return false +} + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastFirstOrNull(predicate: (T) -> Boolean): T? { + contract { callsInPlace(predicate) } + fastForEach { if (predicate(it)) return it } + return null +} + + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastSumBy(selector: (T) -> Double): Double { + contract { callsInPlace(selector) } + var sum = 0.0 + fastForEach { element -> + sum += selector(element) + } + return sum +} + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastMap(transform: (T) -> R): List { + contract { callsInPlace(transform) } + val target = ArrayList(size) + fastForEach { + target += transform(it) + } + return target +} + +// TODO: should be fastMaxByOrNull to match stdlib +/** + * Returns the first element yielding the largest value of the given function or `null` if there + * are no elements. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun > List.fastMaxBy(selector: (T) -> R): T? { + contract { callsInPlace(selector) } + if (isEmpty()) return null + var maxElem = get(0) + var maxValue = selector(maxElem) + for (i in 1..lastIndex) { + val e = get(i) + val v = selector(e) + if (maxValue < v) { + maxElem = e + maxValue = v + } + } + return maxElem +} + +/** + * Returns the last element matching the given [predicate], or `null` if no such element was found. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastLastOrNull(predicate: (T) -> Boolean): T? { + contract { callsInPlace(predicate) } + for (index in indices.reversed()) { + val item = get(index) + if (predicate(item)) return item + } + return null +} + +/** + * Returns a list containing only elements matching the given [predicate]. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental. +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastFilter(predicate: (T) -> Boolean): List { + contract { callsInPlace(predicate) } + val target = ArrayList(size) + fastForEach { + if (predicate(it)) target += (it) + } + return target +} + +/** + * Returns a list containing the results of applying the given [transform] function + * to each element in the original collection. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@OptIn(ExperimentalContracts::class) +@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental. +internal inline fun List.fastMapIndexed( + transform: (index: Int, T) -> R +): List { + contract { callsInPlace(transform) } + val target = ArrayList(size) + fastForEachIndexed { index, e -> + target += transform(index, e) + } + return target +} + +/** + * Returns a list containing the results of applying the given [transform] function + * to each element in the original collection. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@OptIn(ExperimentalContracts::class) +@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental. +internal inline fun List.fastMapIndexedNotNull( + transform: (index: Int, T) -> R? +): List { + contract { callsInPlace(transform) } + val target = ArrayList(size) + fastForEachIndexed { index, e -> + transform(index, e)?.let { target += it } + } + return target +} + +/** + * Returns the largest value among all values produced by selector function applied to each element + * in the collection or null if there are no elements. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental. +@OptIn(ExperimentalContracts::class) +internal inline fun > List.fastMaxOfOrNull(selector: (T) -> R): R? { + contract { callsInPlace(selector) } + if (isEmpty()) return null + var maxValue = selector(get(0)) + for (i in 1..lastIndex) { + val v = selector(get(i)) + if (v > maxValue) maxValue = v + } + return maxValue +} + +/** + * Returns a list containing the results of applying the given [transform] function + * to each element in the original collection. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastMapNotNull(transform: (T) -> R?): List { + contract { callsInPlace(transform) } + val target = ArrayList(size) + fastForEach { e -> + transform(e)?.let { target += it } + } + return target +} + + +/** + * Returns a single list of all elements yielded from results of [transform] function being invoked + * on each element of original collection. + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + */ +@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental. +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastFlatMap(transform: (T) -> Iterable): List { + contract { callsInPlace(transform) } + val target = ArrayList(size) + fastForEach { e -> + val list = transform(e) + target.addAll(list) + } + return target +} + +/** + * Returns the first value that [predicate] returns `true` for + * + * **Do not use for collections that come from public APIs**, since they may not support random + * access in an efficient way, and this method may actually be a lot slower. Only use for + * collections that are created by code we control and are known to support random access. + * + * @throws [NoSuchElementException] if no such element is found + */ +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal inline fun List.fastFirst(predicate: (T) -> Boolean): T { + contract { callsInPlace(predicate) } + fastForEach { if (predicate(it)) return it } + throw NoSuchElementException("Collection contains no element matching the predicate.") +} + +/** + * Copied from Appendable.kt + */ +private fun Appendable.appendElement(element: T, transform: ((T) -> CharSequence)?) { + when { + transform != null -> append(transform(element)) + element is CharSequence? -> append(element) + element is Char -> append(element) + else -> append(element.toString()) + } +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JS.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JS.kt new file mode 100644 index 00000000..c73d7a23 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JS.kt @@ -0,0 +1,27 @@ +package io.github.alexzhirkevich.skriptie.javascript + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.EcmascriptInterpreter +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Script +import io.github.alexzhirkevich.skriptie.ecmascript.ScriptEngine + +public class JS( + override val context: JSScriptContext = JSScriptContext(), + private val globalContext: JsGlobalContext = JsGlobalContext(), + private val extensionContext : JsExtensionContext = JsExtensionContext() +) : ScriptEngine { + + override fun compile(script: String): Script { + val expression = EcmascriptInterpreter( + expr = script, + scriptContext = context, + globalContext = globalContext, + extensionContext = extensionContext + ).interpret() + + return Script { expression.invoke(it.context) } + } + + override fun reset() { + context.reset() + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSScriptContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSScriptContext.kt new file mode 100644 index 00000000..fcfac2d7 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSScriptContext.kt @@ -0,0 +1,10 @@ +package io.github.alexzhirkevich.skriptie.javascript + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.BaseScriptContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.ScriptContext +import io.github.alexzhirkevich.skriptie.ecmascript.GlobalContext + +public open class JSScriptContext( + override val globalContext: GlobalContext = JsGlobalContext() +) : BaseScriptContext() { +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsExtensionContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsExtensionContext.kt new file mode 100644 index 00000000..6c558495 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsExtensionContext.kt @@ -0,0 +1,40 @@ +package io.github.alexzhirkevich.skriptie.javascript + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.number.JsNumberContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.string.JsStringContext +import io.github.alexzhirkevich.skriptie.ecmascript.ExtensionContext +import io.github.alexzhirkevich.skriptie.javascript.iterable.JsIndexOf + + +public open class JsExtensionContext: ExtensionContext { + + override fun interpret( + parent: Expression, + op: String?, + args: List>? + ): Expression? { + return if (args != null){ + when (op) { + "toString" -> Expression { parent(it).toString() } + "indexOf", "lastIndexOf" -> { + checkArgs(args, 1, op) + JsIndexOf( + value = parent, + search = args.argAt(0), + last = op == "lastIndexOf" + ) + } + + else -> JsNumberContext.interpret(parent, op, args) + ?: JsStringContext.interpret(parent, op, args) + } + } else { + JsNumberContext.interpret(parent, op, args) + ?: JsStringContext.interpret(parent, op, args) + } + } +} + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsGlobalContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsGlobalContext.kt new file mode 100644 index 00000000..268a2a12 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsGlobalContext.kt @@ -0,0 +1,216 @@ +package io.github.alexzhirkevich.skriptie.javascript + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.math.JsInfinity +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.math.JsMath +import io.github.alexzhirkevich.skriptie.ecmascript.GlobalContext +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.fastMap +import kotlin.math.min + +public open class JsGlobalContext : GlobalContext { + + override fun interpret( + callable: String?, + args: List>? + ): Expression? { + return when (callable) { + "Infinity" -> JsInfinity + "Math" -> JsMath + else -> null + } + } + + override fun sum(a: Any, b: Any): Any { + return jssum(a.validateJsNumber(), b.validateJsNumber()) + } + + override fun sub(a: Any, b: Any): Any { + return jssub(a.validateJsNumber(), b.validateJsNumber()) + } + + override fun mul(a: Any, b: Any): Any { + return jsmul(a.validateJsNumber(),b.validateJsNumber()) + } + + override fun div(a: Any, b: Any): Any { + return jsdiv(a.validateJsNumber(), b.validateJsNumber()) + } + + override fun mod(a: Any, b: Any): Any { + return jsmod(a.validateJsNumber(), b.validateJsNumber()) + } + + override fun inc(a: Any): Any { + return jsinc(a.validateJsNumber()) + } + + override fun dec(a: Any): Any { + return jsdec(a.validateJsNumber()) + } + + override fun neg(a: Any): Any { + return jsneg(a.validateJsNumber()) + } +} + + + +private fun jssum(a : Any, b : Any) : Any { + return when { + a is Number && b is Undefined || a is Undefined && b is Number -> Float.NaN + a is Long && b is Long -> a+b + a is Number && b is Number -> a.toDouble() + b.toDouble() + a is List<*> && b is List<*> -> { + a as List + b as List + + List(min(a.size, b.size)) { + a[it].toDouble() + b[it].toDouble() + } + } + + a is List<*> && b is Number -> { + if (a is MutableList<*>){ + a as MutableList + a[0] = a[0].toDouble() + b.toDouble() + a + } else { + listOf((a as List).first().toDouble() + b.toDouble()) + a.drop(1) + } + } + a is Number && b is List<*> -> { + if (b is MutableList<*>){ + b as MutableList + b[0] = b[0].toDouble() + a.toDouble() + b + } else { + listOf(a.toDouble() + (b as List).first().toDouble()) + b.drop(1) + } + } + a is CharSequence -> a.toString() + b.toString() + + else -> error("Cant calculate the sum of $a and $b") + } +} + +private fun jssub(a : Any, b : Any) : Any { + return when { + a is Number && b is Undefined || a is Undefined && b is Number -> Float.NaN + a is Long && b is Long -> a-b + a is Double && b is Double -> a-b + a is Number && b is Number -> a.toDouble() - b.toDouble() + a is List<*> && b is List<*> -> { + a as List + b as List + List(min(a.size, b.size)) { + a[it].toDouble() - b[it].toDouble() + } + } + a is CharSequence || b is CharSequence -> { + a.toString().toDouble() - b.toString().toDouble() + } + else -> error("Cant subtract $b from $a") + } +} + +private fun jsmul(a : Any, b : Any) : Any { + return when { + a is Number && b is Undefined || a is Undefined && b is Number -> Float.NaN + a is Long && b is Long -> a*b + a is Double && b is Double -> a*b + a is Long && b is Long -> if (b == 0) Float.POSITIVE_INFINITY else a/b + a is Number && b is Number -> a.toDouble() * b.toDouble() + a is List<*> && b is Number -> { + a as List + val bf = b.toDouble() + a.fastMap { it.toDouble() * bf } + } + a is Number && b is List<*> -> { + b as List + val af = a.toDouble() + b.fastMap { it.toDouble() * af } + } + a is CharSequence || b is CharSequence -> { + a.toString().toDouble() * b.toString().toDouble() + } + else -> error("Cant multiply $a by $b") + } +} + +private fun jsdiv(a : Any, b : Any) : Any { + return when { + a is Number && b is Undefined || a is Undefined && b is Number -> Float.NaN + a is Long && b is Long -> when { + b == 0 -> Double.POSITIVE_INFINITY + a % b == 0L -> a / b + else -> a.toDouble() / b + } + + a is Number && b is Number -> a.toDouble() / b.toDouble() + a is List<*> && b is Number -> { + a as List + val bf = b.toDouble() + a.fastMap { it.toDouble() / bf } + } + + a is CharSequence || b is CharSequence -> { + a.toString().toDouble() / b.toString().toDouble() + } + + else -> error("Cant divide $a by $b") + } +} + +private fun jsmod(a : Any, b : Any) : Any { + return when { + a is Long && b is Long -> a % b + a is Number && b is Number -> a.toDouble() % b.toDouble() + else -> error("Can't get mod of $a and $b") + } +} + + +private fun jsinc(v : Any) : Any { + return when (v) { + is Long -> v + 1 + is Double -> v + 1 + is Number -> v.toDouble() + 1 + else -> error("can't increment $v") + } +} + +private fun jsdec(v : Any) : Any { + return when (v) { + is Long -> v - 1 + is Double -> v - 1 + is Number -> v.toDouble() - 1 + else -> error("can't decrement $v") + } +} + +private fun jsneg(v : Any) : Any { + return when (v) { + is Long -> -v + is Number -> -v.toDouble() + is List<*> -> { + v as List + v.fastMap { -it.toDouble() } + } + + else -> error("Cant apply unary minus to $v") + } +} + +internal fun Any.validateJsNumber() = when(this) { + is Byte -> toLong() + is UByte -> toLong() + is Short -> toLong() + is UShort -> toLong() + is Int -> toLong() + is UInt -> toLong() + is ULong -> toLong() + is Float -> toDouble() + else -> this +} + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/iterable/JsIndexOf.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/iterable/JsIndexOf.kt new file mode 100644 index 00000000..b1d40205 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/iterable/JsIndexOf.kt @@ -0,0 +1,37 @@ +package io.github.alexzhirkevich.skriptie.javascript.iterable + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference +import io.github.alexzhirkevich.skriptie.javascript.JSScriptContext + +internal class JsIndexOf( + private val value : Expression, + private val search : Expression, + private val last : Boolean +) : Expression { + + override fun invoke( + context: JSScriptContext, + ): Any { + val value = value(context) + val search = search(context) + + return when { + value is String && (search is String || search is Char) -> { + if (search is String) { + if (last) + value.lastIndexOf(search) + else value.indexOf(search) + } else { + value.indexOf(search as Char) + } + } + + value is List<*> -> if(last) + value.lastIndexOf(search) + else value.indexOf(search) + else -> unresolvedReference("indexOf") + } + } +} + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/math/JsMath.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/math/JsMath.kt new file mode 100644 index 00000000..12b7a574 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/math/JsMath.kt @@ -0,0 +1,203 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.math + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.InterpretationContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.OpUndefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random.OpRandomNumber +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpConstant +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.fastMap +import io.github.alexzhirkevich.skriptie.ecmascript.operations.value.fastSumBy +import io.github.alexzhirkevich.skriptie.javascript.JSScriptContext +import io.github.alexzhirkevich.skriptie.javascript.validateJsNumber +import kotlin.math.abs +import kotlin.math.acos +import kotlin.math.acosh +import kotlin.math.asin +import kotlin.math.asinh +import kotlin.math.atan +import kotlin.math.atan2 +import kotlin.math.atanh +import kotlin.math.cbrt +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.cosh +import kotlin.math.exp +import kotlin.math.expm1 +import kotlin.math.floor +import kotlin.math.ln1p +import kotlin.math.log +import kotlin.math.log10 +import kotlin.math.log2 +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sign +import kotlin.math.sin +import kotlin.math.sinh +import kotlin.math.sqrt +import kotlin.math.tan +import kotlin.math.tanh +import kotlin.math.truncate + +internal val JsInfinity = OpConstant(Double.POSITIVE_INFINITY) + +internal object JsMath : InterpretationContext { + + override fun invoke(context: JSScriptContext): Any = this + + override fun interpret( + callable: String?, + args: List>? + ): Expression { + return if (args == null){ + interpretVar(callable) + } else { + interpretFun(callable, args) + } + } + + private fun interpretVar( + op: String?, + ): Expression { + return when (op) { + "PI" -> PI + "E" -> E + "LN10" -> LN10 + "LN2" -> LN2 + "LOG10E" -> LOG10E + "LOG2E" -> LOG2E + "SQRT1_2" -> SQRT1_2 + "SQRT2" -> SQRT2 + else -> OpUndefined + } + } + private fun interpretFun( + op: String?, + args: List> + ): Expression { + return when (op) { + "abs" -> op1(args, ::abs, op) + "asoc" -> op1(args, ::acos, op) + "asoch" -> op1(args, ::acosh, op) + "asin" -> op1(args, ::asin, op) + "asinh" -> op1(args, ::asinh, op) + "atan" -> op1(args, ::atan, op) + "atan2" -> op2(args, ::atan2, op) + "atanh" -> op1(args, ::atanh, op) + "cbrt" -> op1(args, ::cbrt, op) + "ceil" -> op1(args, ::ceil, op) + "cos" -> op1(args, ::cos, op) + "cosh" -> op1(args, ::cosh, op) + "exp" -> op1(args, ::exp, op) + "expm1" -> op1(args, ::expm1, op) + "floor" -> op1(args, ::floor, op) + "hypot" -> opN(args, ::hypotN, op) + "imul" -> op2(args, ::imul, op) + "log" -> op2(args, ::log, op) + "log10" -> op1(args, ::log10, op) + "log1p " -> op1(args, ::ln1p, op) + "log2" -> op1(args, ::log2, op) + "max" -> opN(args, List::max, op) + "min" -> opN(args, List::min, op) + "pow" -> op2(args, Double::pow, op) + "random" -> OpRandomNumber() + "round" -> op1(args, Double::roundToInt, op) + "sign" -> op1(args, Double::sign, op) + "sin" -> op1(args, ::sin, op) + "sinh" -> op1(args, ::sinh, op) + "sqrt" -> op1(args, ::sqrt, op) + "tan" -> op1(args, ::tan, op) + "tanh" -> op1(args, ::tanh, op) + "trunc" -> op1(args, ::truncate, op) + + else -> OpUndefined + } + } + + private fun op1( + args: List>, + func: (Double) -> Number, + name: String + ): Expression { + checkArgs(args, 1, name) + + val a = args.argAt(0) + return Expression { + val a = a(it).validateJsNumber() + require(a is Number) { + "Can't get Math.$name of $a" + } + func(a.toDouble()) + } + } + + private fun op2( + args: List>, + func: (Double, Double) -> Number, + name: String + ): Expression { + checkArgs(args, 2, name) + + val a = args.argAt(0) + val b = args.argAt(1) + return Expression { + val a = a(it).validateJsNumber() + val b = b(it).validateJsNumber() + require(a is Number && b is Number) { + "Can't get Math.$name of ($a,$b)" + } + func(a.toDouble(), b.toDouble()) + } + } + + private fun opN( + args: List>, + func: (List) -> Number, + name: String + ): Expression { + check(args.isNotEmpty()){ + "Math.$name must have at least 1 argument" + } + return Expression { context -> + + val a = args.fastMap { + val n = it(context).validateJsNumber().also { + check(it is Number) { + "Illegal arguments for Math.$name" + } + } as Number + + n.toDouble() + } + + func(a) + } + } + + + private val PI = OpConstant(kotlin.math.PI) + private val E = OpConstant(kotlin.math.E) + private val LN10 = OpConstant(2.302585092994046) + private val LN2 = OpConstant(0.6931471805599453) + private val LOG10E = OpConstant(0.4342944819032518) + private val LOG2E = OpConstant(1.4426950408889634) + private val SQRT1_2 = OpConstant(0.7071067811865476) + private val SQRT2 = OpConstant(1.4142135623730951) +} + +private fun hypotN(args : List): Double { + return sqrt(args.fastSumBy { it * it }) +} + +private fun imul(x : Double, y : Double) : Long { + val a = x.toLong().toInt() + val b = y.toLong().toInt() + + val ah = (a ushr 16) and 0xffff + val al = a and 0xffff + val bh = (b ushr 16) and 0xffff + val bl = b and 0xffff + + return ((al * bl) + ((((ah * bl) + (al * bh)) shl 16) ushr 0) or 0).toLong() +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsNumberContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsNumberContext.kt new file mode 100644 index 00000000..52feef1a --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsNumberContext.kt @@ -0,0 +1,28 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.number + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgsNotNull +import io.github.alexzhirkevich.skriptie.ecmascript.ExtensionContext +import io.github.alexzhirkevich.skriptie.javascript.JSScriptContext + +internal object JsNumberContext : ExtensionContext { + + override fun interpret( + parent: Expression, + op: String?, + args: List>? + ): Expression? { + return when(op){ + "toFixed" -> { + checkArgsNotNull(args, op) + JsToFixed(parent, args.singleOrNull()) + } + "toPrecision" -> { + checkArgsNotNull(args, op) + JsToPrecision(parent, args.singleOrNull()) + } + + else -> null + } + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsToPrecision.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsToPrecision.kt new file mode 100644 index 00000000..7399c792 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/number/JsToPrecision.kt @@ -0,0 +1,61 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.number + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference +import io.github.alexzhirkevich.skriptie.javascript.JSScriptContext +import io.github.alexzhirkevich.skriptie.javascript.validateJsNumber +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +internal fun JsToPrecision( + number : Expression, + digits : Expression? = null +) = Expression { context -> + + val number = (number(context).validateJsNumber() as? Number?)?.toDouble() + ?: unresolvedReference("toFixed") + + val digits = (digits?.invoke(context)?.validateJsNumber() as Number?)?.toInt() + ?.takeIf { it > 0 } + ?: return@Expression number + + number.roundTo(digits-1) +} + +internal fun JsToFixed( + number : Expression, + digits : Expression? +) = Expression { context -> + val number = (number(context).validateJsNumber()as? Number?)?.toDouble() + ?: unresolvedReference("toFixed") + + val digits = (digits?.invoke(context)?.validateJsNumber() as Number?)?.toInt() ?: 0 + + if (digits == 0) { + return@Expression number.roundToLong().toString() + } + + val stringNumber = number.roundTo(digits).toString() + + val intPart = stringNumber.substringBefore(".") + val floatPart = stringNumber.substringAfter(".", "").take(digits) + + if (floatPart.isBlank()) { + return@Expression intPart + } + + (intPart + "." + floatPart.padEnd(digits, '0')) +} + +private val pow10 by lazy { + (1..10).mapIndexed { i, it -> i to 10.0.pow(it) }.toMap() +} + +internal fun Double.roundTo(digit : Int) : Double { + if(digit <= 0) + return roundToLong().toDouble() + + val pow = pow10[digit-1] ?: return this + return ((this * pow).roundToInt() / pow) +} diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsString.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsString.kt new file mode 100644 index 00000000..a9a96d05 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsString.kt @@ -0,0 +1,158 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.string + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.skriptie.javascript.JSScriptContext + +internal fun JsEndsWith( + string : Expression, + searchString : Expression, + position : Expression? +) = Expression { context -> + val string = string(context) as String + val searchString = searchString(context) as String + val position = position?.invoke(context) as? Number? + + if (position == null){ + string.endsWith(searchString) + } else { + string.take(position.toInt()).endsWith(searchString) + } +} + +internal fun JsIncludes( + string : Expression, + searchString : Expression, + position : Expression? +) = Expression { context -> + val string = string(context) as String + val searchString = searchString(context) as String + val position = position?.invoke(context) as? Number? + + if (position == null){ + string.contains(searchString) + } else { + string.drop(position.toInt()).contains(searchString) + } +} + +internal fun JsMatch( + string : Expression, + regexp : Expression, +) = Expression { context -> + val string = string(context) as String + val regex = regexp(context) as String + + string.matches(regex.toRegex()) +} + +internal fun JsPadEnd( + string : Expression, + targetLength : Expression, + padString : Expression?, +) = Expression { context -> + val string = string(context) as String + val padString = padString?.invoke(context) as String? ?: " " + val targetLength = (targetLength(context) as Number).toInt() + + buildString(targetLength) { + append(string) + while (length < targetLength) { + append(padString.take(targetLength - length)) + } + } +} + + +internal fun JsPadStart( + string : Expression, + targetLength : Expression, + padString : Expression?, +) = Expression { context -> + val string = string(context) as String + val padString = padString?.invoke(context) as String? ?: " " + val targetLength = (targetLength(context) as Number).toInt() + + val toAppend = targetLength - string.length + + buildString(targetLength) { + while (length < toAppend) { + append(padString.take(toAppend - length)) + } + append(string) + } +} + +internal fun JsRepeat( + string : Expression, + count : Expression, +) = Expression { context -> + val string = string(context) as String + val count = (count(context) as Number).toInt() + string.repeat(count) +} + +internal fun JsReplace( + string : Expression, + pattern : Expression, + replacement : Expression, + all : Boolean +) = Expression { context -> + val string = string(context) as String + val pattern = pattern(context) as String + val replacement = replacement(context) as String + + if (pattern.isEmpty()) { + replacement + string + } else { + if (all) { + string.replace(pattern, replacement) + } else { + string.replaceFirst(pattern, replacement) + } + } +} + +internal fun JsStartsWith( + string : Expression, + searchString : Expression, + position : Expression? +) = Expression { context -> + val string = string(context) as String + val searchString = searchString(context) as String + val position = position?.invoke(context) as? Number? + + if (position == null){ + string.startsWith(searchString) + } else { + string.drop(position.toInt()).startsWith(searchString) + } +} + + +internal fun JsSubstring( + string : Expression, + start : Expression, + end : Expression? +) = Expression { context -> + val string = string(context) as String + val start = (start(context) as Number).toInt() + val end = (end?.invoke(context) as? Number?)?.toInt() + ?.coerceAtMost(string.length) ?: string.length + + string.substring(start, end) +} + +internal fun JsTrim( + string : Expression, + start : Boolean, + end : Boolean, +) = Expression { context -> + val string = string(context) as String + + when { + start && end -> string.trim() + start -> string.trimStart() + end -> string.trimEnd() + else -> string + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsStringContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsStringContext.kt new file mode 100644 index 00000000..0414d6dc --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/string/JsStringContext.kt @@ -0,0 +1,136 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.string + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt +import io.github.alexzhirkevich.compottie.internal.animation.expressions.argForNameOrIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgsNotNull +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.cast +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.withCast +import io.github.alexzhirkevich.skriptie.ecmascript.ExtensionContext +import io.github.alexzhirkevich.skriptie.javascript.JSScriptContext + +internal object JsStringContext : ExtensionContext { + + override fun interpret( + parent: Expression, + op: String?, + args: List>? + ): Expression? { + return when (op) { + "charAt", "at" -> { + checkArgs(args, 1, op) + OpIndex(parent, args.argAt(0)) + } + + "charCodeAt" -> { + checkArgs(args, 1, op) + val ind = args.argAt(0) + + parent.withCast { context -> + val index = (ind(context) as Number).toInt() + get(index).code + } + } + + "endsWith" -> { + checkArgsNotNull(args, op) + JsEndsWith( + string = parent, + searchString = args.argForNameOrIndex(0, "searchString")!!, + position = args.argForNameOrIndex(1, "endPosition") + ) + } + + "startsWith" -> { + checkArgsNotNull(args, op) + JsStartsWith( + string = parent, + searchString = args.argForNameOrIndex(0, "searchString")!!, + position = args.argForNameOrIndex(1, "position") + ) + } + + "includes" -> { + checkArgsNotNull(args, op) + JsIncludes( + string = parent, + searchString = args.argForNameOrIndex(0, "searchString")!!, + position = args.argForNameOrIndex(1, "position") + ) + } + + "padStart" -> { + checkArgsNotNull(args, op) + JsPadStart( + string = parent, + targetLength = args.argForNameOrIndex(0, "targetLength")!!, + padString = args.argForNameOrIndex(1, "padString") + ) + } + + "padEnd" -> { + checkArgsNotNull(args, op) + JsPadEnd( + string = parent, + targetLength = args.argForNameOrIndex(0, "targetLength")!!, + padString = args.argForNameOrIndex(1, "padString") + ) + } + + "match" -> { + checkArgsNotNull(args, op) + JsMatch( + string = parent, + regexp = args.argAt(0) + ) + } + + "replace", "replaceAll" -> { + checkArgsNotNull(args, op) + JsReplace( + string = parent, + pattern = args.argForNameOrIndex(0, "pattern")!!, + replacement = args.argForNameOrIndex(1, "replacement")!!, + all = op.endsWith("All") + ) + } + + "repeat" -> { + checkArgsNotNull(args, op) + JsRepeat( + string = parent, + count = args.argAt(0) + ) + } + + "trim", "trimStart", "trimEnd" -> { + JsTrim( + string = parent, + start = op != "trimEnd", + end = op != "trimStart" + ) + } + + "substring", "substr" -> { + checkArgsNotNull(args, op) + JsSubstring( + string = parent, + start = args.argForNameOrIndex(0, "start", "indexStart")!!, + end = args.argForNameOrIndex(1, "end", "indexEnd") + ) + } + + "toUppercase", "toLocaleLowerCase" -> { + parent.cast(String::uppercase) + } + "toLowerCase", "toLocaleUppercase" -> { + parent.cast(String::lowercase) + } + else -> { + null + } + } + } +} diff --git a/skriptie/src/commonTest/kotlin/js/AddExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/AddExpressionTest.kt new file mode 100644 index 00000000..57b77374 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/AddExpressionTest.kt @@ -0,0 +1,93 @@ +package js + +import expressions.assertExprEquals +import io.github.alexzhirkevich.compottie.internal.animation.Vec2 +import kotlin.test.Test + + +class AddExpressionTest { + + @Test + fun add_num_expr() { + "13+17".assertExprEquals(30f) + "-13+ 17".assertExprEquals(4f) + "-13+ -17".assertExprEquals(-30f) + "-13+ -17.0 + 10 - 4.0".assertExprEquals(-24f) + + "'10'+'5'".assertExprEquals("105") + "'10'+5".assertExprEquals("105") + "10+'5'".assertExprEquals("105") + } + + @Test + fun add_vec_expr(){ + "[13, 17] + [17, 13];".assertExprEquals(Vec2(30f,30f)) + "[-13, 17] + [-17, 13];".assertExprEquals(Vec2(-30f,30f)) + "[13.0, 17.0] + [17, 13];".assertExprEquals(Vec2(30f,30f)) + "[-13, 17.0] + [-17.0, 13];".assertExprEquals(Vec2(-30f,30f)) + } + + @Test + fun add_num_vec() { + "[10,20] + 5".assertExprEquals(15f) + "5 +[10,20]".assertExprEquals(15f) + "sum([10,20], 5)".assertExprEquals(15f) + "sum(5, [10,20])".assertExprEquals(15f) + "\$bm_sum([10,20], 5)".assertExprEquals(15f) + "\$bm_sum(5 ,[10,20])".assertExprEquals(15f) + } + + @Test + fun add_num_fun() { + "add(13,17)".assertExprEquals(30f) + " add( 13, 17); ".assertExprEquals(30f) + "add(-13, 17) ".assertExprEquals(4f) + "add(+13,-17)".assertExprEquals(-4f) + "add( 13, 17.0)".assertExprEquals(30f) + "add(13, 17.0)".assertExprEquals(30f) + " add(-13.0, 17);".assertExprEquals(4f) + "add(-13.0, -17.0) ".assertExprEquals(-30f) + } + + @Test + fun add_num_fun2() { + "sum(13,17)".assertExprEquals(30f) + " sum( 13, 17); ".assertExprEquals(30f) + "sum(-13, 17) ".assertExprEquals(4f) + "sum(+13,-17)".assertExprEquals(-4f) + "sum( 13, 17.0)".assertExprEquals(30f) + "sum(13, 17.0)".assertExprEquals(30f) + " sum(-13.0, 17);".assertExprEquals(4f) + "sum(-13.0, -17.0) ".assertExprEquals(-30f) + } + + @Test + fun add_num_fun3() { + "\$bm_sum(13,17)".assertExprEquals(30f) + " \$bm_sum( 13, 17); ".assertExprEquals(30f) + "\$bm_sum(-13, 17) ".assertExprEquals(4f) + "\$bm_sum(+13,-17)".assertExprEquals(-4f) + "\$bm_sum( 13, 17.0)".assertExprEquals(30f) + "\$bm_sum(13, 17.0)".assertExprEquals(30f) + " \$bm_sum(-13.0, 17);".assertExprEquals(4f) + "\$bm_sum(-13.0, -17.0) ".assertExprEquals(-30f) + } + + + @Test + fun add_vec_fun2(){ + "sum([13, 17], [17, 13]);".assertExprEquals(Vec2(30f,30f)) + "sum([-13, 17], [-17, 13]);".assertExprEquals(Vec2(-30f,30f)) + "sum([13.0, 17.0], [17, 13]);".assertExprEquals(Vec2(30f,30f)) + "sum([-13, 17.0], [-17.0, 13]);".assertExprEquals(Vec2( -30f,30f)) + } + + @Test + fun add_vec_fun3(){ + "\$bm_sum([13, 17], [17, 13]);".assertExprEquals(Vec2(30f,30f)) + "\$bm_sum([-13, 17], [-17, 13]);".assertExprEquals(Vec2(-30f,30f)) + "\$bm_sum([13.0, 17.0], [17, 13]);".assertExprEquals(Vec2(30f,30f)) + "\$bm_sum([-13, 17.0], [-17.0, 13]);".assertExprEquals(Vec2(-30f,30f)) + } +} + diff --git a/skriptie/src/commonTest/kotlin/js/AssignExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/AssignExpressionTest.kt new file mode 100644 index 00000000..18c1c959 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/AssignExpressionTest.kt @@ -0,0 +1,26 @@ +package js + +import expressions.assertExprReturns +import expressions.ret +import io.github.alexzhirkevich.compottie.internal.animation.Vec2 +import kotlin.test.Test + + +class AssignExpressionTest { + + @Test + fun add_sub_mull_div_assign() { + + "var $ret = 13; $ret += 17".assertExprReturns(30f) + "var $ret = 56; $ret -=17".assertExprReturns(39f) + "var $ret = 5; $ret *=2".assertExprReturns(10f) + "var $ret = 13; $ret *= -2*2".assertExprReturns(-52f) + "var $ret = 144; $ret /=6".assertExprReturns(24f) + + "var $ret = []; $ret[0] = 5; $ret[1] = 10; $ret[(5-5)] += 10-3; $ret[5-4] += (4*2);" + .assertExprReturns(Vec2(12f, 18f)) + + "var $ret = []\n $ret[0] = 5\n $ret[1] = 10\n $ret[(5-5)] += 10-3\n $ret[5-4] += (4*2);" + .assertExprReturns(Vec2(12f, 18f)) + } +} diff --git a/skriptie/src/commonTest/kotlin/js/BooleanExpressionsTest.kt b/skriptie/src/commonTest/kotlin/js/BooleanExpressionsTest.kt new file mode 100644 index 00000000..f2867357 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/BooleanExpressionsTest.kt @@ -0,0 +1,60 @@ +package js + +import expressions.assertSimpleExprEquals +import kotlin.test.Test + +class BooleanExpressionsTest { + + @Test + fun and() { + "true && true".assertSimpleExprEquals(true) + "true && true && true".assertSimpleExprEquals(true) + + "false && false".assertSimpleExprEquals(false) + "true && false".assertSimpleExprEquals(false) + "false && true".assertSimpleExprEquals(false) + + "true && true && false".assertSimpleExprEquals(false) + "false && true && true".assertSimpleExprEquals(false) + } + + @Test + fun or() { + "true || true".assertSimpleExprEquals(true) + "true || true || true".assertSimpleExprEquals(true) + + "true || false".assertSimpleExprEquals(true) + "false || true".assertSimpleExprEquals(true) + "false || false".assertSimpleExprEquals(false) + + "true || true || false".assertSimpleExprEquals(true) + "false || true || true".assertSimpleExprEquals(true) + "false || false || true".assertSimpleExprEquals(true) + "false || false || false".assertSimpleExprEquals(false) + } + + @Test + fun and_or_order() { + "true && true || false".assertSimpleExprEquals(true) + "false || true && true".assertSimpleExprEquals(true) + + "(false && true) || (true && true)".assertSimpleExprEquals(true) + "false && true || true && true".assertSimpleExprEquals(true) + "(false && true || true) && true".assertSimpleExprEquals(true) +// + "true || false && false".assertSimpleExprEquals(true) + "(true || false) && false".assertSimpleExprEquals(false) + } + + @Test + fun with_different_source() { + "false || 1 == 1".assertSimpleExprEquals(true) + "true && 1 == 2".assertSimpleExprEquals(false) + + "(1 == 2) || false || (1+1) == 2".assertSimpleExprEquals(true) + "1 == 2 || false || (1+1) == 2".assertSimpleExprEquals(true) + "1 == 1 && 2 == 2".assertSimpleExprEquals(true) + + "1 == 2 && 2 == 1 || 2 * 2 == 4".assertSimpleExprEquals(true) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/ConditionExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/ConditionExpressionTest.kt new file mode 100644 index 00000000..217edbdf --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/ConditionExpressionTest.kt @@ -0,0 +1,39 @@ +package js + +import expressions.assertExprReturns +import expressions.ret +import kotlin.test.Test + +class ConditionExpressionTest { + + @Test + fun if_with_else() { + "var $ret = 1; if (true) { $ret = $ret+1 }".assertExprReturns(2f) + "var $ret = 1; if (true) $ret+=1".assertExprReturns(2f) + "var $ret = 1; if (1==1) { $ret = $ret+1 }".assertExprReturns(2f) + "var $ret = 1; if (1==1) $ret +=1".assertExprReturns(2f) + "var $ret = 1; if (true) { $ret = $ret+1;$ret = $ret+1 }".assertExprReturns(3f) + "var $ret = 1; if (true) { $ret = $ret+1\n$ret +=1 }".assertExprReturns(3f) + + "var $ret = 1; if (false) { $ret = $ret } else { $ret = $ret+1 }" + .assertExprReturns(2f) + + "var $ret = 1.0; if (false) $ret +=1" + .assertExprReturns(1f) + + "var $ret = 1; if (1 != 1) $ret = 0 else { $ret = $ret+1;$ret = $ret+1 }" + .assertExprReturns(3f) + + "var $ret = 1; if (!(1 == 1)) $ret = 0 else { $ret +=1;$ret = $ret+1 }" + .assertExprReturns(3f) + + "var $ret = 0; if (true) { if (true) { $ret +=1 } }" + .assertExprReturns(1f) + + "var $ret = 0; if (true) { if (false) { $ret = 0 } else { $ret += 1 } }" + .assertExprReturns(1f) + + "var $ret = 0; if (true) if (false) $ret = 0 else $ret += 1 " + .assertExprReturns(1f) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/CustomFunctionsTest.kt b/skriptie/src/commonTest/kotlin/js/CustomFunctionsTest.kt new file mode 100644 index 00000000..9c900d25 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/CustomFunctionsTest.kt @@ -0,0 +1,78 @@ +package js + +import expressions.assertExprReturns +import expressions.ret +import kotlin.test.Test + +class CustomFunctionsTest { + + @Test + fun creation() { + """ + var $ret; + function test(a,b) { return sum(a,b) } + $ret = test(1,2) + """.trimIndent().assertExprReturns(3f) + + """ + var $ret; + function test(a,b) { + return sum(a,b) + } + $ret = test(1,2) + """.trimIndent().assertExprReturns(3f) + + """ + var $ret; + function test(a,b) + { + let x = b + 1 + return sum(a,x) + } + $ret = test(1,2) + """.trimIndent().assertExprReturns(4f) + } + + + @Test + fun defaultArgs(){ + """ + var $ret; + function test(a, b = 2) + { + return sum(a,b) + } + $ret = test(1) + """.trimIndent().assertExprReturns(3f) + + """ + var $ret; + function test(a, b = 2) + { + return sum(a,b) + } + $ret = test(2,3) + """.trimIndent().assertExprReturns(5f) + + """ + var $ret; + function test(a = 1, b = 2) + { + return sum(a,b) + } + $ret = test() + """.trimIndent().assertExprReturns(3f) + } + + @Test + fun namedArgs(){ + """ + var $ret; + function test(a, b) + { + return sum(a,b) + } + $ret = test(b = 2, a = 1) + """.trimIndent().assertExprReturns(3f) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/DivExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/DivExpressionTest.kt new file mode 100644 index 00000000..8cb9857a --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/DivExpressionTest.kt @@ -0,0 +1,59 @@ +package js + +import expressions.assertExprEquals +import expressions.assertSimpleExprEquals +import io.github.alexzhirkevich.compottie.internal.animation.Vec2 +import kotlin.test.Test + + +class DivExpressionTest { + + + @Test + fun div_num_expr() { + "26 / 2.0".assertExprEquals(13f) + "-26 / 2.0".assertExprEquals(-13f) + "-26 / -2.0".assertExprEquals(13f) + "-52 / -2.0 / 2".assertExprEquals(13f) + "div(26, 2)".assertExprEquals(13f) + "\$bm_div(26, 2)".assertExprEquals(13f) + "div(-52, 2) / 2".assertExprEquals(-13f) + + "'15'/'3'".assertExprEquals(5f) + "15/'3'".assertExprEquals(5f) + "'15'/3".assertExprEquals(5f) + } + + @Test + fun div_vec_num_expr() { + "[26, 42] / 2;".assertExprEquals(Vec2(13f, 21f)) + "[-26, 42] / 2;".assertExprEquals(Vec2(-13f, 21f)) + "div([26, 42], 2)".assertExprEquals(Vec2(13f, 21f)) + "\$bm_div([26, 42], 2)".assertExprEquals(Vec2(13f, 21f)) + "div([26, 42], 2) / 2".assertExprEquals(Vec2(6.5f, 10.5f)) + } + + @Test + fun div_sum_order() { + "2 - 2 / 2".assertExprEquals(1L) + "2 + 2 / 2".assertExprEquals(3L) + "(2 + 4) / 2".assertExprEquals(3L) + "(6 - 2) * 2".assertExprEquals(8L) + } + + @Test + fun number_type_propagation() { + "4/2".assertSimpleExprEquals(2L) + "4/2.0".assertSimpleExprEquals(2.0) + "4.0/2".assertSimpleExprEquals(2.0) + "4.0/2.0".assertSimpleExprEquals(2.0) + } + + @Test + fun division_by_zero() { + "1/0".assertSimpleExprEquals(Double.POSITIVE_INFINITY) + "1.0/0".assertSimpleExprEquals(Double.POSITIVE_INFINITY) + "1/0.0".assertSimpleExprEquals(Double.POSITIVE_INFINITY) + "1.0/0.0".assertSimpleExprEquals(Double.POSITIVE_INFINITY) + } +} diff --git a/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt b/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt new file mode 100644 index 00000000..78bef4b3 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt @@ -0,0 +1,24 @@ +package js + +import io.github.alexzhirkevich.skriptie.ecmascript.invoke +import io.github.alexzhirkevich.skriptie.javascript.JS + +internal fun String.runJs() : Any { + return JS().invoke(this) +} +// +//internal fun String.assertSimpleExprEquals(expected : Any) { +// "var $ret=$this".assertSimpleExprReturns(expected) +//} +// +//internal fun String.assertSimpleExprReturns(expected : Any) { +// assertEquals(expected, runJs()) +//} +// +//internal fun String.assertSimpleExprEquals(expected : Double) { +// "var $ret=$this".assertSimpleExprReturns(expected) +//} +// +//internal fun String.assertSimpleExprReturns(expected : Double) { +// assertEquals(expected, runJs() as Double, 0.00001) +//} diff --git a/skriptie/src/commonTest/kotlin/js/LoopExpressionsTest.kt b/skriptie/src/commonTest/kotlin/js/LoopExpressionsTest.kt new file mode 100644 index 00000000..b2f3e04e --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/LoopExpressionsTest.kt @@ -0,0 +1,35 @@ +package js + +import expressions.assertExprReturns +import expressions.ret +import kotlin.test.Test + +class LoopExpressionsTest { + + @Test + fun whileLoop() { + """ + var $ret = 0 + while($ret != 3) { + $ret += 1 + } + """.trimIndent().assertExprReturns(3L) + + """ + var $ret = 0 + while($ret < 3) + $ret += 1 + + """.trimIndent().assertExprReturns(3L) + } + + @Test + fun doWhileLoop() { + """ + var $ret = 0 + do { + $ret+=1 + } while($ret != 3) + """.trimIndent().assertExprReturns(3L) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/MathExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/MathExpressionTest.kt new file mode 100644 index 00000000..8c33f0f6 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/MathExpressionTest.kt @@ -0,0 +1,40 @@ +package js + +import expressions.assertExprEquals +import expressions.assertSimpleExprEquals +import kotlin.math.PI +import kotlin.test.Test + +class MathExpressionTest { + + @Test + fun math() { + "Math.sin(Math.PI/2)".assertSimpleExprEquals(1.0) + "Math.sin(Math.PI)".assertSimpleExprEquals(0.0) + "Math.sin(0)".assertSimpleExprEquals(0.0) + "Math.sin(0.0)".assertSimpleExprEquals(0.0) + + "Math.cos(Math.PI)".assertSimpleExprEquals(-1.0) + "Math.cos(0)".assertSimpleExprEquals(1.0) + "Math.cos(0.0)".assertSimpleExprEquals(1.0) + + "Math.sqrt(16)".assertSimpleExprEquals(4.0) + "Math.sqrt(16.0)".assertSimpleExprEquals(4.0) + + "radiansToDegrees(Math.PI)".assertSimpleExprEquals(180.0) + "radiansToDegrees(-Math.PI)".assertSimpleExprEquals(-180.0) + + "degreesToRadians(90)".assertExprEquals(PI.toFloat()/2) + "degreesToRadians(-180)".assertExprEquals(-PI.toFloat()) + + "Math.imul(3,4)".assertSimpleExprEquals(12L) + "Math.imul(-5,12)".assertSimpleExprEquals(-60L) + "Math.imul(0xffffffff, 5)".assertSimpleExprEquals(-5L) + "Math.imul(0xfffffffe, 5)".assertSimpleExprEquals(-10L) + + "Math.hypot(3, 4)".assertSimpleExprEquals(5.0) + "Math.hypot(5, 12)".assertSimpleExprEquals(13.0) + "Math.hypot(3, 4, 5)".assertSimpleExprEquals(7.071068) + "Math.hypot(-5)".assertSimpleExprEquals(5.0) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/ModExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/ModExpressionTest.kt new file mode 100644 index 00000000..3880f611 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/ModExpressionTest.kt @@ -0,0 +1,15 @@ +package js + +import expressions.assertExprEquals +import kotlin.test.Test + + +class ModExpressionTest { + + @Test + fun mod_num(){ + "mod(25,4)".assertExprEquals(1f) + "mod(25.1,4)".assertExprEquals(1.1f) + } +} + diff --git a/skriptie/src/commonTest/kotlin/js/MulExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/MulExpressionTest.kt new file mode 100644 index 00000000..9e2d8f32 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/MulExpressionTest.kt @@ -0,0 +1,42 @@ +package js + +import expressions.assertExprEquals +import io.github.alexzhirkevich.compottie.internal.animation.Vec2 +import kotlin.test.Test + + +class MulExpressionTest { + + + @Test + fun mul_num_expr() { + "13 * 17.0".assertExprEquals(221f) + "-13 * -17".assertExprEquals(221f) + "-13.0 * 17 * 2".assertExprEquals(-442f) + "mul(13, 17)".assertExprEquals(221f) + "\$bm_mul(13, 17)".assertExprEquals(221f) + "mul(-13, 17) * 2".assertExprEquals(-442f) + + "'10'*'3'".assertExprEquals(30f) + "10*'3'".assertExprEquals(30f) + "'10'*3".assertExprEquals(30f) + } + + @Test + fun mul_vec_num_expr() { + "[13, 17] * 2;".assertExprEquals(Vec2(26f, 34f)) + "[-13, 17] * 2;".assertExprEquals(Vec2(-26f, 34f)) + "mul([13, 17], 2)".assertExprEquals(Vec2(26f, 34f)) + "mul(2,[13, 17])".assertExprEquals(Vec2(26f, 34f)) + "\$bm_mul([13, 17], 2)".assertExprEquals(Vec2(26f, 34f)) + "mul([13, 17], 2) * 2".assertExprEquals(Vec2(52f, 68f)) + } + + @Test + fun mul_sum_order() { + "2 + 2 * 2".assertExprEquals(6f) + "2 - 2 * 2".assertExprEquals(-2f) + "(2 + 2) * 2".assertExprEquals(8f) + "(2 - 3) * 2".assertExprEquals(-2f) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/NumberFormatTest.kt b/skriptie/src/commonTest/kotlin/js/NumberFormatTest.kt new file mode 100644 index 00000000..483095c7 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/NumberFormatTest.kt @@ -0,0 +1,18 @@ +package js + +import expressions.assertSimpleExprEquals +import kotlin.test.Test + +class NumberFormatTest { + + @Test + fun test(){ + "123".assertSimpleExprEquals(123L) + "123.1".assertSimpleExprEquals(123.1) + ".1".assertSimpleExprEquals(.1) + + "0xff".assertSimpleExprEquals(255L) + "0b11".assertSimpleExprEquals(3L) + "0o123".assertSimpleExprEquals(83L) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/SubExpressionTest.kt b/skriptie/src/commonTest/kotlin/js/SubExpressionTest.kt new file mode 100644 index 00000000..73bfbce2 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/SubExpressionTest.kt @@ -0,0 +1,62 @@ +package js + +import expressions.assertExprEquals +import io.github.alexzhirkevich.compottie.internal.animation.Vec2 +import kotlin.test.Test + +class SubExpressionTest { + + + @Test + fun sub_num_expr() { + "13-17".assertExprEquals(-4f) + "13 - 17".assertExprEquals(-4f) + "-13-17".assertExprEquals(-30f) + "13.0-17.0".assertExprEquals(-4f) + "13 - 17.0".assertExprEquals(-4f) + "-13.0 -17".assertExprEquals(-30f) + + "'10'-'3'".assertExprEquals(7f) + "10-'3'".assertExprEquals(7f) + "'10'-3".assertExprEquals(7f) + } + + @Test + fun sub_vec_expr() { + "[13, 17] + [17, 13];".assertExprEquals(Vec2(30f, 30f)) + "[-13, 17] + [-17, 13];".assertExprEquals(Vec2(-30f, 30f)) + "[13.0, 17.0] + [17, 13];".assertExprEquals(Vec2(30f, 30f)) + "[-13, 17.0] + [-17.0, 13];".assertExprEquals(Vec2(-30f, 30f)) + } + + @Test + fun sub_num_fun() { + "sub(13,17)".assertExprEquals(-4f) + "sub(13, 17)".assertExprEquals(-4f) + "sub(-13, 17)".assertExprEquals(-30f) + "sub(13.0,17.0)".assertExprEquals(-4f) + "sub(13 , 17.0)".assertExprEquals(-4f) + "sub(-13.0 ,17)".assertExprEquals(-30f) + } + + @Test + fun sub_num_fun2() { + "\$bm_sub(13,17)".assertExprEquals(-4f) + "\$bm_sub(13, 17)".assertExprEquals(-4f) + "\$bm_sub(-13, 17)".assertExprEquals(-30f) + "\$bm_sub(13.0,17.0)".assertExprEquals(-4f) + "\$bm_sub(13 , 17.0)".assertExprEquals(-4f) + "\$bm_sub(-13.0 ,17)".assertExprEquals(-30f) + } + + @Test + fun sub_vec_fun() { + "sub([13, 17] , [17, 13]);".assertExprEquals(Vec2(-4f, 4f)) + "sub([-13, 17] , [-17, 13]);".assertExprEquals(Vec2(4f, 4f)) + "sub([13.0, 17.0] , [17, 13]);".assertExprEquals(Vec2(-4f, 4f)) + "sub([-13, 17.0] , [-17.0, 13]);".assertExprEquals(Vec2(4f, 4f)) + + "sub(vec2=[20.0, 15], vec1=[10, 10]);".assertExprEquals(Vec2(-10f, -5f)) + } +} + diff --git a/skriptie/src/commonTest/kotlin/js/SyntaxExpressionsTest.kt b/skriptie/src/commonTest/kotlin/js/SyntaxExpressionsTest.kt new file mode 100644 index 00000000..bbe5c0b4 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/SyntaxExpressionsTest.kt @@ -0,0 +1,95 @@ +package js + +import expressions.ret +import kotlin.test.Test +import kotlin.test.assertFails + +class SyntaxExpressionsTest { + + @Test + fun newline_property() { + """ + Math + .imul(3,4) + .toString() + """.trimIndent().assertSimpleExprEquals("12") + + """ + Math + + .imul(3,4) + + .toString() + """.trimIndent().assertSimpleExprEquals("12") + + """ + Math + + .imul(3,4) + + .toString() + ; + """.trimIndent().assertSimpleExprEquals("12") + + + assertFails { + """ + Math + .imul(3,4); + .toString() + """.trimIndent().runJs() + } + } + + @Test + fun increment_decrement() { + "let $ret = 5; $ret++".assertSimpleExprReturns(6L) + "let $ret = 5; ++$ret".assertSimpleExprReturns(6L) + + "let $ret = 5; $ret--".assertSimpleExprReturns(4L) + "let $ret = 5; --$ret".assertSimpleExprReturns(4L) + + "let x = 5; let $ret = --x".assertSimpleExprReturns(4L) + "let x = 5; let $ret = ++x".assertSimpleExprReturns(6L) + "let x = 5; let $ret = x--".assertSimpleExprReturns(4L) + "let x = 5; let $ret = x++".assertSimpleExprReturns(6L) + } + + @Test + fun variable_scopes() { + """ + var $ret; + if(true){ + var x = 5 + } + $ret = x + """.trimIndent().assertSimpleExprReturns(5L) + + assertFails { + """ + var $ret = 1; + if(true){ + let x = 5 + } + $ret = x + """.trimIndent().runJs() + } + } + + @Test + fun constMutating() { + assertFails { + """ + const x = 1; + x++ + """.trimIndent().runJs() + } + } + + @Test + fun tryCatch() { + assertFails { + "let x = [123".runJs() + } + } +} \ No newline at end of file